hctf2018-admin(复现)
Ebounce
撰写于 2019年 07月 18 日

今天研究了一下去年hctf2018-admin的题解,总之还是涨了不少姿♂势,接下来是一个复现的过程,首先利用docker进行环境的复现,然后进入题目页面,先F12看看
<!--more-->

Part1



发现这里会给个hint1,大概是要求我们使用admin的身份登陆,先注册账户,后登陆,在登陆后的改密码页面找到了hint2,将所给地址源码下载下来,便是这道题的源码了,使用flask框架写的,先看看routes.py,看看都允许访问哪些路径吧
routes.py:

@app.route('/code')
...//生成验证码

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

@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)

@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')
...

@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)

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

@app.errorhandler(404)
.....

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

这里省略了一些不太会用到的路径,接着进行代码审计,发现了一个问题这里在注册时,会将我们传入的用户名数据转变为小写,

并且在改密码处也有相同的操作

这里使用的不是python自带的lower()方法,而是自定义的strlower(),或许有什么端倪,先来看看.

这里使用了一个nodeprep.prepare()进行小写操作,我们并不知道是什么,然后进行谷歌

发现这是Twisted包中的一个方法,接着查看requirements.txt发现当前环境Twisted为10.2.0,然后去github上查看当前最新版本已经19.2.1了,说明这里会有问题

最后找到两篇文章
<a href="https://labs.spotify.com/2013/06/18/creative-usernames/
">有关Twisted某个版本转换问题
Unicode编码安全问题
最后得到信息:

说明在11.0版本以下的Twisted均存在这样的问题,我们环境版本为10.2.0刚好符合,同时提到nodeprep.prepare()不具有幂等性,即对某个数据反复进行某个操作,总会得到相同的结果举个例子

这种情况a字符串无论进行多少次lower()都会得到一个结果,因此lower()方法具有幂等性,而nodeprep.prepare(),则会出现下面的情况

也就是说,同样举个例子,也就说:
ᴺ--->N--->n(Unicode中相同的字母和符好,对应的编码可能不同,如:Ω和Ω)
nodeprep.prepare()会执行类似于这样的转换,在第一次进行转化时会将其匹配为类似的大写字母,然后第二次才将其匹配为小写字母,两次相同操作所得结果不一样,因此这个方法不具有幂等性.

有关利用


首先我们注册一个
ᴬᴰmin的账户,这时候我们传入的数据会进行一次转化,这时ᴬᴰmin-->ADmin,服务器端会判断该用户是否存在,然后成功注册

登陆该账号,因为登陆时也会被进行一次转化,所以使用ᴬᴰmin登陆,但后台的账号是ADmin

我们可以看到账号登陆成功,且为ADmin

然后执行一次更改密码操作,改密码时也会进行一次转化,这时我们便从ADmin-->admin,完成了admin账户的密码更改操作,这时候再登陆admin账号,就可以得到flag了

Part2


有关session欺骗


session欺骗似乎是非预期解法,但这好像是flask框架本身存在的问题,这里再次贴出P神的文章

传送门: 客户端 session 导致的安全问题


简要来讲,就是由于flask框架是不带数据库的,因此无法像PHP,或者JAVA等语言发放一个查询session的id,如PHPSESSIONID,JSSESSIONID,这些能够将真正的session存放在服务器上,在客户端上仅仅只是一个查询的凭证,而flask框架由于没有这样的功能,因此只能将session发放给客户端储存,虽然flask对session进行了签名,防止了用户篡改session,根据源码签名的内容是直接连接在session值后面,也就是说session的所有内容是在客户端可见的,而由于没有对session进行加密,因此如果用户知道了secret_key的情况下,就会造成安全问题,而由于源码可见,通过查找发现,secret_key已知.

这样我们就可以进行session伪造了,并且通过index.html发现了得到flag的条件

由前面源码可知,这里的参数name是直接从session中取出来的,并没有进行任何处理,因此session伪造可行.
先随便注册一个账号获取当前session

然后进行解密,这里直接使用P神的解session脚本进行解密.
decodeflask.py:

#!/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()))

得到结果:

然后修改name参数为admin,为了防止u_id也要被查询,因此改成1,然后利用github脚本
flask-session-cookie-manager
进行加密得到:

修改当前session,刷新最后得到flag:


