序言
前面分析了CC1 链,其是有版本限制的,在JDK 1.8 8u71版本以后,对AnnotationInvocationHandler的readObject进行了改写,导致高版本JDK无法使用CC1。
CC2利用的是cc4.0,因为cc3.2-3.1中TransformingComparator没有继承Serializable,所以无法进行java反序列化。
CC2链 并不是使用 AnnotationInvocationHandler构造,而是使用 javassist 和 PriorityQueue 构造链子。
Maven - pom.xml
1 | <dependency> |
POC链:
前置知识
javassist
通常我们需要将.java编译成.class才能正常运行,命令行通过javac进行编译,利用编译生成的固定格式的字节码(.class)来供jvm虚拟机进行使用,每一个class文件中都包含这一个java类和一个接口。
javalist就是一个处理字节码的类库,能够动态的修改class中的字节码。
常用用法:
1.ClassPool是一个CtClass对象的容器,一个CtClass必须从中进行获取,通过ClassPool.getDefault()返回了默认的类池。
ClassPool pool=ClassPool.getDefault();
2.创建名为Evil的类
CtClass test=pool.make('Evil');
3.添加类搜索路径,通过 ClassPool.getDefault(); 获取的ClassPool使用JVM的类进行搜索路径,但是如果程序运行在JBOSS或Tomcat就有可能回找不到用户的类,因为Web服务器回使用多个类加载器作为系统加载器,这时候我们必须要要通过如下命令来添加我们的路径:
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
4.设置要继承的类:
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
5.创建一个空的类初始化器(静态构造函数)
CtConstructor constructor = test.makeClassInitializer();
6.将字节码插入到开头:
constructor.insertBefore("System.out.println(\"Hello,Teabo\");");
7.再将编译的类创建为.class文件:
test.writeFile('./');
Demo:
ClassLoader#defineClass
ClassLoader是java的类加载器,负责将字节码转化成内存中的Java类,加载过程中采用双亲委派来实现加载。
这里可以利用类加载器的defineClass方法来加载我们的字节码
用反射方法调用 defineClass方法
需要注意的是,ClassLoader#defineClass 返回的类并不会初始化,只有这个对象显示调用其构造函数时才会执行初始化代码,所以要找办法调用返回得类的构造函数才能执行命令,这时候TemplatesImpl出场,为什么选择它,因为它最后会自动实例初始化,看到下面分析就知道了。
TemplatesImpl
在TemplatesImpl类中定义了一个内部类
这个类对defineClass进行了重写,并且没有显式的对定义域声明,因此可以被外部进行调用。
这个调用的内部过程为:
1 | TemplatesImpl.getOutputProperties() |
1.TemplatesImpl.getOutputProperties()
2.TemplatesImpl.newTransformer()
3.TemplatesImpl.getTransletInstance()
4.TemplatesImpl.defineTransletClasses()
getSuperclass()将字节码还原成class对象
TemplatesImpl内部链就是这样 蓝色框为需满足的条件。
注意还要设置父类
因为有父类判断,通过才赋值下标
现在去主函数里利用TemplatesImpl
代替 ClassLoader
来加载字节码
成功加载字节码
PriorityQueue
构造方法:
PriorityQueue() 创建一个PriorityQueue,根据其自然顺序对元素排序
PriorityQueue(int ) 创建指定容量的PriorityQueue
常用方法:
前置知识 javassist生成修改字节码 ClassLoader TemplatesImpl加载字节码 大致就是这样。
CC2利用链分析
完整利用链
1 | ObjectInputStream.readObject() |
(偷张图~)
TransformingComparator
在CC2中是利用 TransformingComparator
的compare
来触发 transform的方法(感觉各个版本的CC链就是花式想办法触发transform方法)
如果传入的obj1可控,那么就可以利用 InvokerTransformer 反射执行任意类的任意方法。
InvokerTransformer在CC1中已经详细分析过了
这里如果compare中调用InvokerTransformer
,并且传入的obj1包含恶意的任意类任意方法,那么就能反射执行命令。
已知TransformingComparator.compare()
可以触发transform
,那么现在就要找什么地方调用这个compare()
,并且要参数可控。在CC2中用的是 PriorityQueue 队列来触发
PriorityQueue
从readObject()开始看, 先是对传入的ObjectputStream进行反序列化然后788行赋值给queue数组。
那么我们传入的肯定就是序列化数据了。
所以来看writeObject()
writeObject序列化时传入的参数是 queue数组,他是这个类的一个属性,既然是属性,我们就可以通过反射方法修改控制它。
回到readObject(),在最后调用了heapif(),进入其
进入siftDown()
进入siftDownUsingComparator()
可以看到有comparator.compare
的调用 ,同时参数x可控。
因此就可以控制这里的comparator为TransformingComparator
。
所以就成功利用 PriorityQueue 触发到了 compare,和上面半段联系起来。
Demo-1:
先手动给出Transform利用链,然后反射赋值PriorityQueue
的compare
属性,从而调用到TransformingCompare.transform()
,进而触发Transform利用链。
注意这里为什么给出两次add(),因为如果只给出一次,在heapify()中的i是-1,add()就相当于控制size和添加queue[]了。
i=-1时
i=0时
以上是利用ChainedTransform链达到命令执行目的。
接下来的最后一步就简单了,就是利用TemplatesImpl加载包含任意CMD的字节码
1 | javassist生成字节码 -> 利用InvokerTransformer触发TemplatesImpl.newTransformer -> TemplatesImpl加载字节码 -> 任意命令执行 |
最终POC
1.javassist生成字节码
2.生成TemplatesImpl实例
3.反射设置TemplatesImpl链子所需要满足的值
4.准备InvokerTransformer,用于触发TemplatesImpl.newTransformer
5.准备TransformingComparator,用于触发第4步的InvokerTransformer
6.创建PriorityQueue队列,并传入第5步的TransformingComparator
,在PriorityQueue.java中触发到TransformingComparator.compare()
这里对queue属性赋值一个数组,为什么不直接传递templates
的原因就是queue是一个Object[]数组。并且元素个数为2的原因就是之前解释过为什么两次add()
的原因。
那这样和上面的Demo-1有什么区别呢?
上面的Demo1不是加载字节码的方式执行命令,而是通过ChainedTransformer来执行命令,
所以也就不需要去触发TemplatesImpl.newTransformer了,只需要在TransformingComparator.compare()
中触发
ChainedTransformer.transform即可,所以才不需要将精心构造的templatesImpl类传入queue数组,只需要控制comparator属性的值为TransformingComparator类即可。
进入到siftDownUsingComparator
中触发compare()
跳过第一个if后直接触发到TransformingComparator.compare()
其中 templatesImpl作为参数一 obj1 传递
调用Invokerformer.transform(templates)
结合之前反射赋值的iMethodName="newTransform"
最终利用Invoke反射调用执行templatesImpl.newTransform
再看看触发
templatesImpl.newTransform
之后templatesImpl
内部发生了什么
1.
2.
3.newInstance() 自动初始化加载的恶意字节码,执行命令
为什么我们的字节码文件只要初始化之后就会运行呢,也没有去调用呀??
是因为生成的字节码类的构造函数是static。所以初始化加载的时候会调用里面的代码。
包含cmd的字节码在这就被加载执行了,链也就结束了
所以说这里完整POC的调用链是这样的:
1 | PriorityQueue.readObject() |
代码:
1 | package CC2; |
CC2的分析就到这里了,前前后后啃了2 3天,可能对一些细节一些概念还不能完全理解,甚至有些地方不正确 :(,以后再回头来看可能会有新的理解。要说CC2和CC1除了调用链这些区别外还有什么我觉得最大的不同,那就是CC1是用ChainedTransformer
手动构造一条InvokerTransformer
利用链然后去找链触发,而CC2是引入了字节码
,通过生成、加载字节码来执行命令,所以不需要像CC1那样构造一个命令执行链,只需要把要执行的命令写入字节码中,然后去加载字节码触发命令执行。