Apache Struts2 远程代码执行漏洞(S2-046)技术分析与防护方案

3月21日凌晨,Apache Struts2官方发布了一条安全公告,该公告指出Apache Struts2的Jakarta Multipart parser插件存在远程代码执行漏洞,漏洞编号为CVE-2017-5638。攻击者可以通过设置Content-Disposition的filename字段或者设置Content-Length超过2G这两种方式来触发异常并导致filename字段中的OGNL表达式得到执行从而达到远程攻击的目的。

该漏洞与之前S2-045漏洞成因及原理一样(CVE漏洞编号是同一个),只是漏洞利用的字段发生了改变。

相关地址:

https://struts.apache.org/docs/s2-046.html

   https://cwiki.apache.org/confluence/display/WW/S2-046

影响的版本

  • Struts 2.3.5 – Struts 2.3.31
  • Struts 2.5 – Struts 2.5.10

不受影响的版本

  • Struts 2.3.32
  • Struts 2.5.10.1

绿盟威胁情报中心NTI关于Struts2漏洞范围分布图

  • 全球分布图

  • 国内分布图

  • 全球排行

  • 国内排行

漏洞分析

  • 漏洞简介

Apache Struts2存在远程代码执行漏洞,攻击者可以将恶意代码放入http报文头部的Content-Disposition的filename字段,通过不恰当的filename字段或者大小超过2G的Content-Length字段来触发异常,进而导致任意代码执行。

  • 漏洞分析

官方的漏洞描述如下:

It is possible to perform a RCE attack with a malicious Content-Disposition value or with improper Content-Length header. If the Content-Dispostion / Content-Length value is not valid an exception is thrown which is then used to display an error message to a user. This is a different vector for the same vulnerability described in S2-045 (CVE-2017-5638)

从官方的漏洞描述我们可以知道,这个漏洞是由于Struts2对错误消息处理出现了问题,漏洞原理同S2-045。但是触发点不一样,可以通过Content-Dispostion中的含有%00的filename字段,或者Content-Length超过2G大小来触发这个漏洞。

本次分析基于Struts 2.3.24版本进行测试。POC及漏洞验证如下:

首先我们尝试Content-Disposition字段,看一下POC,攻击指令通过”Content-Disposition”的filename字段传递给存在漏洞的服务器。下面先看一下远程命令执行注入点,这个和S2-045是一样的:

观察报错输出的调用栈,从这里可以看出端倪:

最终出现异常的位置在checkFileName函数处,下面接着看checkFileName的实现:

这里首先判断文件名是否为null,当文件名中包含“\u0000”字符时就会抛出异常,异常信息中会带入filename字段,从我们的POC中看出这个字段就是我们要攻击的字段。接下来这个异常信息会交给buildErrorMessage执行。还是同s2-045的分析一样,我们首先来看Struts2的入口点doFilter

接着进入org.apache.struts2.dispatcher.Dispatcher.wrapRequest:

接着进入dispacher的wrapRequest,wrapRequest会调用org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper,在这个MultiPartRequestWrapper中会进行初始化,这就是设计模式中的适配器模式。初始化会调用org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse,parse的实现如下,首先是处理上传文件,如果遇到异常则会触发buildErrorMessage。

跟进去processUpload,实现如下:

再跟进去processFileField,在这里会处理各个header头:

在这里有getName函数,继续跟进去:

看看上面的注释就知道了,代码会特别处理NULL字符串,继续跟进去,这里就会验证filename参数,判断是否存在“\u0000”,然后抛出异常:

在JakartaMultiPartRequest.java 中buildErrorMessage函数,这个函数中的localizedTextUtil.findText会执行OGNL表达式,从而导致命令执行。我们先看下findtext的定义:

https://struts.apache.org/maven/struts2-core/apidocs/com/opensymphony/xwork2/util/LocalizedTextUtil.html

接下来它被JakartaMultiPartRequest.java中的parse调用。

对于第二种情况,是在JakartaStream 上传模式下,也就是Struts2 配置文件中设置了<constant name=”struts.multipart.parser” value=”jakarta-stream” /> ,Content-Length超出Struts2允许的最大值2G则抛出异常,异常信息包含filename字段会丢入OGNL表达式执行,POC如下:

漏洞原因:

上图中的代码先会判断请求的大小,当超过Struts2的默认数值2G时就会调用addFileSkeppedError函数,这个函数则会继续调用buildErrorMessage从而导致OGNL表达式远程代码执行。

(3)补丁分析

2.5.10.1版本的修复方案如下:

https://github.com/apache/struts/commit/b06dd50af2a3319dd896bf5c2f4972d2b772cf2b

2.3.32版本的修复方案如下:

https://github.com/apache/struts/commit/352306493971e7d5a756d61780d57a76eb1f519a

