关键要点
- Java 序列化在很多库中引入了安全漏洞。
- 对序列化进行模块化处于开放讨论状态。
- 如果序列化能够成为模块,开发人员将能够将其从攻击表面上移除。
- 移除其他模块可以消除它们所带来的风险。
- 插桩提供了一种编织安全控制的方法,提供现代化的防御机制。
多年来,Java 的序列化功能饱受安全漏洞和 zero-day 攻击,为此赢得了“持续奉献的礼物”和“第四个不可饶恕的诅咒”的绰号。
作为回应,OpenJDK 贡献者团队讨论了一些用于限制序列化访问的方法,例如将其提取到可以被移除的jigsaw 模块中,让黑客无法攻击那些不存在的东西。
一些文章(例如“序列化必须死”)提出了这样的建议,将有助于防止某些流行软件(如VCenter 6.5)的漏洞被利用。
什么是序列化?
自从 1997 年发布 JDK 1.1 以来,序列化已经存在于 Java 平台中。
它用于在套接字之间共享对象表示,或者将对象及其状态保存起来以供将来使用(反序列化)。
在 JDK 10 及更低版本中,序列化作为 java.base 包和 java.io.Serializable 方法的一部分存在于所有的系统中。
GeeksForGeeks 对序列化的工作原理进行了详细的描述。
有关更多如何使用序列化的代码示例,可以参看 Baeldung 对 Java 序列化的介绍。
序列化的挑战和局限
序列化的局限主要表现在以下两个方面:
- 出现了新的对象传输策略,例如 JSON、XML、Apache Avro、Protocol Buffers 等。
- 1997 年的序列化策略无法预见现代互联网服务的构建和攻击方式。
进行序列化漏洞攻击的基本前提是找到对反序列化的数据执行特权操作的类,然后传给它们恶意的代码。为了理解完整的攻击过程,可以参看 Matthias Kaiser 在 2015 年发表的“ Exploiting Deserialization Vulnerabilities in Java ”一文,其中幻灯片第 14 页开始提供了相关示例。
其他大部分与序列号有关的安全研究都是基于Chris Frohoff、Gabriel Lawrence 和Alvaro Munoz 的工作成果。
序列化在哪里?如何知道我的应用程序是否用到了序列化?
要移除序列化,需要从 java.io 包开始,这个包是 java.base 模块的一部分。最常见的使用场景是:
- 实现 Serializable 接口和(可选)serialversionuid 长整型字段。
- 使用 ObjectInputStream 或 ObjectOutputStream。
- 使用严重依赖序列化的库,例如:Xstream、Kryo、BlazeDS 和大多数应用程序服务器。
使用这些方法的开发人员应考虑使用其他存储和读回数据的替代方法。Eishay Smith 发布了几个不同序列化库的性能指标。在评估性能时,需要在基准度量指标中包含安全方面的考虑。默认的Java 序列化“更快”一些,但漏洞也会以同样的速度找上门来。
我们该如何降低序列化缺陷的影响?
项目Amber 包含了一个关于将序列化API 隔离出来的讨论。我们的想法是将序列化从java.base 移动到单独的模块,这样应用程序就可以完全移除它。在确定 JDK 11 功能集时并没有针对该提议得出任何结果,但可能会在未来的 Java 版本中继续进行讨论。
通过运行时保护来减少序列化暴露
一个可以监控风险并自动化可重复安全专业知识的系统对于很多企业来说都是很有用的。Java 应用程序可以将 JVMTI 工具嵌入到安全监控系统中,通过插桩的方式将传感器植入到应用程序中。Contrast Security 是这个领域的一个免费产品,它是 JavaOne 大会的 Duke's Choice 大奖得主。与其他软件项目(如 MySQL 或 GraalVM)类似, Contrast Security 的社区版对开发人员是免费的。
将运行时插桩应用在 Java 安全性上的好处是它不需要修改代码,并且可以直接集成到 JRE 中。
它有点类似于面向切面编程,将非侵入式字节码嵌入到源端(远程数据进入应用程序的入口)、接收端(以不安全的方式使用数据)和转移(安全跟踪需要从一个对象移动到另一个对象)。
通过集成每个“接收端”(如 ObjectInputStream),运行时保护机制可以添加额外的功能。在从 JDK 9 移植反序列化过滤器之前,这个功能对序列化和其他攻击的类型(如 SQL 注入)来说至关重要。
集成这个运行时保护机制只需要修改启动标志,将 javaagent 添加到启动选项中。例如,在 Tomcat 中,可以在 bin/setenv.sh 中添加这个标志:
CATALINA_OPTS=-javaagent:/Users/ecostlow/Downloads/Contrast/contrast.jar
启动后,Tomcat 将会初始化运行时保护机制,并将其注入到应用程序中。关注点的分离让应用程序可以专注在业务逻辑上,而安全分析器可以在正确的位置处理安全性。
其他有用的安全技术
在进行维护时,可以不需要手动列出一长串东西,而是使用像 OWASP Dependency-Check 这样的系统,它可以识别出已知安全漏洞的依赖关系,并提示进行升级。也可以考虑通过像 DependABot 这样的系统进行库的自动更新。
虽然用意很好,但默认的 Oracle 序列化过滤器存在与 SecurityManager 和相关沙箱漏洞相同的设计缺陷。因为需要混淆角色权限并要求提前了解不可知的事物,限制了这个功能的大规模采用:系统管理员不知道代码的内容,所以无法列出类文件,而开发人员不了解环境,甚至 DevOps 团队通常也不知道系统其他部分(如应用程序服务器)的需求。
移除未使用模块的安全隐患
Java 9 的模块化 JDK 能够创建自定义运行时镜像,移除不必要的模块,可以使用名为jlink 的工具将其移除。这种方法的好处是黑客无法攻击那些不存在的东西。
从提出模块化序列化到应用程序能够实际使用以及使用其他序列化的新功能需要一段时间,但正如一句谚语所说:“种树的最佳时间是二十年前,其次是现在”。
剥离 Java 的原生序列化功能还应该为大多数应用程序和微服务提供更好的互操作性。通过使用标准格式(如 JSON 或 XML),开发人员可以更轻松地在使用不同语言开发的服务之间进行通信——与 Java 7 的二进制 blob 相比,python 微服务通常具有更好的读取 JSON 文档的集成能力。不过,虽然 JSON 格式简化了对象共享,针对 Java 和.NET 解析器的“ Friday the 13th JSON attacks ”证明了银弹是不存在的(白皮书)。
在进行剥离之前,序列化让然保留在 java.base 中。这些技术可以降低与其他模块相关的风险,在序列化被模块化之后,仍然可以使用这些技术。
为 Apache Tomcat 8.5.31 模块化 JDK 10 的示例
在这个示例中,我们将使用模块化的 JRE 来运行 Apache Tomcat,并移除任何不需要的 JDK 模块。我们将得到一个自定义的 JRE,它具有更小的攻击表面,仍然能够用于运行应用程序。
确定需要用到哪些模块
第一步是检查应用程序实际使用的模块。OpenJDK 工具 jdeps 可以对 JAR 文件的字节码执行扫描,并列出这些模块。像大多数用户一样,对于那些不是自己编写的代码,我们根本就不知道它们需要哪些依赖项或模块。因此,我使用扫描器来检测并生成报告。
列出单个 JAR 文件所需模块的命令是:
jdeps -s JarFile.jar
它将列出模块信息:
tomcat-coyote.jar -> java.base
tomcat-coyote.jar -> java.management
tomcat-coyote.jar -> not found
最后,每个模块(右边的部分)都应该被加入到一个模块文件中,成为应用程序的基本模块。这个文件叫作 module-info.java,文件名带有连字符,表示不遵循标准的 Java 约定,需要进行特殊处理。
下面的命令组合将所有模块列在一个可用的文件中,在 Tomcat 根目录运行这组命令:
find . -name *.jar ! -path "./webapps/*" ! -path "./temp/*" -exec jdeps -s {} \; | sed -En "s/.* -\> (.*)/ requires \1;/p" | sort | uniq | grep -v "not found" | xargs -0 printf "module com.infoq.jdk.TomcatModuleExample{\n%s}\n"
这组命令的输出将被写入 lib/module-info.java 文件,如下所示:
module com.infoq.jdk.TomcatModuleExample{
requires java.base;
requires java.compiler;
requires java.desktop;
requires java.instrument;
requires java.logging;
requires java.management;
requires java.naming;
requires java.security.jgss;
requires java.sql;
requires java.xml.ws.annotation;
requires java.xml.ws;
requires java.xml;
}
这个列表比整个 Java 模块列表要短得多。
下一步是将这个文件放入 JAR 中:
javac lib/module-info.java
jar -cf lib/Tomcat.jar lib/module-info.class
最后,为应用程序创建一个 JRE:
jlink --module-path lib:$JAVA_HOME/jmods --add-modules ThanksInfoQ_Costlow --output dist
这个命令的输出是一个运行时,包含了运行应用程序所需的恰到好处的模块,没有任何性能开销,也没有了未使用模块中可能存在的安全风险。
与基础 JDK 10 相比,只用了 98 个核心模块中的 19 个。
java --list-modules
com.infoq.jdk.TomcatModuleExample
java.activation@10.0.1
java.base@10.0.1
java.compiler@10.0.1
java.datatransfer@10.0.1
java.desktop@10.0.1
java.instrument@10.0.1
java.logging@10.0.1
java.management@10.0.1
java.naming@10.0.1
java.prefs@10.0.1
java.security.jgss@10.0.1
java.security.sasl@10.0.1
java.sql@10.0.1
java.xml@10.0.1
java.xml.bind@10.0.1
java.xml.ws@10.0.1
java.xml.ws.annotation@10.0.1
jdk.httpserver@10.0.1
jdk.unsupported@10.0.1
运行这个命令后,就可以使用 dist 文件夹中的运行时来运行应用程序。
看看这个列表:部署插件(applet)消失了,JDBC(SQL)消失了,JavaFX 也不见了,很多其他模块也消失了。从性能角度来看,这些模块不再产生任何影响。从安全角度来看,黑客无法攻击那些不存在的东西。保留应用程序所需的模块非常重要,因为如果缺少这些模块,应用程序也无法正常运行。
关于作者
Erik Costlow 是甲骨文的 Java 8 和 9 产品经理,专注于安全性和性能。他的安全专业知识涉及威胁建模、代码分析和安全传感器增强。在进入技术领域之前,Erik 是一位马戏团演员,可以在三轮垂直独轮车上玩火。
查看英文原文: The State of Java Serialization
评论