沙箱逃逸小结
字数 7269
更新时间 2026-02-27 12:59:48

沙箱逃逸技术教学文档

概述

沙箱逃逸指在受限代码执行环境中绕过安全限制,执行未授权操作或访问被禁止资源的技术。本技术广泛存在于各类编程语言(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)
绕过限制:

  1. 禁用不用括号调用函数 → 使用装饰器链。
  2. AST检查变量名,变量名不能在源码中出现 → 使用 Unicode 全角字符。
  3. 禁用 \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。

例题: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 内部调用 evalexec
  • 突破:沙箱只检查外层输入的 2 个字符,但 Jelly 语言内部的 Ɠ 命令调用了 input()exec,形成了二次输入通道。

例题:ASMaaS (Assembly as a Service)

  • 场景:服务使用 pwntoolsasm 函数将输入编译为 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 二次解析 利用 evalprintf -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 对 localPromiseglobalPromise 的处理不一致,async 函数返回的是 globalPromise,其 then/catch 方法未经过充分的沙箱包装。

vm 模块与上下文隔离

Node.js 内置的 vm 模块存在上下文共享问题,并非完全隔离。

例题:js-without-getattr (JailCTF 2024)

  • 限制:
    1. 长度 ≤ 480 字符。
    2. 禁用字符:`<.;,;.>:[\x09\x0a\x0b\x0c\x0d]upq"x20``。
    3. Esprima 解析结果中禁用:变量声明、函数调用、成员调用、数组索引访问等。
  • 绕过方法:
    1. with 代替 . 进行成员访问。
    2. && 代替 ; 执行多条语句。
    3. 通过 withconsole.log 覆盖为 eval
    4. Base64 编码绕过字符串黑名单。
    5. process.mainModule.require 代替 require

Java 沙箱逃逸

SecurityManager 绕过

CVE-2024-27348: Apache HugeGraph Gremlin 沙箱逃逸

  • 根本原因:HugeSecurityManager 仅基于线程名称进行访问控制,而非基于调用栈的完整权限检查。
  • 利用:通过创建新线程并设置特定名称来绕过权限检查。

Jinjava 模板引擎沙箱绕过 (CVE-2025-59340)

  • 漏洞原理:Jinjava 通过黑名单限制危险方法,但忽略了 JavaType 的构造器。
  • 利用链:
    1. 模板内置变量 _int3rpr3t3r_ 暴露 JinjavaInterpreter 实例。
    2. 通过 config 字段获取 ObjectMapper
    3. constructFromCanonical 可以构造任意类的 JavaType
    4. 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 漏洞。
  • 利用流程:
    1. 构造特殊的 Lua 表,触发垃圾回收。
    2. __gc 元方法中操作已释放内存。
    3. 修改 Lua 虚拟机内部结构,关闭沙箱标志。
    4. 执行任意系统命令。

工业设备中的 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 只允许 openatmmapwritev 等少数系统调用。
  • 绕过策略:
    1. 使用 openat 打开文件 (需使用绝对路径)。
    2. 使用 lseek 获取文件大小。
    3. 使用 mmap 将文件映射到内存。
    4. 使用 writev 写入 stdout。writev 的规则只检查 fd 是否大于 0x3e8,通过 dup2stdout 复制到 0x3f9 即可绕过。

chroot 逃逸的多种技巧

chroot 并非安全边界。

  • 方法 1:未 chdir 的 chrootchroot 后未调用 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)可能导致逃逸。

沙箱设计黄金法则

  1. 纵深防御:单层防护不可行。需要组合语言层、运行时层、操作系统层等多层防护。
  2. 最小权限与功能裁剪
    • 错误做法:黑名单过滤危险函数。
    • 正确做法:白名单只允许必要操作;静态链接裁剪后的自定义运行时;使用专用 Unikernel/MicroVM 而非通用容器。
  3. 不可变基础设施:移除不必要的工具 (/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
 全屏