java Commons Collection1链学习
java 安全学习笔记
Java反序列化漏洞学习 Commons Collection
参考了这位博主的文章
1 | https://tttang.com/archive/1337/ |
题外话是最近参加hvv,太坐牢了爬回来学会java回回血说是((
环境搭建
在学习Commons Collection利用链之前我们需要先搭建好调试环境,第一步就是安装JDK和maven了这里我推荐多安装多几个版本的JDK,因为有的利用会有版本的限制,我们在分析这些利用链是需要切换指定的JDK。安装好JDK和maven后需要配置好idea的环境,然后需要克隆ysoserial的源码下来,之后的调试我们会在ysoserial的项目中进行,因为ysoserial中配好了我们所需要的库环境,我们只需要利用maven将各种包导入即可。最后就是在项目中创建好我们的测试文件夹。
URLDNS
URLDNS利用链是java反序列化攻击中一种非常经典的“无回显”探测链,用于探测目标是否存在反序列化漏洞,利用的是URL和HashMap的特性,触发一次DNS请求。
核心原理是:
1、java的URL类在HashCOde()时会尝试解析主机名
2、如果这个URL被用作HashMap的key,就会自动触发hashcode()
3、一旦触发,系统会向DNS发起请求解析主机名
4、如果这个主机名是攻击者控制的(比如 xyz.attacker.com),DNS 请求就会到达攻击者的服务器
调用链如下:
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
Commons Collections1
transformer
首先找到commons-collections包的一个接口transformer,有一个transform方法
ConstantTransformer
查找到用法
定位到ConstantTransformer
ConstantTransformer是实现了transformer接口的一个类,构造方法传入一个对象然后transform方法返回对象。
InvokerTransformer
同样的做法
定位到InvokerTransformer包
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
InvokerTransformer是实现了Transformer接口的一个类,InvokerTransformer构造方法传入了3个变量,分别是String类型的方法,一个Clas类型的参数类型列表,还有一个Object类型的参数值列表。
关键在于这段代码
1 | Class cls = input.getClass(); |
通过分析这个类的transform方法可以调用任意方法,这是反序列化能执行任意代码的关键。
仿照文章写一个简单的例子:
反弹成功
ChainedTransformer
反序列化中可以利用InvokerTransformer执行任意对象的任意方法,但在反序列化中我们不能直接传入Runtime对象,因为Runtime类没有实现Serializable接口不能被序列化,如果可以多次调用方法,可以使用Runtime类的getRuntime方法获取Runtime实例。
ChainedTransformer也是实现了Transformer接口的一个类,构造方法传入一个transformer数组。
transform方法遍历执行所有transformer数组的transform方法,且前一个的结果做为后一个的输入,我们可以传入多个InvokerTransformer对象,可以执行多次任意方法。
利用ChainedTransformer改进InvokerTransformer执行命令。使用getMethod方法获得Runtime类的getRumtime方法,之后通过调用Method的invoke方法执行getRumtime获取Runtime实例,最后调用Runtime的exec方法执行系统命令。
利用这个点写出利用代码:
1 | package ysoserial.test; |
执行结果:
TransformedMap
有了执行命令的链我们要看一下有没有什么方法可以自动调用类的transform方法,找到一个TransformedMap类。
TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。构造方法为protected类型,创建对象需要使用decorate方法创建对象。decorate传入一个Map和两个transformer对象,用于将一个普通的Map对象修饰成TransformedMap对象。
TransformedMap分别有3个方法调用了transform。
没看到有地方会直接调用transformKey和transformValue,但是查看注释可以发现当调用setValue时会自动调用checkSetValue方法,但是在TransformedMap中没有方向有setValue方法,查看TransformedMap的父类(AbstractInputCheckedMapDecorator)发现setValue方法。
这里理解一下这段关系:
TransformedMap继承了AbstractInputCheckedMapDecorator
可以使用其中的setValue方法
this.parent就是TransformedMap,AbstractInputCheckedMapDecorator的根父类就是 Map,所以只要找到readObject中调用了Map.setValue即可完成反序列化利用链
AnnotationInvocationHandler
可以看到AnnotationInvocationHandler的readObject方法调用了setValue方法,我们只需要让他反序列化满足上面的条件进入到setValue即可。
(不知道为什么我这里定位不到这个类的源码,这里先跟着文章图片参考一下)))))))
先来看一下到达setValue的条件,先来看一下构造函数。
1 | AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { |
Class<? extends Annotation> var1:
var1 是一个 Class 对象,表示一个注解类型。
<? extends Annotation> 表示 var1 必须是 Annotation 接口的子类型(即一个注解类)。
Map<String, Object> var2:
var2 是一个 Map,键是字符串(注解的成员名),值是对象(注解的成员值)。
1 | Class<?>[] var3 = var1.getInterfaces(); |
var1.getInterfaces() 返回 var1 实现的接口数组。
1 | if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) |
var1.isAnnotation():
检查 var1 是否是一个注解类型。
Class.isAnnotation() 方法返回 true,如果 var1 是一个注解类(用 @interface 定义)。
var3.length == 1:
检查 var1 实现的接口数量是否为 1。
注解类通常只实现一个接口:Annotation。
var3[0] == Annotation.class:
检查 var1 实现的唯一接口是否是 Annotation.class。
确保 var1 是一个合法的注解类型:
它必须是一个注解(isAnnotation())。
它只实现 Annotation 接口(length == 1 且 var3[0] == Annotation.class)。
1 | this.type = var1; |
如果条件满足,说明 var1 是一个合法的注解类型。
将 var1 赋值给 this.type(表示注解的类型)。
将 var2 赋值给 this.memberValues(表示注解的成员值)。
可以看到第一个参数指定var1必须是Annotation的子类,第二个参数是传入一个Map类型的var2。将var1赋值给成员变量type然后将var赋值给成员变量memberValues。然后看readObject要触发setValue的条件。
看readObject函数
1 | private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { |
看到这条语句AnnotationType.getInstance(this.type),var0为我们构造函数传入的vaar0。
setValue 被调用的条件:
成员名(var6)在注解定义中存在(var7 != null)。
成员值(var8)的类型不匹配(!var7.isInstance(var8))。
成员值不是 ExceptionProxy(!(var8 instanceof ExceptionProxy))。
查看getInstance函数:
1 | JavaLangAccess var1 = SharedSecrets.getJavaLangAccess(); |
通过SharedSecrets获取JavaLangAccess实例。这通常用于访问JAVA内部API,JavaLangAccess是一个接口,提供一些JAVA底层的功能
1 | AnnotationType var2 = var1.getAnnotationType(var0); |
通过调用Var1(JavaLangAccess实例)的getAnnotationType(var0)方法,尝试获取var0(传入的 Annotation 类型)对应的 AnnotationType 实例。
如果获取不到,则创建新的 AnnotationType:
1 | if (var2 == null) { |
接着跟到AnonotationType的构造函数
1 |
|
这段代码创建了一个 AnnotationType 实例,并做了以下工作:
确保传入的是一个合法的注解类型。
获取该注解的所有成员方法,并检查它们是否符合注解方法的要求(如无参数,且是公共抽象方法)。
提取和保存注解的成员信息(方法、返回类型、默认值等)。
处理注解的特殊属性,如 Retention 和 Inherited。
Retention
Retention为Annotation的一个子类且有一个value的方法。
那么我们只要AnnotationInvocationHandler第一个参数传入Retention,然后Map加入一个value为键的entry即可。
这里不太理解原理,查了一下:
Retention 注解:Retention 是 Java 中的一个元注解,它用于指定注解的生命周期。Retention 注解本身是 Annotation 的子类,并且它有一个 value 方法,表示注解的保留策略(如:SOURCE, CLASS, RUNTIME)。
AnnotationInvocationHandler 的工作机制:AnnotationInvocationHandler是用来处理注解实例的方法调用的,他主要是通过反射来处理注解接口的方法调用,通常是注解是接口,在编译是生成的类会通过InvocationHandler 来代理这些接口方法的调用。
当我们创建一个注解的实例时,例如@Retention ,这个实例是通过代理模式创建的,AnnotationInvocationHandler 会代理 Retention 注解的所有方法,包括 value() 方法。Map 中的 value 方法:
在 AnnotationInvocationHandler 中,注解的成员方法(如 Retention 的 value() 方法)会被映射到一个 Map 中,通常是将方法名作为键(例如 “value”),方法的实现或返回值作为值。这就解释了为什么 value 方法会作为键添加到 Map 中。setValues 的作用:
setValues 方法通常用于设置注解的属性值。当我们通过AnnotationInvocationHandler 处理注解时,它会根据注解的定义和方法(比如 value() 方法),动态地生成值并将其放入 Map 中。由于 value 方法是 Retention 注解的成员方法之一,AnnotationInvocationHandler 会通过反射来访问并调用它,然后把 value 方法的返回值(RetentionPolicy)放入 Map 中。
构造poc
终于到了激动人心的时刻:
1.构造恶意的Transformer
2.构造transformedMap
3.构造AnnotationInvocationHandler,因为AnnotationInvocationHandler的构造方法没有写限定符默认为default,只能同个包内访问,所以需要利用反射获取构造方法。
同样是解析一下代码:
1 | Transformer[] transformer = new Transformer[]{ |
这个 transformer 数组由四个 Transformer 对象组成。Transformer 是一种功能,它通常用于将输入对象转换为另一个对象。
ConstantTransformer:首先,它将输入对象转为 Runtime.class,也就是 Java 的 Runtime 类。
InvokerTransformer (“getMethod”):接着,使用反射来调用 getMethod 方法,目标是获取 Runtime.getRuntime 方法。
InvokerTransformer (“invoke”):然后,使用反射来调用 invoke 方法,即实际执行 getRuntime(),返回 Runtime 的实例。
InvokerTransformer (“exec”):最后,调用 exec 方法执行一个操作。在这里,它执行的是 open -a Calculator,这将打开计算器。
1 | Map innermap = new HashMap(); |
这段代码创建了一个 Map(innermap),并将 value 键映射到一个任意的字符串 “lsf”。
接着,TransformedMap.decorate() 方法将原始的 innermap 包装为一个新 Map,这个新的 Map 使用了前面定义的 ChainedTransformer 进行变换。简单来说,它将映射中的每个值都经过 transformer 数组中的变换器处理。
这个 outermap 就是一个特殊的 Map,它的操作被 ChainedTransformer 转换成了触发计算器打开的反射调用。
1 | Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
这里通过 Reflection 获取了 AnnotationInvocationHandler 类并调用了其构造函数,将 Retention.class 和 outermap(包含恶意变换器的 Map)作为参数传入。
AnnotationInvocationHandler 的实例 handler 就是这个恶意注解对象的处理器。接下来,它会根据注解的方法调用,通过代理来执行实际的操作。
1 | ByteArrayOutputStream barr = new ByteArrayOutputStream(); |
这段代码将 handler(即 AnnotationInvocationHandler 对象)写入一个 ByteArrayOutputStream 中进行序列化。序列化是将对象转换为字节流的过程。
然后,通过反序列化的方式,将之前序列化的 handler 对象读回内存。反序列化的过程中,handler 会被重新构造,触发它内部的恶意操作。
调用链
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
总结
感觉我学的还是比较青涩的,这个链子作为反序列化学习的敲门砖,希望后面有机会可以不参考文章自己重新梳理一下