-------------------------the end-----------------------

hctf2018-admin(复现)

今天研究了一下去年hctf2018-admin的题解,总之还是涨了不少姿♂势,接下来是一个复现的过程,首先利用docker进行环境的复现,然后进入题目页面,先F12看看
<!--more-->

Part1



发现这里会给个hint1,大概是要求我们使用admin的身份登陆,先注册账户,后登陆,在登陆后的改密码页面找到了hint2,将所给地址源码下载下来,便是这道题的源码了,使用flask框架写的,先看看routes.py,看看都允许访问哪些路径吧
routes.py:

@app.route('/code')
...//生成验证码

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

@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)

@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')
...

@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)

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

@app.errorhandler(404)
.....

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

这里省略了一些不太会用到的路径,接着进行代码审计,发现了一个问题这里在注册时,会将我们传入的用户名数据转变为小写,

并且在改密码处也有相同的操作

这里使用的不是python自带的lower()方法,而是自定义的strlower(),或许有什么端倪,先来看看.

这里使用了一个nodeprep.prepare()进行小写操作,我们并不知道是什么,然后进行谷歌

发现这是Twisted包中的一个方法,接着查看requirements.txt发现当前环境Twisted为10.2.0,然后去github上查看当前最新版本已经19.2.1了,说明这里会有问题

最后找到两篇文章
<a href="https://labs.spotify.com/2013/06/18/creative-usernames/
">有关Twisted某个版本转换问题
Unicode编码安全问题
最后得到信息:

说明在11.0版本以下的Twisted均存在这样的问题,我们环境版本为10.2.0刚好符合,同时提到nodeprep.prepare()不具有幂等性,即对某个数据反复进行某个操作,总会得到相同的结果举个例子

这种情况a字符串无论进行多少次lower()都会得到一个结果,因此lower()方法具有幂等性,而nodeprep.prepare(),则会出现下面的情况

也就是说,同样举个例子,也就说:
ᴺ--->N--->n(Unicode中相同的字母和符好,对应的编码可能不同,如:Ω和Ω)
nodeprep.prepare()会执行类似于这样的转换,在第一次进行转化时会将其匹配为类似的大写字母,然后第二次才将其匹配为小写字母,两次相同操作所得结果不一样,因此这个方法不具有幂等性.

有关利用


首先我们注册一个
ᴬᴰmin的账户,这时候我们传入的数据会进行一次转化,这时ᴬᴰmin-->ADmin,服务器端会判断该用户是否存在,然后成功注册

登陆该账号,因为登陆时也会被进行一次转化,所以使用ᴬᴰmin登陆,但后台的账号是ADmin

我们可以看到账号登陆成功,且为ADmin

然后执行一次更改密码操作,改密码时也会进行一次转化,这时我们便从ADmin-->admin,完成了admin账户的密码更改操作,这时候再登陆admin账号,就可以得到flag了

Part2


有关session欺骗


session欺骗似乎是非预期解法,但这好像是flask框架本身存在的问题,这里再次贴出P神的文章

传送门: 客户端 session 导致的安全问题


简要来讲,就是由于flask框架是不带数据库的,因此无法像PHP,或者JAVA等语言发放一个查询session的id,如PHPSESSIONID,JSSESSIONID,这些能够将真正的session存放在服务器上,在客户端上仅仅只是一个查询的凭证,而flask框架由于没有这样的功能,因此只能将session发放给客户端储存,虽然flask对session进行了签名,防止了用户篡改session,根据源码签名的内容是直接连接在session值后面,也就是说session的所有内容是在客户端可见的,而由于没有对session进行加密,因此如果用户知道了secret_key的情况下,就会造成安全问题,而由于源码可见,通过查找发现,secret_key已知.

这样我们就可以进行session伪造了,并且通过index.html发现了得到flag的条件

由前面源码可知,这里的参数name是直接从session中取出来的,并没有进行任何处理,因此session伪造可行.
先随便注册一个账号获取当前session

然后进行解密,这里直接使用P神的解session脚本进行解密.
decodeflask.py:

#!/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()))

得到结果:

然后修改name参数为admin,为了防止u_id也要被查询,因此改成1,然后利用github脚本
flask-session-cookie-manager
进行加密得到:

修改当前session,刷新最后得到flag:


-------------------------the end-----------------------

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论