为什么有些 Java API 会绕过标准的 SecurityManager 检查?

信息安全 应用安全 访问控制 爪哇 权限
2021-09-07 02:50:12

在 Java 中,权限检查通常由 SecurityManager 处理。为了防止不受信任的代码调用特权代码并利用特权代码中的某些错误,SecurityManager 会检查整个调用堆栈;如果堆栈跟踪中的任何调用者没有特权,则默认情况下该请求被拒绝。至少,这就是标准 SecurityManager 检查的工作方式

但是,一些特殊的 Java API 遵循不同的规则。他们绕过标准的 SecurityManager 检查,并替换为较弱的检查。特别是,它们只检查直接调用者,而不是整个调用堆栈。(有关详细信息,请参阅Java 安全编码指南的指南 9-8。特殊 API 包括,例如,、Class.forName()Class.getMethod()。)

为什么?为什么这些特殊的 API 会绕过标准检查并替代较弱的检查?而且,为什么这是安全的?换句话说,为什么他们只检查直接调用者就足够了?这不是重新引入了标准 SecurityManager 检查旨在防御的所有风险吗?

我是在阅读最近的 Java 零日漏洞分析时才知道这一点的(CVE-2012-4681)。该分析解构了漏洞利用的工作原理。除其他外,攻击涉及利用这些特殊 API 进行的较弱检查。特别是,恶意 Java 代码设法获取对受信任系统类的引用(通过单独的错误),然后欺骗受信任系统类调用这些特殊 API 之一。生成的权限检查仅查看其直接调用者,看到直接调用者是受信任的,并允许该操作——即使该操作最初是由不受信任的代码发起的。因此,较弱的检查不会阻止攻击,但据我所知,使用标准 SecurityManager 检查可以防止这种攻击(因为调用者的调用者不受信任)。换句话说,

但是,我知道 Java 设计者是聪明人。我怀疑 Java 设计者一定已经考虑过这些问题,并且有充分的理由绕过标准检查并用较弱的检查代替这些特殊的 API——或者,至少,他们认为他们有充分的理由证明这是安全的。所以,也许我错过了一些东西。

任何人都可以对此有所了解吗?Java 设计者是否搞砸了这些特殊的 API,或者是否有正当的理由来替换较弱的检查?

编辑 9/1:我不是在问这个漏洞是如何工作的;我想我理解这个漏洞是如何工作的。我也没有问为什么在这个特定的例子中,调用这些特殊 API 的可信代码有问题。相反,我在问为什么特殊的 API——比如Class.forName(),Class.getMethod()等等——被指定并实现为使用非标准的较弱权限检查(只看直接调用者)而不是标准的 SecurityManager 权限检查(看在整个调用堆栈)。这个设计决策(对那些特殊的 API 使用较弱的权限检查)允许最近的漏洞,因此很容易批评设计决策。但是,我想这样做可能有一些很好的理由,我想知道这些可能是什么。

2个回答

(这只是对“为什么”的一般评论,而不是您所暗示的具体攻击。)

不幸的是,Java 设计人员发现,从结构上讲,很有可能把自己画到一个角落里。例如,有 injava.io in的类java.net,它们都参与了 I/O。让我们假设给定的 JVM 在 中具有特殊的操作系统交互本机代码java.io.FileDescriptor,这允许它执行send()receive()系统调用(Sun/Oracle JVM 不是这种情况,但它可能发生在另一个 JVM 中,并且确实至少在我曾经写过的那个)。为了强制执行沙盒语义,这些方法当然不是 public

很自然地,执行java.net.Socket想使用这些方法。但是,根据命名约定Socket是位于java.net包中的类,而不是java.io. java.io.FileDescriptor考虑到即使从不受信任的代码调用它也必须这样做(不受信任的代码可以打开套接字,尽管不是到每个目的地),它如何访问 的非公共方法?主要有两种方式:

  • 在中添加一些“桥接”native方法java.net.Socket,将调用转发到中的方法java.io.FileDescriptor原生代码对包和可见性嗤之以鼻;本机代码可以做任何事情。

  • 允许代码java.net进行一些反射以访问其他包中的非公共方法。可以做到这一点的代码可以做任何事情,因为它可以修改SecurityManager自己使用的数据结构(嘿,我记得你在十年前的一次 Usenet 讨论中向我指出了这一点,我们都使用了我们的真实姓名)。因此,不能将无限反射授予所有人,但是,在我在这里描述的情况下,必须授予特定系统包 ( java.net) 中的代码,即使代码是从不受信任的小程序调用的。

