Apache CC反序列化漏洞:CC1调用链POC构造过程,丛零基础到精通,收藏这篇就够了!

温馨提示: 本文仅供网络安全技术交流,请勿用于非法用途。所有渗透测试需获得授权,否则后果自负,与本号及作者无关。请务必遵守法律法规!

剧透警告: 阅读本文,你将掌握 Apache Commons Collections (CC) 反序列化漏洞 CC1 调用链的精髓,从此告别“一脸懵逼”状态,走上安全巅峰!

漏洞分析大纲

1. CC1 调用链流程:抽丝剥茧,直击核心

先来一张图镇楼,让你对 CC1 的整体流程有个清晰的认识:

再来一张图,加深印象:

简单来说,CC1 调用链就像一条贪吃蛇,你要做的就是找到它的头和尾:

  • S1:找到反序列化入口。 就像发现了一个宝藏,这个入口可以接收你精心构造的序列化数据流。
  • S2:寻找序列化入口类。 找到重写了 readObject 方法的类,并且这个方法能够像多米诺骨牌一样,触发一系列的危险操作,最终引爆我们的“炸弹”——执行 Runtime.exec("calc")

目标明确: 我们的终极目标是让程序执行 Runtime.exec("calc"),弹出计算器!为了达成这个目标,我们需要逆向追踪,找到能够调用这个危险方法的“幕后黑手”,以及能够调用“幕后黑手”的“黑手老大”,最终找到重写了 readObject 方法的序列化入口类。

友情提示: 上一篇文章已经介绍了 Commons Collections 中一个非常重要的接口—— Transformer 接口,它是我们构建 CC1 调用链的基石。

2. Transformer 接口:化腐朽为神奇的“变形金刚”

(1) 作用:

Transformer 接口就像一个“变形金刚”,接收一个 Object,然后返回另一个 Object。它的实现类可以根据你的需求,对输入的 Object 进行各种自定义处理,并返回相应的结果。

CC1 调用链中,我们主要会用到以下几个 Transformer 接口的实现类: InvokerTransformerTransformedMapAnnotationInvocationHandlerConstantTransformerChainedTransformer

3. InvokerTransformer 类:反射的艺术

(1) 作用:

InvokerTransformer 类就像一个“遥控器”,你输入一个对象,它会通过反射的方式,调用你指定的对象的某个方法(方法名、参数类型和参数值都由你来控制)。

关键点: iMethodName 参数是从哪里传入的?

真相只有一个: iMethodName 参数在 InvokerTransformer 类初始化构造的时候就被传入并赋值了!这意味着,我们可以控制执行任意对象的方法!

目标锁定: 让它执行一个危险方法!

(2) 正常情况下(未使用 InvokerTransformer 类)执行危险方法:

import org.junit.Test;

(3) 使用 InvokerTransformer 类执行危险方法:

import org.apache.commons.collections.functors.InvokerTransformer;

代码解读:

  • new InvokerTransformer:创建一个 InvokerTransformer 对象,需要传入方法名、参数类型列表和参数值列表。
  • exec:方法名是 exec,也就是我们要执行的命令。
  • Class[] {String.class}:参数类型列表是一个 Class 数组,这里表示 exec 方法接收一个字符串类型的参数。
  • new Object[] {"calc"}:参数值列表是一个 Object 数组,这里表示我们要执行的命令是打开计算器。
  • Runtime.getRuntime():我们要对 Runtime.getRuntime() 对象进行方法调用。

小目标达成: 我们成功地将危险方法向前推进了一步!调用流程变成了:

4. TransformedMap 类:巧妙的“中转站”

接下来,我们需要找到谁调用了 Transformer 方法。

秘籍: 搜索哪里使用了 Transformer 方法!

答案揭晓: TransformedMap 类的 checkSetValue() 函数调用了 Transformer 方法!

精髓: 如果 valueTransformer = invokerTransformer,并且将 Runtime.getRuntime() 对象传入 checkSetValue() 函数,那么执行效果就等同于 invokerTransformer.transform(Runtime.getRuntime())

问题来了: 如何给 valueTransformer 变量赋值?

溯源: valueTransformer 变量是在哪里赋值的?

真相: TransformedMap 方法可以给 valueTransformer 变量赋值,但这个方法的访问级别是 protected,只能在 TransformerMap 类的内部调用。

峰回路转: TransformedMap 类还有一个 decorate 装饰方法,可以将 valueTransformer 变量传入,然后再调用 TransformedMap 方法。decorate 方法是一个公开静态方法,可以进行实例化。

测试代码:

Map decorated = TransformedMap.decorate(null, null, invokerTransformer);

完美: 我们可以调用 TransformedMap.decorate 方法,传入一些 null 值,再把我们要调用的 invokerTransformer 传进去。最后,我们再调用 TransformedMap 类中的 checkSetValue() 函数,并把 Runtime.getRuntime() 对象传进去,就等同于 invokerTransformer.transform(Runtime.getRuntime())