官方解决方案

  • 官方表示,如果用户正在使用基于Jakarta的文件上传插件,建议升级至Struts版本3.32或2.5.10.1。

Struts升级指南:

在2016年,Struts2远程代码执行漏洞(S2-032)同样引发了无数应用系统的“血案”。为保证升级的通用性,本次升级过程将以S2-032漏洞爆发后的第一个官方升级包(2.3.24)升到本次不受影响的2.3.32版本为例进行说明。

首先通过官网下载2.3.32版本的升级包,下载地址为:

http://struts.apache.org/download.cgi

通过下图Struts 2.3.24版本与2.3.32版本对比,可发现除了Struts2核心包(struts2-core与xwork-core)升级之外,一些依赖包的版本也进行了更新:

针对Struts2的升级,可将原应用相关的依赖jar包替换为最新的Struts2包,其中,有三个包是必须要升级的:

  • struts2-core-2.3.32.jar:Struts2核心包,也是此次漏洞发生的所在。
  • xwork-core-2.3.32.jar:Struts2依赖包,版本跟随Struts2一起更新。
  • ognl-3.0.19.jar:用于支持OGNL表达式,为其他包提供依赖。

以Struts2官方的样例struts2-blank应用为例进行说明,首先备份原依赖库,如果升级失败后可随时进行还原,不对业务造成影响:

然后对其中关键文件进行替换:

启动tomcat,并测试业务是否正常:

  • 如果暂时不便升级,官方也已准备了两个可以作为应急使用的Jakarta插件版本,用户可以下载使用,链接地址如下:

https://github.com/apache/struts-extras

直接将jar包放在WEB-INF/lib目录下重启应用即可。

  • 改用其他Multipart parser应用,比如Pell、Cos等解析器。具体更换操作如下:

以更换Cos为例,修改WEB-INF/classes目录的struts.xml文件,在struts节点下方添加<constant name=”struts.multipart.parser” value=”cos” />

 

提示:修改文件解析器会影响文件上传功能,请在修改后对相关功能进行调试。

技术防护方案

产品类

  • 如果您不清楚是否受此漏洞影响:
  • 公网资产可使用绿盟云 紧急漏洞在线检测,检测地址如下:

https://cloud.nsfocus.com/#/krosa/views/initcdr/productandservice?page_id=12

2、内网资产可以使用绿盟科技的远程安全评估系统(RSAS V5、V6)或 Web应用漏洞扫描系统(WVSS) 进行检测。

远程安全评估系统(RSAS V5)

http://update.nsfocus.com/update/listAurora/v/5

远程安全评估系统(RSAS V6)

http://update.nsfocus.com/update/listRsasDetail/v/vulweb

Web应用漏洞扫描系统(WVSS)

http://update.nsfocus.com/update/listWvss

通过上述链接,升级至最新版本即可进行检测!

  • 使用绿盟科技防护类产品(IPS/IDS/NF/WAF)进行防护:

入侵防护系统(IPS)

http://update.nsfocus.com/update/listIps

入侵检测系统(IDS)

http://update.nsfocus.com/update/listIds

下一代防火墙系统(NF)

http://update.nsfocus.com/update/listNf

Web应用防护系统(WAF)

http://update.nsfocus.com/update/wafIndex

通过上述链接,升级至最新版本即可进行防护!

服务类

绿盟科技提供专业的安全技术服务,全方位的保障客户应用系统安全,避免受此漏洞影响。

  • 短期服务:我们可以提供应急服务,服务内容包括对客户应用系统有针对性的提供修复建议,保障客户系统的安全升级。
  • 中长期服务:结合绿盟科技检测与防护产品,提供7*24的安全运营服务,在客户应用系统遭到安全威胁时第一时间通知客户,并定期进行安全检测,针对安全风险提供专业的解决方案。

临时解决方案

为防止Struts2升级可能对业务产生影响,绿盟科技提供专业的代码级防护方案,在保证安全的同时,尽最大努力,不对您的业务产生影响:

对Struts2 S2-046漏洞分析可知,漏洞的产生是将Content-Disposition的filename字段添加空指针字符或者设置Content-Length值超出2G触发异常导致的代码执行,我们可通过对content-length的值以及filename的有效性进行检测。

添加过滤器,将攻击行为的请求在进入struts2的StrutsPrepareAndExecuteFilter拦截器之前捕获。过滤器中,获取Content-Length以及Content-Type的值,将正常请求放过。放过的条件为:

  1. 请求内容不超过2M;
  2. Content-type值为正常值,不受S2-045漏洞影响。

然后对请求内容进行判断,获取上传文件名,判断请求中是否包含S2-046漏洞的利用相关条件:

1.请求长度是否超出2G;

2.filename中是否包含S2-046漏洞的利用字符。

然后将过滤器配置到web.xml文件中。

