内容纲要

前言

下面两章我将从 YSoSerial 工具的角度来学习各类 POP 利用链 . 在此之前 , 我们需要搭建 YSoSerial 调试环境以及了解 Java 一大核心机制 : 动态代理机制

PS : 最近好多企业开始 HW 演练了 , 要去各种现场 , 真是忙到不行 ! 不过能看到大佬手工挖 0day , 也不亏哈哈 .


环境搭建

YSoSerial 测试环境搭建

  1. 首先从 GitHub 上下载最新的 YSoSerial 源代码

    git clone https://github.com/frohoff/ysoserial.git

    可以看到源码中存在 pom.xml 配置文件 , 那么这应该是一个使用 Maven 搭建的项目 , 我们直接通过 IDEA 来打开他 .

  2. 通过 IDEA 打开该项目后 , Maven 会自动下载对应的依赖 , 但是由于各种原因 , 部分依赖可能无法下载成功 , 可以手工通过 Maven 导入 jar 包 . 我在导入项目时就遇到了这个问题 .

    举个例子:

  3. 当所有依赖加载完毕后 , 运行 YSoSerial 项目的主函数 , 即可运行 YSoSerial

    YSoSerial 的主函数的位置如下

    ysoserial/src/main/java/ysoserial/GeneratePayload.main()

    Console 出现上述输出信息 , 则代表 YSoSerial 项目已经导入成功 .

  4. 此时可以为 YSoSerial 添加参数 , 例如我们想要通过 CommonsCollections1 这个CC链执行系统命令 mate-calc

    再次运行后即可得到序列化字符串.

  5. 之后测试时我们需要把这段 Payload 给发送出去 , 但是 java 序列化数据并非以纯文本的形式显示 , 所以无法直接复制粘帖 , 我们需要将这段序列化数据保存到一个二进制文本中

    那么在哪里添加这段保存文件的代码呢? 有了之前的基础 , 你应该知道 , 当程序执行 writeObject() 方法后 , 会将 Java 类序列化为二进制字节数组 . 所以我们直接定位到序列化时的 writeObject() 方法中

    在上述这行代码中 , Serializer.serialize() 方法会调用 writeObject() 方法 , 所以我们在这个 writeObject() 方法中添加保存文件的代码 .

    现在再运行 main() 函数时 , 会在 /tmp 目录下生成 payload.ser 文件 , 并向其写入序列化字符串.

    序列化数据中存在我们要执行的 mate-calc 系统命令 .


WebServer 环境搭建

  1. WebServer 就比较好搭建了 , 直接通过 IDEA 新建一个 Java Web 项目 , 并编写一个测试 Servlet .

    在处理POST请求时 , 对接收到的序列化数据进行反序列化操作 .

  2. web.xml 文件中编写路由 , 配置当访问 /demotest 路径时 , 会调用刚才编写的 Servlet 进行处理.

  3. 别忘了加载 Apache CommonsCollections 包 , 反序列化要求目标主机上必须存在序列化时使用的类

  4. 部署并启动 Tomcat 中间件

    这方面没啥好说的 , 直接部署就完事了 .


Demo环境测试

  1. 首先通过 YSoSerial 生成执行系统命令 mate-calc 的 Payload , 并保存到 /tmp/payload.ser 文件中.

  2. 向 tomcat 服务器发送恶意序列化数据 , 这里推荐使用 Curl 工具来发送

    curl http://localhost:8080/webdemotest_war_exploded/demotest --data-binary @/tmp/payload.ser

    注意 : 通过 Curl 发送二进制文件时 , 需要在文件路径前加上 @ 符号 , 此时文件中所有的回车符和换行符都将被自动转换 .

    成功执行系统命令 mate-calc , 攻击成功.


YSoSerial Payload Main() 函数解析

