题目很有趣,但是也没有做出来几道
Custom Web Server (1)
这个题目是一道服务器漏洞的题目
给了附件我们打开看一下
这是一个用c语言写的服务器,跟据这个代码和前面的页面的测试发现,这个服务器只能请求
if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;
这些后缀的页面,根据dockerfile我们可以知道我们的那个flag的名字是flag.txt
这里的化我们经过测试发现这里面会有路径穿越
这里有路径穿越但是这里面限制了文件后缀,我们无法穿越到根目录去读取这个flag.txt
我们知道,穿越目录到根目录之后再次穿越还是根目录
这里我们看一下打开文件的逻辑
这里的缓冲区的大小做了限制,这里我们就可以利用缓冲区溢出来截断文件后缀
我们可以看到这里是先做了判断再次对这个文件名进行检查,判断是否出现了溢出
这里限制的字节大小是1024,加上前面的public/,前面的七个字节,我们要填充1017个字节后面文件名后缀的进行截断
/../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../flag.txt.js
比赛的时候想到了这个缓冲区溢出,但是没有想到还能和截断联系在一起
Custom Web Server (2)
这两道题的服务器一摸一样,但是与(1)不同的是,这个服务器加载了nginx的代理,加上了代理之后,我们就无法进行路径穿越来读取文件了
但是两个服务器可能会发生请求走私来访问到我们想要的东西
这个没有弄明白
Mystiz’s Mini CTF (1)
这个是一个ctf的平台让我们找出来这个平台的漏洞
这个是要我们找到这个Hack this site!这个web挑战的flag
我们先注册一个账号,登进去看一下
这里就是那个挑战,有一个解
给了源码我们看一下源码
在web\migrations\versions这个目录下面发现了一个sql语句,我们可以观察一下,这个数据库里面有什么
我们可以看到这里面有三个表,分别是challenge,user,attempt,这里的话,这三个表分别记录的是我们的挑战,用户和提交记录
这是向这三个表里面插入数据,用户表中有两个用户分别是admin,player
这个challenge里面有挑战,这个攻击表里面也有数据是这个player提交的flag,我们的目的就是要获取这个player用户提交的flag
这里面我们看一下这个minnctf的大致框架
在这里面我们可以看到这个应该是文件的入口点,我们也可以在Dockerfile文件中看到,是先运行这个app.py
我们先看一下这个服务的整体框架然后再去分析这个入口文件
这里面有个migrations和app,这个app就是我们的主要的框架内容,下面的migrantions是我们的数据库迁移数据
migrations/
│
├── alembic.ini # Alembic 配置文件
├── env.py # 迁移环境配置
├── script.py.mako # 迁移脚本模板
└── versions/ # 迁移脚本目录
├── 1234567890ab_add_field_to_user.py # 迁移脚本
└── 9876543210bc_remove_old_table.py # 迁移脚本
这个就是这个目录,接下来看一下这个app这个目录
这里我们可以看到这个框架的入口,这里面有
app/
│
├── models/
├── static/
├── templates/
├── views
├── app.py
└── ...
app.py就是框架的入口文件,其他的models/目录下面是框架的核心模板,static/目录下都是一些静态的资源,templates/下面是一些模板渲染的,views/是视图目录
我们先把文件的入口点分析一下
这里面是初始化了各个功能,数据库,视图,数据库迁移,登入,限制请求
这个初始化了数据库
接下来是这个初始化了这个视图模块
这个模块是定义了一些api,这个api我们后面有大用,到后面我们再分析这个
接下就是这个command.py
这个模块,是进行的数据库迁移操作
接下来初始化用于管理 Flask 应用中的用户会话
最后一个就是初始化了限制
这里面限制了我们的请求次数
入口文件分析完成之后,接下来我们通过功能点来分析这个代码的具体作用
这个models,
大概有三个功能点,攻击,挑战,用户
这些都是一些类,用于向数据库中添加攻击,挑战,用户
最后我们可以看到这个密码储存到数据库中是密文储存的
他是调用的了这个compute_hash()方法
这个加密的是salt.sha256(密码)
这个加密我们到后面也有大用
这里的话这个静态资源我们就不分析了
接下来我们重点分析一下这个views模块
这里我们看到这个确实是比较复杂我们一层一层的分析
首先是这个__init__.py
from flask import Blueprint, request, jsonify
from flask.views import MethodView
import collections
from app.views import pages
from app.views.api import users
from app.views.api import challenges
from app.views.api.admin import challenges as admin_challenges
from app.models.user import User
from app.models.challenge import Challenge
from app.models.attempt import Attempt
class GroupAPI(MethodView):
init_every_request = False
def __init__(self, model):
self.model = model
self.name_singular = self.model.__tablename__
self.name_plural = f'{self.model.__tablename__}s'
def get(self):
# the users are only able to list the entries related to them
items = self.model.query_view.all()
group = request.args.get('group')
if group is not None and not group.startswith('_') and group in dir(self.model):
grouped_items = collections.defaultdict(list)
for item in items:
id = str(item.__getattribute__(group))
grouped_items[id].append(item.marshal())
return jsonify({self.name_plural: grouped_items}), 200
return jsonify({self.name_plural: [item.marshal() for item in items]}), 200
def register_api(app, model, name):
group = GroupAPI.as_view(f'{name}_group', model)
app.add_url_rule(f'/api/{name}/', view_func=group)
def init_app(app):
# Views
app.register_blueprint(pages.route, url_prefix='/')
# API
app.register_blueprint(users.route, url_prefix='/api/users')
app.register_blueprint(challenges.route, url_prefix='/api/challenges')
app.register_blueprint(admin_challenges.route, url_prefix='/api/admin/challenges')
register_api(app, User, 'users')
register_api(app, Challenge, 'challenges')
register_api(app, Attempt, 'attempts')
这里面就是注册了几个这个api路由我们结合这着前面的功能点进行分析
我们访问这个api/challenges
发现了这个返回的json数据包
这里面我们看到这个有个这个group参数,分组查询,看不懂代码什么意思
我们抓个包看一下
这个历史记录里面有请求这个group这个参数,我们试着访问一下
这里好像是要分组查询这个东西
我们也可以按照其他的分组进行查询
这个功能我们搞明白了之后,我们看一下下面的这个pages.py这个
from flask import Blueprint, request, jsonify, render_template, flash, url_for, redirect
from http import HTTPStatus, HTTPMethod
from wtforms_sqlalchemy.orm import model_form
from flask_login import login_user, logout_user, current_user
from app.db import db
from app.limiter import limiter
from app.models.user import User
route = Blueprint('pages', __name__)
@route.route('/', methods=[HTTPMethod.GET])
def homepage():
return render_template('homepage.html', current_user=current_user), HTTPStatus.OK
@route.route('/register/', methods=[HTTPMethod.GET])
def register():
return render_template('register.html'), HTTPStatus.OK
@route.route('/register/', methods=[HTTPMethod.POST])
def register_submit():
user = User()
UserForm = model_form(User)
form = UserForm(request.form, obj=user)
if not form.validate():
flash('Invalid input', 'warning')
return redirect(url_for('pages.register'))
form.populate_obj(user)
user_with_same_username = User.query_view.filter_by(username=user.username).first()
if user_with_same_username is not None:
flash('User with the same username exists.', 'warning')
return redirect(url_for('pages.register'))
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for('pages.homepage'))
@route.route('/login/', methods=[HTTPMethod.GET])
def login():
return render_template('login.html'), HTTPStatus.OK
@route.route('/login/', methods=[HTTPMethod.POST])
@limiter.limit("2/minute")
def login_submit():
username = request.form.get('username')
password = request.form.get('password')
user = User.query_view.filter_by(username=username).first()
if user is None:
flash('User not found.', 'warning')
return redirect(url_for('pages.login'))
if not user.check_password(password):
flash('Invalid password.', 'warning')
return redirect(url_for('pages.login'))
login_user(user)
return redirect(url_for('pages.homepage'))
@route.route('/logout/', methods=[HTTPMethod.GET])
def logout():
logout_user()
return redirect(url_for('pages.homepage'))
@route.route('/challenges/', methods=[HTTPMethod.GET])
def list_challenges():
return render_template('challenges.html'), HTTPStatus.OK
@route.route('/scoreboard/', methods=[HTTPMethod.GET])
def scoreboard():
return render_template('scoreboard.html'), HTTPStatus.OK
@route.route('/admin/challenges/', methods=[HTTPMethod.GET])
def manage_challenges():
if not current_user.is_authenticated:
flash('You are not logged in.', 'warning')
return redirect(url_for('pages.login'))
if not current_user.is_admin:
return render_template('403.html'), HTTPStatus.FORBIDDEN
return render_template('admin/challenges.html'), HTTPStatus.OK
这里面都是各个路由的具体实现
下面我们看一下这个views这个模块的其他的
这个api这个路由下面是是一些管理员的管理的一些操作,
一些验证flag正确的操作
现在我们对这个框架已经分析完了,现在我们知道的是,我们有一个用户叫做player提交了flag
就只有他提交了flag,我们要获得这个flag,但是我们没有密码,通过数据库我们知道了,他的密码就是六位数字
爆破肯定是不行的,这里面限制了请求次数
这里面有几个api我们可以试着访问一下,这里面可能会暴露敏感信息
这里面是没有这个字段的,所以默认按user进行分组,我们试一下用这个password进行分组
确实是用泄露了密码,但是这个密码是加密过后的,我们暴露破解一下(前面分析过这个密码是六位)
import os
import sys
import string
import hashlib
PLAYER = "973fbe24.2e029c76eade955c007327d37168122318dfecb5cbf99b8f708cf940828de820"
SALT = PLAYER.split('.')[0]
for a in string.hexdigits:
for b in string.hexdigits:
for c in string.hexdigits:
for d in string.hexdigits:
for e in string.hexdigits:
for f in string.hexdigits:
result = SALT + '.' + hashlib.sha256(f'{SALT}/{a}{b}{c}{d}{e}{f}'.encode()).hexdigest()
if result == PLAYER:
print(f"User: player, password: {a}{b}{c}{d}{e}{f}")
sys.exit()
成功爆出来了这个密码
我们登入一下
这里面我们看到了这个web题目已经被攻破,这里面,我们用同样的方法来泄露这个flag
至此我们的挑战一完成
理一下思路
就是我们通过查看数据库,我们知道了这个挑战被player用户攻破了
我们经过代码审计发现,无法爆破这个用户名和密码
但是这里面我们可以通过这个api来泄露这个用户的哈希值
我们爆破这个哈希值登入进去
然后就用同样的方式来泄露这个flag
Mystiz’s Mini CTF (2)
这个挑战2和挑战1一样是同一个网站,但是题目的描述变了,这里面要我们获得的是A placeholder challenge的这个flag
这个挑战我们看数据库知道这个挑战是一年之后才发布的
db.session.add(Challenge(id=1337, title='A placeholder challenge', description=f'Many players complained that the CTF is too guessy. We heard you. As an apology, we will give you a free flag. Enjoy - <code>{FLAG_2}</code>.', category=Category.MISC, flag=FLAG_2, score=500, solves=0, released_at=RELEASE_TIME_BACKUP))
这肯定要我们登入管理员的后台了
但是我们肯定不能像方法一一样,用来爆破这个admin的密码
这个密码有66位,我们肯定爆破不了
我们看一下他验证这个admin身份的逻辑
这个是通过这个is_admin,来验证这个是否是admin用户
再这个注册功能里面,我们可以看到这里面是直接获得我们传入的参数,这个参数我们抓个包看一下
这里面的参数我们可以加一下这个is_admin=true
这里就污染了原来的参数is_admin=true
注册成功之后,我们看到这个用户已经是admin用户了
我们看一下这个/api/admin/challenges
至此这两个挑战都已经完成