但是! checkSetValue() 函数也是受限制的!

继续追踪:TransformedMap 类中,哪里调用了 checkSetValue() 函数?

答案: TransformedMap 类继承的父类 AbstractlnputCheckedMapDecorator 中调用了 checkSetValue() 函数,它是在 MapEntry 实体中调用的。也就是说,在调用 MapEntry 实体的时候,就会调用 checkSetValue() 函数。

当前目标:

  1. AbstractlnputCheckedMapDecorator$MapEntry.setValue() 函数中传入我们的 Runtime.getRuntime()
  2. TransformedMap.valueTransformer 赋值成之前构造好的 invokerTransformer 对象。

这样就等同于执行了 invokerTransformer.transform(Runtime.getRuntime())

再次梳理: checkSetValue() 函数的本质是,每次赋值的时候,它会把你的值替换成通过 Transformer 方法转换之后的输出对象。也就是说,不管是赋值什么,它都会通过 Transformer 方法给你转换一下再存进 Map 里面。

Decorator 模式: decorate(map) 方法传入一个 map,然后返回给你一个 map。这个 decorator 对你输入的 map 做了一些增强,添加了一些新的功能,或者在调用原始对象的方法之前或之后执行自己的操作。

具体来说: 被增强后的 TransformedMap 对象,在执行 Map.Entry.setValue() 的时候,会被替换成重写过的 setValue() 函数。

构造方法:

  1. 先构造一个我们自己的 map,键和值都是 Object 类型;
  2. 在这个 map 里面放入一些值,比如 "a" 和 "b";
  3. 最后,我们用 TransformedMap 对原始 map 做一个增强,keyTransformernullvalueTransformer 使用之前构造好的 invokerTransformer 对象。它会返回一个增强过的 Map,它的键和值仍然都是 Object 类型。

问题: 如何调用这个增强过的 map 呢?

答案: 使用 for 循环遍历它的实体,拿到实体之后,就可以调用增强之后的 map 了。

代码示例:

import org.apache.commons.collections.Transformer;

可以看到里面的键值对就成了一个实体。

然后我们再通过 entry.SetValue 传入我们的危险方法,即 Runtime.getRuntime()

代码如下:

import org.apache.commons.collections.Transformer;

里程碑: 我们把利用链又往前推进了一步!调用流程变成了:

5. AnnotationInvocationHandler 类:终极 Boss 登场!

接下来,我们在 AbstractlnputCheckedMapDecorator 中寻找谁可以调用 Map.Entry.setValue() 函数。

方法依旧: 通过搜索,发现有很多类都调用了 Map.Entry.setValue() 函数。但是,我们最好找到一个序列化入口类,也就是重写了 readObject 方法,并且在 readObject 方法里面调用了 setValue() 函数,这样就可以一步到位,省略了寻找中间类的过程。

Bingo! AnnotationlnvocationHandler 类满足上述所有条件:

  1. 可以被序列化:

  1. 重写了 readObject 方法:

  1. readObject 方法里面调用了 setValue() 函数:

至此,我们又向前推进了一步,现在这个链条已经完整了!调用流程变成了:

But! 还存在一些问题:

问题一: 如何让遍历之后的 Map 等于我们增强的 TransformedMap,也就是如何使 AnnotationlnvocationHandler 类中 memberValues 等于我们的 TransformedMap 对象呢?

答案: 只需查看 AnnotationlnvocationHandler 类中 memberValues 是在哪里开始赋值的。

真相: AnnotationlnvocationHandler 类的构造方法会进行赋值!所以,当我们在构造它的时候,就可以给它传入一个我们增强好的 Map,那么在它对我们增强好的 Map 进行 setValue 的时候,它就会调用我们链条中的方法。

问题二: AnnotationlnvocationHandler 类不是 public 的,我们无法在外部构造(实例化),该怎么办呢?

答案: 可以使用反射把它构造出来!

具体操作: 我们需要查看它要传入什么参数。第一个参数传入的是一个注解类型,第二个参数就是我们可以给它传入一个增强好的 Map

尝试使用反射把这个序列化入口类构造出来:

import org.apache.commons.collections.Transformer;

下一步: 调用 AnnotationlnvocationHandler 类的 readObject 方法。但是,readObject 方法是私有的,所以不能直接调用 o.readObject。我们知道,在进行反序列化的时候会调用 readOject 方法,所以在反序列化之前,我们还需要序列化一次。因此,我们可以通过调用前面准备好的序列化和反序列化函数(在 CC.java 文件中),并给反序列化函数传入我们序列化之后的文件名。

验证: 在执行这段代码的时候,AnnotationlnvocationHandler 类的 readObject 方法也会被执行起来。我们可以在 AnnotationlnvocationHandler 类的 readObject 方法这里打上断点测试一下。

结果: 成功停在了断点处,并且执行了我们的 readObject 方法!

问题三: 如何确保我们的代码能够执行 setValue() 函数?

分析: setValue() 函数外面有两层 if 判断。只有 if 判断成立,才能执行到 setValue() 函数。