YsoSerial Main() 函数中的内容是比较简单的 , 我们直接来看代码 .

  1. 首先是对命令行传入的参数进行解析 , 例如传入的参数个数是否相同 , 传入的 PayloadType 类型是否合法 , 并进行初始化复制操作 .

  2. 在获取了参数后 , 就会生成指定POP链的实例对象 , 并调用该对象的 getObject() 方法 , 构造恶意类 , 这一步是核心 . 最后将恶意类序列化后打印到系统控制台中 , 最后释放实例对象 .

    关于 POP 链中的操作 , 我放在后面说 . 现在你需要了解另外一个知识点 : Java 代理机制 , 这个机制贯穿了所有的 POP 利用链 , 相信你看过后会对前面几章的内容有更深入的理解 .


Java 代理模式

什么是 Java 代理模式呢? SegmentFaultJava三种代理模式:静态代理、动态代理和cglib代理 一文中说的非常棒 , 引用如下 :

代理模式是一种设计模式 , 提供了对目标对象额外的访问方式 , 即通过代理对象访问目标对象.

举个例子 , 存在一个 对象A , 但是开发人员不希望程序直接访问 对象A , 而是通过访问一个中介对象B来间接访问 对象A , 以达成访问 对象A 的目的 . 此时 , 对象A 被称为 "委托类" , 对象B 被称为 "代理类" .

总的来说 , 使用代理模式有如下两个优点 :

  1. 可以隐藏委托类的实现.
  2. 可以实现客户与委托类间的解耦 , 在不修改原目标对象的前提下 , 提供额外的功能操作 , 扩展目标对象的功能.

Java 中代理分为 " 静态代理 " 和 " 动态代理 " , 如下:

这两种代理模式之间有什么区别呢 ? 一个 Java 程序从编写到被执行一共会经过两大步骤.

其中 , 根据 "字节码文件的创建时间" , 可以将 Java 代理分为 " 静态代理 " 与 " 动态代理 ".

  • 在程序运行前 , 已经存在代理类的字节码文件 , 这种代理模式被称为 "静态代理" , 代理类和委托类的关系提前就被确定好了.

  • 在程序运行前 , 不存在代理类的字节码文件 , 这种代理模式被称为 "动态代理" , 代理类的实例对象在程序运行期间由 JVM 根据反射机制动态创建.


Java 静态代理

写一个demo来说明什么是 Java 静态代理模式 .

有朋友提到在代理类的构造函数中 , 构造函数的参数应该为接口类类型 , 即本图中的 testImpl 类型 . 我看了下网上其他文档 , 两种写法都有 . 我个人感觉是没有区别 . testDelegate 是继承 testImpl 接口的 , 子类继承父类的所有方法 . 在此处应该没有影响 .

前面说过 , 静态代理体现在 "程序运行前 , 就已经存在代理类的字节码文件" . 的确是这样的 . 手工编写完上述类 , 需要先通过 javac 编译所有类 , 此时就生成了代理类的字节码文件 . 最后再运行测试类 .

静态代理的优缺点.

  • 优点
    可以在不改变目标类(委托类)的前提下 , 修改目标类(委托类)的功能 .

  • 缺点
    当接口类需要增加修改删除方式时 , 委托类和代理类都需要修改 , 不易维护 .

    当需要代理多个类时 , 由于代理类要与委托类实现相同的接口 , 因此一般有两种方法 .

    1. 只编写一个代理类 , 每代理一个委托类就多实现一个接口 , 但这样会导致代理类过于庞大 .
    2. 为每个委托类都编写一个代理类 , 但这样会导致代理类数量过多.

Java动态代理

Java 动态代理原理

既然编写代理类会有很多问题 , 那么是否有方法不编写代理类 , 而是让程序在执行时自动生成代理类呢 ?

方法当然是有的 , 即 Java 动态代理模式 .

关于类为什么能自动生成 , 我觉得 小旋峰师傅<Java动态代理详解> 一文中说的非常好 , 我这里简单记录下 .

关于类的动态生成 , 可以参考 <深入理解Java虚拟机> 第七节 Java类加载机制 . 其中说明了Java 类从 " 被加载到 JVM 内存 " 到 " 从 JVM 内存中卸载 " 会经过7个阶段 .

其中Java 类的加载过程包括了前 5 个阶段 , Java 类的动态生成与第 1 个阶段 " 加载 " 有关 .

