导语

这也能偷鸡?

[HCTF 2018]admin

偷鸡

看到有个login,看到题目是admin,于是先随便试试密码,试了123,直接登进去了(啊这

1679299950671

但是感觉应该还有芝士点

分析

这个webapp有login和register两个功能

1679300190273

随便注册一个账号并登陆,发现没有如之前一样显示flag,页面里有个注释说you are not admin,所以是登陆了admin即可获取flag。

1679300505412

在change password里可以看到一个github

1679300619392

但是发现403木得了,看了别人的writeup,是源码

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code

@app.route('/code')
def get_code():
image, code = get_verify_code()
# 图片以二进制形式写入
buf = BytesIO()
image.save(buf, 'jpeg')
buf_str = buf.getvalue()
# 把buf_str作为response返回前端,并设置首部字段
response = make_response(buf_str)
response.headers['Content-Type'] = 'image/gif'
# 将验证码字符串储存在session中
session['image'] = code
return response

@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title = 'hctf')

@app.route('/register', methods = ['GET', 'POST'])
def register():

if current_user.is_authenticated:
return redirect(url_for('index'))

form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)
if session.get('image').lower() != form.verify_code.data.lower():
flash('Wrong verify code.')
return render_template('register.html', title = 'register', form=form)
if User.query.filter_by(username = name).first():
flash('The username has been registered')
return redirect(url_for('register'))
user = User(username=name)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('register successful')
return redirect(url_for('login'))
return render_template('register.html', title = 'register', form = form)

@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
user = User.query.filter_by(username=name).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title = 'login', form = form)

@app.route('/logout')
def logout():
logout_user()
return redirect('/index')

@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])
user = User.query.filter_by(username=name).first()
user.set_password(form.newpassword.data)
db.session.commit()
flash('change successful')
return redirect(url_for('index'))
return render_template('change.html', title = 'change', form = form)

@app.route('/edit', methods = ['GET', 'POST'])
def edit():
if request.method == 'POST':

flash('post successful')
return redirect(url_for('index'))
return render_template('edit.html', title = 'edit')

@app.errorhandler(404)
def page_not_found(error):
title = unicode(error)
message = error.description
return render_template('errors.html', title=title, message=message)

def strlower(username):
username = nodeprep.prepare(username)
return username

unicode欺骗

审计代码可以看到在register、login还有change都用了strlower()将用户名转小写,python自带的转小写是lower(),所以去看看strlower()

查看strlower()函数具体实现

def strlower(username):
username = nodeprep.prepare(username)
return username

这里用的nodeprep.prepare函数,而nodeprep是从Twisted模块导入的,在requirements.txt文件中发现 Twisted==10.2.0,是非常老的版本

https://symbl.cc/en/search/?q=small+capital

small capital进过一次nodeprep.prepare会变成大写字母,在经过一次nodeprep.prepare会变成小写字母。小写字母nodeprep.prepare后还是他自己

所以注册一个ᴬdmin账号,密码自己设置,在注册完成之后变成Admin,登录

1679303107453

修改密码,此时变成admin,相当于直接改了admin的密码

1679303418200

session伪造

在index.html可以看到,如果把session里的name修改为admin,即可让他输出flag

1679309116969

flask中session是存储在客户端cookie中的,也就是存储在本地。flask仅仅对数据进行了签名。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的。

用抄来的脚本解析

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

1679309530183

但是如果我们想要加密伪造生成自己想要的session还需要知道SECRET_KEY,然后我们在config.py里发现了SECRET_KEY

SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'

添加SECRET_KEY,把name改成admin,用github上的脚本加密

https://github.com/noraj/flask-session-cookie-manager

1679309569707

然后用生成的session来request即可

1679308896276

同时找答案的时候看到这个,觉得很不错

https://www.leavesongs.com/PENETRATION/client-session-security.html#

参考

https://www.cnblogs.com/chrysanthemum/p/11722351.html

https://blog.csdn.net/weixin_44677409/article/details/100733581

https://xz.aliyun.com/t/3569