破解:

  • 第一层 if memberType != null。从 Map<String, Class<?>> memberTypes = annotationType.memberTypes() 可以看出,memberType 又是一个 Map,从 memberTypes.get(name) 可以看出,这个函数里面的参数不能为空。那么,我们可以给它传入一个已经存在的 key,这样就不可能为空了。
  • 追溯 memberTypes memberTypes = annotationType.memberTypes()memberTypes 等于一个注解 TypeannotationType)。
  • 追溯 annotationType annotationType = AnnotationType.getlnstance(type),这个注解 Type 就是我们前面构造 AnnotationlnvocationHandler 方法的时候传进去的注解 Type

结论: 这个注解 Type 就是我们自己传入的!

整体流程: 通过我们传入的注解 Type 返回了我们的注解对象,然后我们再调用这个注解对象的 memberTypes 方法。

先看一下调用这个注解对象的 memberTypes 能得到什么东西?

测试代码:

@Test

结果: AnnotationType.getInstance 将我们的注解类 Target 传进去,然后它返回的是一个注解 Type,然后这个 memberTypes 方法返回的是一个 Map

结论: 只要出现键值对,我们就直接把这个键(key)传给 memberTypes.get(),它就不可能为空了!

追溯 nameString name = memberValue.getKey()for(Map.Entry<Object, Object> menberValue:memberValues.entrySet()) 可以看到,这个 name 就是我们之前传进去的增强 Map

最终结论: 我们会给它传入增强 Map,然后这个增强 Map 是可控的,它会自动地寻找它的 key,所以最后 memberTypes.get() 函数我们传入 map.key,只要这个 key 里面有前面实体(前面测试代码)中键值对的 value,我们就可以使这两个 if 成立!

接下来开始构造:

import org.apache.commons.collections.Transformer;

验证: 成功停在了断点处,成功地使第一层 if 成立!

第二层 if 判断我们传入的 memberType 是否可以进行实例化,也就是测试代码中的实体中的键值对,一般情况下肯定是可以进行实例化的,所以第二层 if 也自然而然成立。

问题四: 我们给 setValue() 函数传入的参数是不可控的,怎么能够让我们可控呢?

答案: 使用 ConstantTransformer 类可以解决!

6. ConstantTransformer 类:点石成金的魔法

ConstantTransformer 类的作用: 不管你传入什么,它都能正常返回 iConstant 常量!

追溯 iConstant

真相: 在构造 ConstantTransformer 类的时候就已经传入了!

开始构造:

因此,我们在调用的时候必须传入 Runtime.getRuntime(),所以在 entry.setValue() 函数中不管传入什么都不重要了,因为它返回的都是一个 iConstant 常量!

代码如下:

import org.apache.commons.collections.Transformer;

也就是说,其实我们之前的:

和现在的:

效果完全一致!但是,最终我们的调用链没了!虽然可以把常量 TransformerConstantTransformer)传进去了,但是无法调用起后面的那些类,即 InvokerTransformer 类和执行我们的危险方法!

So,接下来该怎么办呢?

当然是使用 ChainedTransformer 类!

7. ChainedTransformer 类:串联一切的终极武器

作用: 它传入一个 transformer 数组,然后链式调用数组里面的这些 transformer,前一个 transformer 的输出是后一个 transformer 的输入。

开始构造:

ChainedTransformer() 函数中传入我们的 transformer 数组,然后把之前构造好的两个常量 transformerConstantTransformer)复制过来,分别是 ConstantTransformerinvokerTransformer。最后把我们的两个常量 transformer 放到 transformer 数组中。

由于 ChainedTransformer 不管传入什么都返回的特性,你最终的 ChainedTransformer.transformer 传入什么都不重要了,链条也得以执行,最终执行到我们的危险方法!

现在我们开始调用我们的 transformer,传入什么都行!

然后现在的话我们就开始把我们的 ChainedTransformer 传给我们的增强 Map,同样将前面构造好的增强 Map 复制过来,将 ChainedTransformer 传入进去。

最后我们通过反射把我 decorated.map(装饰好的 map)复制过来。

代码如下:

import org.apache.commons.collections.Transformer;

执行程序:

What?报错了?!为什么?以及该怎么调用呢?

答案将在交流群里揭晓!

最后献上完整简化代码:

import org.apache.commons.collections.Transformer;

执行程序:

成功弹出计算器!

黑客/网络安全学习包

资料目录

  1. 成长路线图&学习规划

  2. 配套视频教程

  3. SRC&黑客文籍

  4. 护网行动资料

  5. 黑客必读书单

  6. 面试题合集

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************

1.成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************

2.视频教程

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************

3.SRC&黑客文籍

大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录

SRC技术文籍:

黑客资料由于是敏感资源,这里不能直接展示哦!

4.护网行动资料

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

5.黑客必读书单

**

**

6.面试题合集

当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。

更多内容为防止和谐,可以扫描获取~

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*********************************