在 " 加载 " 阶段中 , JVM 会完成以下三件事 .

  1. 通过一个类的全限定名来获取其定义的二进制字节流 .
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构 .

    方法区在Java虚拟机启动时被创建 , 用于存储已被虚拟机加载的类信息 , 常量 , 静态变量 , 即时编译器编译后的代码 等数据 . 这部分内容可以参考 <JVM运行时数据区(Run-TimeDataAreas)及内存结构> 一文 , 说的非常详细 .

  3. 在 Java 堆中生成一个代表这个类的 java.lang.Class 实例对象 , 作为方法区中数据的访问入口 .

上述 " 类的全限定名 " 就是类名全程 , 简单的说就是 "包名.类名" 的形式 , 例如 "java.lang.Runtime" . 且由于Java虚拟机规范中对这3点要求并不具体 , 因此实现上是非常灵活的.

例如获取类的二进制字节流(Class字节码)就有很多途径:

  • 从压缩包中获取 , 例如从 Jar , Ear , War 等格式的压缩包中获取 Java Class 字节码 .
  • 从网络请求中获取 , 典型的应用是 Applet.
  • 运行时计算生成 , 例如动态代理技术 , 根据接口和目标类的实例对象 , 生成代理类的 Java Class 字节码.
  • 由其它文件生成 , 例如通过 JSP 文件生成对应的 Java Class 字节码 .
  • 从数据库中获取 , 有些中间件会将 Java Class 字节码存入数据库 , 用来实现代码在集群间分发.

综上所述 , 我们一般会通过 " 实现接口 " 和 "继承类" 两种来生成代理类的 Java Class 字节码.

  1. 通过 " 实现接口 " 的方式生成代理类字节码 => JDK 原生动态代理

  2. 通过 " 继承类 " 的方式生成代理类字节码 => Java 第三方字节码操作库 CGLIB 等动态代理

注 : Cglib 只是 Java 第三方字节操作码库中的一个 , 其他的字节码操作库可以参考官方文档 <Open Source ByteCode Libraries in Java >. 本章中我们以 Cglib 库来举例 .


JDK原生动态代理

JDK 原生动态代理核心 API

JDK 原生动态代理利用拦截器和反射来实现 , 只需要 JDK 环境就可以实现代理 , 不需要第三方库的支持 .

JDK 原生动态代理的核心 API 为如下两个类 .

java.lang.reflect.Proxy

java.lang.reflect.InvocationHandler

下文的内容部分引用自 Throwable 前辈的 <深入分析Java反射(四)-动态代理> 一文 , 这篇文章讲的非常清晰 .

  • java.lang.reflect.Proxy

    java.lang.reflect.Proxy 为 JDK原生动态代理的核心类 , 该类的功能为 : 提供四个静态方法来为一组接口动态地生成的代理类并返回代理类的实例对象.

    InvocationHandler getInvocationHandler(Object proxy) : 通过指定的代理类实例查找与它关联的调用处理器实例

    Class<?> getProxyClass(ClassLoader loader, Class<?>[] interfaces) : 获取关联于指定类装载器( ClassLoader )和一组接口的动态代理类的类对象

    boolean isProxyClass(Class<?> cl) : 判断指定类是否是一个动态代理类

    Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) : 为指定类装载器 , 一组接口 , 调用处理器 ; 该方法是 JDK 原生动态代理中最核心的方法.

  • java.lang.reflect.InvocationHandler

    java.lang.reflect.InvocationHandler 为调用处理器接口 , 该接口定义了一个 invoke() 方法 , 用于集中处理在动态代理类对象上的方法调用 . 当程序通过代理对象调用某一个方法时 , 该方法调用会被自动转发到 InvocationHandler.invoke() 方法来进行调用 .

    proxy : 代理类实例对象

    method : 被调用方法名 , 该参数的类型为 java.lang.reflect.Method 类型

    args : 被调用方法的参数数组

    继承 java.lang.reflect.InvocationHandler 接口,通过实现 invoke() 方法来添加代理访问的逻辑 . 逻辑代码块中除了调用委托类的方法 , 还可以添加自定义逻辑 ;AOP( 面向切面编程 )就是这样实现的 , 当然这不属于本章内容 , 所以我们不深究了.