第二种方法需要弱化安全模型。实际上,它是非常通用的:如果不受信任的小程序必须在某个时候做任何有用的事情,它必须能够访问系统(如果只是显示计算结果或将其发送回服务器),这需要一种门。本机代码就是这样一个门。安全弱化模型是另一种门,它具有留在“纯 Java”世界中的额外好处(我可以理解,在 JVM 维护团队中,本机代码可能不受欢迎,因为它使事情变得更加昂贵)。不好的一面是,通过削弱安全模型,攻击面大大扩大:现在,packages-with-privileges 中的所有 Java 代码都变得至关重要。要重用@Hendrik 的类比,它是关于给根setuid位到大量代码(来自java.*软件包)。

特别是,Java 削弱安全模型的方式是指定一些特殊的 API——如Class.findClass()Class.newInstance()Class.getMethod()和其他与反射相关的 API——使用较弱的权限检查。在上面的示例中,这允许受信任的系统代码java.net.Socket使用这些特殊的 API 来获取对非公共方法的java.io.FileDescriptor引用并反射性地调用它。

  • 例如,java.net.Socket代码可以Class.getMethod()用来获取对 in 非公共方法的引用java.io.FileDescriptor(这是允许的,因为 is 的直接调用者Class.getMethod()java.net.Socket()这是受信任的代码),然后调用它。

    请注意,此实施策略依赖于Class.getMethod()使用较弱的权限检查。Class.getMethod()如果使用标准权限检查,它就是行不通的。如果不受信任java.net.Socket的代码调用 ,然后java.net.Socket代码调用Class.getMethod(),则标准权限检查将拒绝此调用,因为调用堆栈中的某处存在不受信任的代码,并且这些java.net.Socket东西将无法正常工作(在非攻击场景中)。相比之下,弱化安全模型中使用的较弱权限检查确实允许这样做。

因此,较弱的权限检查有助于 Java 设计人员摆脱他们描绘的困境。

假设,对于系统代码和命名约定的“完美”结构,少数本地方法就足够了,但是在 JVM 系统库代码中存在特定的 is-immediate-caller-from-a-trusted-package 调用表明所述代码的结构并不完美。

概括

一些受信任的方法在内部需要更多权限才能完成其任务。但是,如果这些方法存在错误,它们可能会允许不受信任的攻击者以更高的权限级别执行不希望的操作。

背景

我们在操作系统层面有一个非常相似的情况。在 Unix 上,有 setuid/setgid 标志和 sudo 命令。它们允许非特权用户执行内部需要更高级别特权的任务。

例如普通用户是不允许修改的/etc/shadow但我们希望用户能够更改他们的密码。因此,该passwd命令被标记为受信任(setuid root)并允许修改该文件。当然,它必须执行自己的安全检查。

同样的情况也适用于 Java 沙箱。例如,不允许非特权代码动态调用方法。但是系统的某些部分需要在内部执行此操作。

就像该passwd命令只允许普通用户更改自己的密码一样,MethodFinder.findMethod()它应该只允许受信任的代码调用任意方法。

到目前为止,一切都很好。

类加载漏洞

ClassFinder.findClass()是这样一个值得信赖的方法。它使用调用代码的权限加载其他类。就像passwd让您更改自己的密码一样。

但是如果出现错误,它会尝试通过加载具有完全权限的类来恢复。如果我们考虑一下passwd,类似的错误会导致passwd更改 root 而不是当前用户的密码。

操作系统将允许passwd更改 root 密码,就像Class.forName()允许ClassFinder.findClass()在错误的安全上下文中加载类一样。

方法调用漏洞

这个有点复杂。值得信赖的方法在Method.invoke()这里。

但是还有第二种受信任的方法,称为MethodFinder.findMethod(). 继续与操作系统类比,将其视为通过 sudo 以 root 权限运行的 shell 脚本。

此方法/程序不验证它的参数,它只是将它们传递给passwd. 现在passwd在受信任的上下文中调用。因此,它会很乐意更改任何人的密码。