由于过滤器直接继承自org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter,因此,可将原有的Struts过滤器直接替换为我们实现的过滤器。

以上就是临时解决方案的操作步骤,最终的漏洞修复还是要升级到官方发布的不受影响的版本。

附代码:

Struts2Filter.java:

package com.strutsfilter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter;

public class Struts2Filter extends StrutsPrepareAndExecuteFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String contentType = null;
        int contentLength = request.getContentLength();
        ServletContext sctx = request.getServletContext();
        String params = sctx.getInitParameter("content-type-param");
        if (request.getContentType() != null) {
            contentType = request.getContentType().toLowerCase(Locale.ENGLISH);
            // 请求大小小于2M,不是文件上传并且是正常请求时,放过
            if (params.contains(contentType) && contentLength < 2097152) {
                super.doFilter(request, response, chain);
            }
        }
        contentType = contentType.contains(",") ? contentType.split(",")[0].trim() : contentType.split(";")[0].trim(); // 文件上传时过滤掉文件边界
        if (contentType != null && contentLength < 2097152000) { // 文件上传并且文件小于2g
            if (!Contain_space(request)) { // content-type位于白名单放过并且上传的文件名称当中不包括空字节
                super.doFilter(request, response, chain);
            } else {
                PrintWriter writer = response.getWriter();
                writer.write("reject!");
                writer.flush();
                writer.close();
            }
        }
    }

    public boolean Contain_space(ServletRequest request) {
        try {
            InputStream is = request.getInputStream();
            BufferedReader read = new BufferedReader(new InputStreamReader(is, "utf-8"));
            StringBuilder sb = new StringBuilder();
            String tmp = null;
            while ((tmp = read.readLine()) != null) {
                sb.append(tmp + "\r\n");
            }
            Pattern pattern = Pattern.compile("filename(.*?)\r\n");
            // 从filename一直截取到下一个换行符位置,通过正则表达式过滤出上传的文件名称
            Matcher matcher = pattern.matcher(sb.toString().toLowerCase(Locale.ENGLISH)); // 将文件请求内容全部小写
            while (matcher.find()) {
                String filename = matcher.group();
                if (filename.contains("\\0b") || filename.contains(" ") || filename.contains("\\u0000")
                        || filename.contains("@ognl")) { // 对文件名称进行过滤,筛选掉含有空字符的上传请求
                    return true;
                }
            }
        } catch (IOException e) {
            //e.printStackTrace();
        }
        return false;
    }

}

web.xml配置参考:

<web-app>
  <display-name>Struts 2 Web Application</display-name>

  <filter>
    <filter-name>struts2</filter-name>
      <filter-class>com.strutsfilter.Struts2Filter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  
  <context-param>
    <param-name>content-type-param</param-name>
    <param-value>application/octet-stream,application/pdf,application/vnd.android.package-archive,
    application/vnd.rn-realmedia-vbr,application/x-bmp,application/x-img,application/x-javascript,
    application/x-jpe,application/x-jpg,application/x-png,application/x-shockwave-flash,
    application/x-x509-ca-cert,application/x-xls,audio/mp3,image/gif,image/jpeg,image/png,
    image/x-icon,image/rfc822,text/css,text/html,text/plain,text/xml,video/mpg,video/mpeg4,video/mpg,
    video/x-ms-wmv,application/x-www-form-urlencoded,multipart/form-data</param-value>
  </context-param>
  
</web-app>

声 明

本安全公告仅用来描述可能存在的安全问题,绿盟科技不为此安全公告提供任何保证或承诺。由于传播、利用此安全公告所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,绿盟科技以及安全公告作者不为此承担任何责任。绿盟科技拥有对此安全公告的修改和解释权。如欲转载或传播此安全公告,必须保证此安全公告的完整性,包括版权声明等全部内容。未经绿盟科技允许,不得任意修改或者增减此安全公告内容,不得以任何方式将其用于商业目的。

关于绿盟科技

北京神州绿盟信息安全科技股份有限公司(简称绿盟科技)成立于2000年4月,总部位于北京。在国内外设有30多个分支机构,为政府、运营商、金融、能源、互联网以及教育、医疗等行业用户,提供具有核心竞争力的安全产品及解决方案,帮助客户实现业务的安全顺畅运行。

基于多年的安全攻防研究,绿盟科技在网络及终端安全、互联网基础安全、合规及安全管理等领域,为客户提供入侵检测/防护、抗拒绝服务攻击、远程安全评估以及Web安全防护等产品以及专业安全服务。

北京神州绿盟信息安全科技股份有限公司于2014年1月29日起在深圳证券交易所创业板上市交易,股票简称:绿盟科技,股票代码:300369。

如果您需要了解更多内容,可以
加入QQ群:570982169
直接询问:010-68438880

Spread the word. Share this post!

Meet The Author

Leave Comment