JDK原生动态代理 Demo

JDK 原生动态代理的执行流程分为如下三步.

  1. 通过实现 java.lang.reflect.InvocationHandler 接口来创建自定义的调用处理器(InvocationHandler) .
  2. java.lang.reflect.Proxy 类指定一个类加载器(ClassLoader) , 一组接口(Interfaces) 和 一个调用处理器(InvocationHandler)
  3. 调用 java.lang.reflect.Proxy.newProxyInstance()方法 , 分别传入类加载器 , 被代理接口 , 调用处理器 ; 创建动态代理实例对象.

    上述步骤可能比较难理解 , 我这里写一个例子你就明白了. 这里我们分开来写

    • 接口类定义

    • 委托类定义

    • 调用处理器定义( 中介类 )

      这里我们实现了调用处理器 , 并在其 invoke() 方法中添加自定义的逻辑 : 在调用 method.invoke() 前输出 test DymanicProxy

      实现调用处理器的类也被称为 " 中介类 " .

    • 测试类

      此处便是真正创建动态代理对象的地方 , 分别获取委托类的的 ClassLoader , 被代理的Interfaces , 以及之前创建的调用处理器 , 然后创建动态代理对象 .

现在通过 javac 来生成字节码文件时 , 不会生成动态代理类的字节码文件 .

通过IDEA调试 , 可以看到程序分别获取了需要的需要的参数 , 随后创建了动态代理对象 .

生成动态代理对象后, 会调用代理对象的 invoke() 方法 .

继续调试 , 发现程序最终会调用委托类的 sayHello() 方法 ,

现在整个JDK 原生动态调用就走完了 , 成功实现了在不修改源码的情况下 , 增加自定义的代码逻辑.


那么程序运行时自动生成代理类是什么样的呢? 其实我们可以通过设置系统属性的方法来查看代理类. 即在生成代理对象前添加 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 属性

此时再运行 main() 方法时 , 会在程序根目录中生成 com.sun.proxy.$Proxy0.class 文件 , 它就是动态代理类的字节码文件 .

这个字节码文件倒不算太复杂 .

  1. 首先进行类初始化 , 执行静态代码块( static{} ) 中的内容

    静态代码块中是一组反射调用 , 用于获取一些方法 . 比如获取了 testImpl.sayHello() 方法 .

  2. 在实例化该类时会调用其父类的构造函数 , 获取调用处理器实例对象

  3. 在调用 sayHello() 方法时 , 会被转发到调用处理器的 invoke() 方法中

动态代理类具体的实现方式我们就不深究了 , 有兴趣的师傅可以自行钻研 .


Java CGLIB 动态代理

Java CGLIB 动态代理核心 API

CGLIB( Code Generation Library ) 是一个强大的 , 高性能的 , 高质量的Code生成类库 ;它可以在程序运行期扩展Java类与实现Java接口 .

Throwable 前辈的 <简述CGLIB常用API> 中是这么讲解 CGLIB 动态代理的 , 我认为讲的非常清楚 , 引用如下 :

CGLIB 代理时会生成一个委托类的子类 , 子类重写委托类中所有非 final 修饰的方法.

子类中采用方法拦截的技术拦截所有对父类(委托类)方法的调用 , 在其调用基础上添加自定义代码逻辑.

