沙箱逃逸技术教学文档
概述
沙箱逃逸指在受限代码执行环境中绕过安全限制,执行未授权操作或访问被禁止资源的技术。本技术广泛存在于各类编程语言(Python、JavaScript、Java等)及系统环境(容器、虚拟机)的安全评估与CTF竞赛中。
沙箱逃逸通用原理
沙箱的本质
沙箱的核心目标是限制代码的执行环境,防止恶意代码访问敏感资源。其根本矛盾在于功能与安全的权衡:沙箱必须保留足够的语言能力来执行用户代码,而这些保留的能力往往成为逃逸的突破口。
通用逃逸方法论
| 方法论 | 原理 | 适用场景 |
|---|---|---|
| 继承链遍历 | 利用面向对象继承体系,从受限对象回溯到基类或全局命名空间 | Python、Java、JavaScript、Ruby |
| 原型链污染 | 修改对象原型,影响所有继承该原型的对象 | JavaScript、Lua |
| 反射/内省 | 通过语言提供的反射API访问私有成员或绕过访问控制 | Java、Python、C# |
| 序列化/反序列化 | 利用反序列化过程重建对象,绕过构造函数的安全检查 | Java、Python、PHP、JavaScript |
| 内存corruption | 利用缓冲区溢出、UAF等漏洞修改沙箱内存结构 | C/C++、Rust、WebAssembly |
| 侧信道攻击 | 通过时间、错误信息、资源消耗等间接泄露信息 | 所有语言 |
各语言沙箱逃逸技术详解
Python 沙箱逃逸
字节码与代码对象层面的攻击
手动构造 CodeType 对象
import types
# 直接构造代码对象,绕过AST检查
code = types.CodeType(
0, 0, 0, 0, 0, 0,
b'd\x01\x00d\x02\x00l\x00\x00m\x01\x00}\x01\x00...', # 原始字节码
(), (), (), '', '', 0, b''
)
func = types.FunctionType(code, globals())
func()
利用已编译的字节码
# 从现有函数提取字节码
().__class__.__base__.__subclasses__()[0].__init__.__code__.co_code
# 通过 marshal 加载预编译字节码
import marshal
exec(marshal.loads(b'...'))
例题:pickled magic (JailCTF2024)
- 限制:仅可使用
numpy模块,find_class硬编码检查module == 'numpy',过滤双下划线,无法访问__builtins__。 - 突破:利用
__reduce__魔术方法。Pickle 反序列化时会调用对象的__reduce__方法,其返回的(callable, args)会被执行。
import pickle
class Evil:
def __reduce__(self):
from numpy import loadtxt
return (loadtxt, ('flag.txt',))
pickled = pickle.dumps(Evil())
print(pickled.hex())
例题:ezJail (2024ZZUCTF招新赛)
- 沙箱代码限制:代码长度
M=14,仅 ASCII 字符,禁用exec,eval等关键词。 - 逃逸思路:利用递归调用和变量覆盖。
# 用户输入分三次:
> N=input();f(N) # 第一步,定义N
> f(input()) # 第二步,将字符串“f(input())”赋值给N
> M=99;f(N) # 第三步,修改M并执行f(N),最终执行 f(input())
执行流程:N=input();f(N) → N="f(input())" → f(N) 执行 f("f(input())") → M=99;f(N) → 最终 f(input()) 在 M=99 的限制下可执行任意代码。
帧对象与栈操作
获取调用栈帧
例题:lost in transit (JailCTF 2024)
- 限制:只能使用
getattr和无参数调用(),起点为0(int),无__globals__可用。 - 突破路径:在无参数调用约束下,通过
getattr和()遍历 Python 对象继承链,利用列表/字典的迭代器方法定位到sys.breakpointhook,触发 PDB 交互式调试器实现任意代码执行。
编码与隐写技巧
例题:no nonsense (JailCTF 2024)
绕过限制:
- 禁用不用括号调用函数 → 使用装饰器链。
- AST检查变量名,变量名不能在源码中出现 → 使用 Unicode 全角字符。
- 禁用
\n(结束输入) → 用\r代替换行。
原理:Python 会将全角英文字母等 Unicode 字符转换为 ASCII 字符,从而绕过源码变量名检查。配合装饰器链实现无括号函数调用,用\r作为换行符。
例题:parity 1 (JailCTF 2024)
- 限制:每个字符必须满足
i % 2 == ord(v) % 2(索引奇偶性 = ASCII 奇偶性),只允许 ASCII 字符。 - 绕过原理:利用引号的奇偶性差异构造任意字符。
例如,构造字符'p'(ASCII 112, 偶):- 索引 0:
'(ASCII 39, 奇),索引为偶,不满足条件。 - 索引 1:
p(ASCII 112, 偶),索引为奇,不满足条件。 - 索引 2:
'(ASCII 39, 奇),索引为偶,不满足条件。
构造为"'p'"时: - 索引 0:
"(ASCII 34, 偶),索引为偶,满足条件。 - 索引 1:
p(ASCII 112, 偶),索引为奇,不满足条件。 - 索引 2:
"(ASCII 34, 偶),索引为偶,满足条件。
因此需要通过字符串拼接,利用eval和引号奇偶性组合生成任意 payload。
- 索引 0:
例题:parity 2 (JailCTF 2024)
与 parity 1 类似,但允许使用 lambda 表达式,移除了 _ 的检查。
其他 Python 逃逸技术
- 内存与对象生命周期攻击:利用垃圾回收残留、引用计数操纵。
- 协程与异步特性利用:利用
async/await或生成器状态绕过。 - 环境变量与外部数据泄露:读取进程环境、遍历文件描述符。
- 第三方库与 C 扩展攻击:利用已加载的 C 扩展或
ctypes。 - 类与元编程深度利用:动态类创建、描述符协议利用、属性访问链劫持。
- 输入输出与交互劫持:标准输入重定向、伪终端分配。
- 时间/顺序与竞态条件:延迟执行技巧、弱引用回调。
- Python 版本特性差异:利用 Python 2/3 差异、海象表达式 (3.8+)、
match/case(3.10+)。 - 解释器内部机制:篡改代码对象属性。
例题:JellyJail (JailCTF2024)
- 场景:Jelly(基于 Python 的 DSL)解释器
jelly_eval内部调用eval或exec。 - 突破:沙箱只检查外层输入的 2 个字符,但 Jelly 语言内部的
Ɠ命令调用了input()和exec,形成了二次输入通道。
例题:ASMaaS (Assembly as a Service)
- 场景:服务使用
pwntools的asm函数将输入编译为 shellcode。 - 突破:GNU 汇编器的
.incbin指令可用于包含文件内容。 - EXP:使用
.incbin指令读取flag.txt。
Bash 沙箱逃逸
| 技术类别 | 原理 | 示例 Payload |
|---|---|---|
| 算术扩展注入 | 解析 $(( )) 时先执行 $(cmd) |
a[$(id)] $(($((1+$(whoami)))) |
| Brace 扩展副作用 | 花括号展开时执行命令 | {$(id),} echo {$(cat /etc/passwd),} |
| 进程替换注入 | 利用 <(cmd) 或 >(cmd) |
cat <(ls) echo >(id) |
| 数组索引注入 | 数组下标解析时执行命令 | a[$(id)]=1 b[$(cat flag)] |
| 字符串替换执行 | ${var/pattern/replacement} 中的代码执行 |
${a/$(id)/b} |
| eval 二次解析 | 利用 eval 或 printf -v 重新解析 |
eval "$(id)" printf -v a "$(cat flag)" |
| 路径通配注入 | 利用通配符匹配触发命令 | /*/$(id) |
| Here-document 注入 | 利用 here-doc 的变量/命令替换 | cat <<EOF\n$(id)\nEOF |
| IFS/分隔符绕过 | 修改 IFS 改变词法分析行为 | IFS=;$(id) |
| 别名/函数劫持 | 覆盖内置命令 | id() { cat flag; }; id |
例题:Blind Calc (JailCTF 2024)
- 场景:交互式算术计算器。
- 突破:Bash 的算术扩展在解析表达式时,会先执行命令替换
$(cmd),再尝试将结果作为数字运算。即使最终语法报错,命令已经执行。 - EXP:提交包含命令替换的“算术表达式”。
Ruby 沙箱逃逸
send 方法注入
例题:SUS-Calculator (JailCTF 2024)
- 漏洞核心:
send方法的元编程特性。send可调用对象的任意方法(包括私有/保护方法)。 - 沙箱逻辑:
op.to_sym将用户输入的字符串转为符号(如"system"→:system),left.send(op, right)将参数传给被调用的方法。 - 利用:通过
send调用Kernel#system执行任意系统命令。Kernel模块已混入Object,因此可直接调用。
JavaScript 沙箱逃逸
vm2 模块漏洞
CVE-2023-29199: Proxy 对象逃逸
- 原理:vm2 对
Promise的处理存在缺陷,通过Proxy对象的then属性可以访问到宿主环境的Error构造函数,进而获取process对象。 - 关键路径:在顶层代码中,
this引用由contextObject参数创建的宿主环境对象。this.constructor获取宿主Object构造函数,this.constructor.constructor获取宿主Function构造函数。通过宿主Function执行任意代码并访问process。
CVE-2024-XXXX: Function.prototype.call 劫持
最新的 vm2 漏洞利用了 Function.prototype.call 的拦截。关键点:vm2 对 localPromise 和 globalPromise 的处理不一致,async 函数返回的是 globalPromise,其 then/catch 方法未经过充分的沙箱包装。
vm 模块与上下文隔离
Node.js 内置的 vm 模块存在上下文共享问题,并非完全隔离。
例题:js-without-getattr (JailCTF 2024)
- 限制:
- 长度 ≤ 480 字符。
- 禁用字符:`<.;,;.>:[\x09\x0a\x0b\x0c\x0d]upq"x20``。
Esprima解析结果中禁用:变量声明、函数调用、成员调用、数组索引访问等。
- 绕过方法:
- 用
with代替.进行成员访问。 - 用
&&代替;执行多条语句。 - 通过
with将console.log覆盖为eval。 - Base64 编码绕过字符串黑名单。
- 用
process.mainModule.require代替require。
- 用
Java 沙箱逃逸
SecurityManager 绕过
CVE-2024-27348: Apache HugeGraph Gremlin 沙箱逃逸
- 根本原因:
HugeSecurityManager仅基于线程名称进行访问控制,而非基于调用栈的完整权限检查。 - 利用:通过创建新线程并设置特定名称来绕过权限检查。
Jinjava 模板引擎沙箱绕过 (CVE-2025-59340)
- 漏洞原理:Jinjava 通过黑名单限制危险方法,但忽略了
JavaType的构造器。 - 利用链:
- 模板内置变量
_int3rpr3t3r_暴露JinjavaInterpreter实例。 - 通过
config字段获取ObjectMapper。 constructFromCanonical可以构造任意类的JavaType。readValue反序列化时实例化目标类,造成沙箱逃逸。
- 模板内置变量
Lua 沙箱逃逸
Redis Lua 沙箱历史漏洞
CVE-2022-0543: Debian/Ubuntu 包污染
- 原因:Debian 打包 Redis 时未移除 Lua 的
package库,允许加载任意共享库。 - 利用:通过 Redis 的
EVAL命令执行 Lua 代码加载恶意库。
CVE-2025-49844: Redis Lua Use-After-Free
- 类型:Lua 垃圾回收器的 UAF 漏洞。
- 利用流程:
- 构造特殊的 Lua 表,触发垃圾回收。
- 在
__gc元方法中操作已释放内存。 - 修改 Lua 虚拟机内部结构,关闭沙箱标志。
- 执行任意系统命令。
工业设备中的 Lua 逃逸 (CVE-2025-41688)
- 目标:MB Connect Line 工业路由器 (mbNet HW1)。
- 漏洞:未记录的 Lua 函数
os.execute直接暴露。
Lua 元表与调试库攻击
即使标准库被移除,Lua 的元表 (metatable) 和调试库 (debug) 仍可能提供强大能力用于逃逸。
Rust 与 WebAssembly 沙箱逃逸
WebAssembly 的内存安全假象
CVE-2023-26489: Wasmtime 沙箱逃逸
- 漏洞位置:Cranelift 编译器的指令选择阶段。
- 技术细节:Wasm 的
i8x16.swizzle指令实现存在边界检查错误。恶意构造的 Wasm 模块可读写线性内存之外的地址,通过覆盖 Wasm 虚拟机内部结构(如函数表)实现代码执行。
WASI 的能力模型绕过
WASI (WebAssembly System Interface) 采用基于能力的安全模型,但实现缺陷可能导致逃逸,例如权限提升或越权访问文件系统。
Rust 的 unsafe 块与 FFI
Rust 本身不提供沙箱,但常用于构建沙箱 (如 wasmtime)。unsafe 块和 FFI (外部函数接口) 是主要的风险点,可能引入内存安全漏洞或直接调用系统 API。
操作系统级沙箱逃逸
Seccomp BPF 绕过技术
Seccomp (Secure Computing Mode) 是 Linux 的系统调用过滤器。
允许列表绕过 (UIUCTF 2024)
- 场景:Seccomp 只允许
openat、mmap、writev等少数系统调用。 - 绕过策略:
- 使用
openat打开文件 (需使用绝对路径)。 - 使用
lseek获取文件大小。 - 使用
mmap将文件映射到内存。 - 使用
writev写入 stdout。writev的规则只检查 fd 是否大于0x3e8,通过dup2将stdout复制到0x3f9即可绕过。
- 使用
chroot 逃逸的多种技巧
chroot 并非安全边界。
- 方法 1:未 chdir 的 chroot:
chroot后未调用chdir("/"),可利用相对路径../../访问根目录。 - 方法 2:双重 chroot:在
chroot环境中再次调用chroot并配合chdir可能逃逸。 - 方法 3:已打开的文件描述符:在
chroot前打开的文件描述符(如目录句柄)可以用于访问外部文件系统。
Namespace 与 Capabilities 逃逸
Docker 容器逃逸
- CVE-2024-41110: Docker Engine 授权绕过:通过构造
Content-Length: 0的请求,绕过 AuthZ 插件,执行未授权命令。 - Capabilities 滥用:容器拥有过多 Linux 能力(如
CAP_SYS_ADMIN,CAP_DAC_READ_SEARCH)可能导致逃逸。
沙箱设计黄金法则
- 纵深防御:单层防护不可行。需要组合语言层、运行时层、操作系统层等多层防护。
- 最小权限与功能裁剪:
- 错误做法:黑名单过滤危险函数。
- 正确做法:白名单只允许必要操作;静态链接裁剪后的自定义运行时;使用专用 Unikernel/MicroVM 而非通用容器。
- 不可变基础设施:移除不必要的工具 (
/bin/sh,curl)、动态链接库,甚至采用无文件系统的纯内存环境。
未来趋势:AI 时代的沙箱挑战
- LLM 沙箱逃逸 (Prompt Injection 2.0):攻击面扩大至自然语言与代码的边界,传统语法检查失效。
- 模型权重窃取:通过侧信道攻击(时间、功耗、缓存)从 ML 推理沙箱中提取模型参数。
参考资源
- JailCTF 2024/2025: https://ctf.pyjail.club/
- Chrome DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/
- Wasmtime Security: https://docs.wasmtime.dev/security.html
- Typhon 工具: https://github.com/LamentXU123/Typhon