前段时间发生的 Log4j2 漏洞事件着实让人有点蛋疼,可以说是值得广大中国企业技术人员纪念的日子。在修复漏洞的过程中,让人看到的是:在风险面前,我们的系统就像一个裸奔的男人站在海边,被海风肆意地虐打着,毫无反抗力……
加上之前的 Fastjson 事件,以及后来的 Logback 事件,更进一步体现出:基础组件的重要性!技术创新的重要性!科技发展的重要性!尤其在此次的 Log4j2 漏洞上反映的更为淋漓尽致,各种“核弹级漏洞”、“超高危” 等惊人术语啪啪啪打在我们的脸上……
Log4j2 漏洞回顾
在解析 JNDI 前,我们先回顾一下 Log4j 2 漏洞事件,大概的脉络是:Apache Log4j 漏洞( JNDI注入 CVE-2021-44228 ),使得攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器。依据官方的描述:Apache Log4j2 中存在 JNDI 注入漏洞,当程序将用户输入的数据进行日志记录时,即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码。转换成专业术语,即:若日志内容中包含关键词 “${” ,在输出 Log 的时候,攻击者便能够将关键字所包含的内容当作变量来解析成任何可以攻击的命令,并进行执行,以破坏应用系统。
此次漏洞事件影响版本为:Apache log4j2 >= 2.0, <= 2.14.1。所影响的组件较为广泛,基本上基于 Java 技术生态栈的应用系统均有涉及,具体如下所示:
Apache Struts2
Apache Solr
Apache Druid
Apache Flink
Apache Flume
Apache Dubbo
Apache Kafka
Sping-boot-strater-loj2
ElasticSearch
Redis
Logstash
关于 Apache log4j2 漏洞的检测及修复,本文暂不做描述,大家有兴趣可以去官网查阅。说到这里,可能很多新手就会问:“Log4j2 跟 JNDI 有毛关系?” 这不说了个寂寞 ……接下来,我们先看如下图所示:
基于上图,我们可以看到:所有的“关联”来自于 Lookups 组件。Log4j2 的强大之处在于,除了能够输出程序中的变量信息,同时,其还引入了一个叫 Lookup 的破玩意,可以用来输出更多内容。
Lookups,顾名思义,理解为“查找、搜索”,即允许在输出日志的时候,通过某种方式去查找要输出的内容。换言之,这家伙相当于是一个接口,具体去哪里查找,怎么查找,就需要编写具体的模块去实现了,类似于面向对象编程中多态思想。试想想这样一组场景:假设某一场景中需要通过日志输出一个 Java 对象,此时,这个对象在程序中没有定义,而是在其他环境中,这种情况下如何处理呢?在 Log4j2 官网 https://logging.apache.org/log4j/2.x/manual/lookups.html 我们可以看到 Log4j2 已经帮我们把常见的查找途径都进行实现了,具体如下所示:
通过上图,我们可以看到,Lookups 提供了一种在任意位置向 Log4j 配置添加值的方法。其实现的查找途径较为广泛,几乎支持所有的环境。它们是实现 StrLookup 接口的特定类型的插件。有关如何在配置文件中使用查找的信息,请参见 Configuration 页面的 Property Substitution 部分。
JNDI 体系解析
在上面的 Log4j2 Lookups 路径中,定义了 JNDI 环境入口,接下来,我们来了解一下 JNDI Lookups,JndiLookup 允许通过 JNDI 进行变量检索。在默认情况下,键的前缀为 java:comp/env /,但是,如果键包含“:”,则不会添加前缀。其具体的配置信息如下所示:
- name="Application"fileName="application.log">
-
- %d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n
-
备注:Java 的 JNDI 模块在 Android 上不可用。
那么,什么是 JNDI 呢?JNDI 即全称为 “Java Naming and Directory Interface”,中文释义为 JAVA 命名和目录接口,它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。站在数据请求角度可以这样理解:
存在一个类似于字典的数据源,应用程序可以通过 JNDI 接口,传一个特定的参数进去,就能获取到对象信息。但是,不同的数据源有不同的查找方式,所以 JNDI 也只是一个上层封装,在它下面也支持很多种具体的数据源。我们来了解一下其体系架构,具体如下所示:
基于上述架构图,我们可以看到:JNDI 体系结构由一个 API 和一个服务提供者接口 (Service Provider Interface) 组成。Java 应用程序使用 JNDI API 来访问各种命名和目录服务。SPI 允许以透明方式插入各种命名和目录服务,从而允许使用 JNDI API 的 Java 应用程序访问其服务。SPI 作为一种服务发现机制,通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。
通常来讲,JNDI 由以下软件包组成:
- javax.nameing
包含用于访问命名服务的类和接口。
- javax.nameing.directory
扩展核心 javax.命名包,以提供除命名服务之外访问目录的功能。
- javax.nameing.event
包含用于支持命名和目录服务中的事件通知的类和接口。
- javax.nameing.ldap
包含用于支持 LDAPv3 扩展操作和控制的类和接口。
- javax.nameing.spi
包含允许在 JNDI 下动态插入各种命名和目录服务提供程序的类和接口。
接下来,我们再来了解一下 JNDI 服务提供商相关概念,通常,要将 JNDI 与特定的命名或目录服务配合使用,我们需要一个 JNDI 服务提供程序,该提供程序是插入 JNDI API 下方以访问命名或目录服务的模块。目前,Java SE 发行版包括以下服务提供程序:
- LDAP 服务提供程序
- COS 命名服务提供商
- RMI 注册服务提供程序
- 域名解析服务提供商
在整个Java 生态体系栈中,Java 命名和目录接口 (JNDI) 是 Java 平台的一部分,它为基于 Java 技术的应用程序提供了多个命名和目录服务的统一接口。我们可以使用此行业标准构建功能强大且支持便携式目录的应用程序。
命名和目录服务通过提供有关用户、计算机、网络、服务和应用程序的各种信息的全网络共享,在 Intranet 和 Internet 中发挥着至关重要的作用。基于协同支撑角度而言,JNDI 与 Java Platform、Enterprise Edition (Java EE) 中的其他技术协同工作,以在分布式计算环境中组织和定位组件。
目前,JNDI 主要应用于如下 Java 版本中:Java SE 8 |Java SE 7 |Java SE 6 |J2SE 5.0 |J2SE 1.4.2
在实际的业务场景中,使用 JNDI 的任何工作都需要了解底层服务以及可访问的实现。例如,数据库连接服务调用特定属性和异常处理。但是,在通常情况下,JNDI 的抽象将连接配置与应用程序分离。让我们来探讨一下包含 JNDI 核心功能的名称。
以 Name Interface 名称接口为例,具体如下所示:
Name objectName = new CompositeName("java:com/env/jdbc");
Name Interface 名称接口提供了管理 JNDI 名称的组件名称和语法的功能。字符串的第一个标记表示全局上下文,之后添加的每个字符串表示下一个子上下文:
Enumeration<String> elements = objectName.getAll();while(elements.hasMoreElements()) { System.out.println(elements.nextElement()); }
其输出结果为:
java:com env jdbc
如我们所定义,/ 是 Name 子上下文的分隔符。现在,让我们添加一个子上下文:
jectName.add("example");
然后,我们再进行测试所添加的参数信息:
assertEquals("example", objectName.get(objectName.size() - 1));
我们再以 MySql 数据库为例,简要介绍下 JNDI 在应用系统中如何运行,我们在 Java 环境中配置一个数据库连接,例如配置名为“java:MySqlDS”。
然后别的 Java 进程通过 JNDI 去查找 ”java:MysqlDs“, 接着就会得到一个数据库连接。其代码配置信息如下所示:
Connection conn=null; // Context 为 JNDI 的类Context ctx = new InitialContext(); // JNDI 关键方法,通过 Loopup 找一个对象Object datasourceRef = ctx.lookup("java:MySqlDS"); //引用数据源 DataSource ds = (Datasource) datasourceRef; conn = ds.getConnection(); ...... c.close();
最后,我们来了解一下 JNDI 在现代应用程序体系架构中的作用。虽然 JNDI 在轻量级的容器化 Java 应用程序(如 Spring Boot)中扮演的角色越来越少,但还有其他用途。比如,在与后端组件交互,仍然使用 JNDI 的三种 Java 技术是 JDBC、EJB 和 JMS。所有这些都在 Java 企业应用程序中具有广泛的用途。
从管理规范角度而言,单独的 DevOps 团队可以管理相关环境变量资源,例如所有环境中敏感信息(数据库连接的用户名和密码)。可以在 Web 应用程序容器中创建 JNDI 资源,将 JNDI 用作在所有环境中工作的一致抽象层。基于此设置,允许开发人员创建和控制用于开发的本地定义,同时通过相同的 JNDI 名称连接到生产环境中的敏感资源。
最后,从本质上而言, JNDI 并没较多的内容,其仅仅是作为一种 Interface 存在,不过,正因为很不起眼,很容易让大家忽略掉,所以,一旦出现安全风险,还是很致命的。以上为 JNDI 的简要解析,希望对大家有用。