我认为 CGLIB 类库的核心 API 只有一个 .

  • net.sf.cglib.proxy.Enhancer

    Enhancer 类即 " 字节码增强类 " , 它是 CGLIB 库中最常用的一个 , 类似 JDK 原生动态代理中的 Proxy 类 . 只不过它不仅能代理普通 Java 类 , 也能够代理 Java 接口 .

    Enhander 类的核心功能就是创建一个被代理类( 委托类 )的子类并且拦截所有对委托类的方法调用.

    需要注意的时 , Enhander 类无法拦截使用 Final 修饰符修饰的方法 , 也无法代理使用 Final 修饰符修饰的 Java 类.

    Enhander 类中有三个方法比较重要 , 如下 :

    void setSuperclass(Class superclass) : 指定当前类的父类( 实际上也就是委托类 ).

    void setCallback(Callback callback) : 指定当前类的回调对象

    Object create() : 生成代理对象

    上述这个 " 回调对象( Callback ) " 是指什么呢 ? 实际上 , Callback 是一个空接口 , 其中未定义任何方法 , 该接口的唯一作用就是在代理类的方法被调用时进行回调 , 即 : 当生成的代理类中的方法被调用时 , 会调用 Callback 实例对象中的代码逻辑.

    通过对 Enhancer 实例对象调用 setCallback()setCallbacks() 两个方法来设置一个或多个 Callback 实例对象 , 当通过 setCallbacks() 方法设置了多个 Callback 实例对象时会按照设置顺序进行回调 ..

    CGLIB 中提供的 Callback 实例对象有如下几个 :

    1. NoOP : 该类不进行任何操作 , 只把对被代理方法请求转发到委托类的原方法中
    2. FixedValue : 该类提供一个 loadObject() 方法 , 拦截所有的方法调用 , 并调用 loadObject() 方法 , 返回一个固定值 ; 该类不会调用委托类任何方法.
    3. InvocationHandler : 该类与 JDK 原生动态代理类似 , 当程序想要调用某一个被代理的方法时 , 该调用会被转发到 InvocationHandler.invoke() 方法中
    4. MethodInterceptor : 方法拦截器接口 , 该类提供一个 intercept() 方法 . 当继承了该接口后 , 所有对代理类的方法调用都会被转发到该接口的 intercept() 方法中 . 该接口的功能非常强大.
    5. Dispatcher : 分发器 , 提供一个 loadObject() 方法 , 该方法会返回一个代理对象来拦截每次对原方法的调用 .
    6. LazyLoader : 懒加载器 , 提供一个 loadObject() 方法 , 该方法会在第一次调用被代理方法时触发 , 返回一个代理对象. 这个代理对象会被存储起来 , 用于处理后续所有对被代理方法的调用.

Java CGLIB 动态代理 Demo

有了前面的基础 , 我们直接来看代码 . 这里我们使用 MethodInterceptor 接口来生成 Callback 实例对象 .

  1. 引入 CGLIB 第三方类库

    推荐通过 Maven 来搭建测试环境 . 通过 IDEA 新建一个 Maven 项目 , 按照 pom.xml 来配置依赖关系 .

    配置后重载项目 , Maven 会自动下载并导入依赖.

  2. 委托类

    因为通过 CGLIB 实现动态代理时不需要继承接口 , 所以不再需要接口类了. 直接编写委托类

    注意 , 委托类没有继承接口 , 这是 CGLIB 动态代理的一大特点 .

  3. 中介类

    这一步可能比较复杂 , 实际上就是中介类中的 getProxyInstance() 方法会返回代理对象 .

    我们只需要向中介类的构造方法中传入委托类实例对象 , 并调用其 getProxyInstance() 方法 , 中介类就会自动生成代理对象 , 代理委托类中所有非 Final 修饰的方法 . 并拦截所有对被代理方法的调用 , 将其转发到回调方法( 此处为 MethodInterceptor.intecept() 方法 )中.

  4. 测试类

    测试类就很简单了 , 生成代理对象后调用代理对象的 sayHello() 方法 .

动态代理调用链如下 , 最终调用委托类的 sayHello() 方法

由于篇幅原因 , 我们就不深究 CGLIB 动态代理的具体实现原理了 , 有兴趣的师傅可以参考 Throwable 前辈的 CGLIB动态代理原理分析 一文 .


总结

本章内容的核心就是 Java 动态代理 . 该机制对于理解 YSoSerial CommonsCollections Payload 有至关重要的作用 . 如果想要完全搞懂 YSoSerial Payload 的生成原理 , 就必须得明白什么是 Java 动态代理 .

本章内容参考了很多前辈们的文章 , 如果您发现文中有哪些不正确对方 , 欢迎指出 !

下一章的主要内容就是 YSoSerial 中 7 条 Apache CommonsCollections Payloads 的实现原理 .

最后修改日期:2020年8月6日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。