记一次struts2漏洞修复带来的问题
Posted 897807300
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次struts2漏洞修复带来的问题相关的知识,希望对你有一定的参考价值。
struts2作为万年漏洞王,感觉已经被弃如敝屣了,除了一些古老的项目,比如我手上的一个项目,以前每次出现漏洞就如临大敌,手忙脚乱的赶在公司红头文件发出来前修复它。然后改了一两次后毅然决然用别的框架代替它了。
万事大吉,再也不用担心struts2出漏洞了。然而上个月又爆出了个,还有个还在维护期的古老项目需要修复。。。又要填坑了。
攻略是将struts2的jar包替换,然后加一些配置就完了。
修复办法:
然后带来了一系列问题。
由于struts包由2.3.?升级到2.5.16,很多依赖包、配置需要响应调整。找了个总结的:
1,2.5.X版本不再提供xwork.jar ,整合到了 struts-core包中。
2,方法不能访问的问题,需要在每个action配置文件中加上 strict-method-invocation="false":
<package name="login" namespace="/login" extends="struts-default" strict-method-invocation="false">
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"><struts>
3 ,session失效的问题,针对weblogic server,增加session-descriptor节点:
<?xml version="1.0" encoding="UTF-8"?> <weblogic-web-app xmlns="http://www.bea.com/ns/weblogic/90"> <context-root>/ynwjnw</context-root> <container-descriptor> <servlet-reload-check-secs>-1</servlet-reload-check-secs> <prefer-web-inf-classes>true</prefer-web-inf-classes> </container-descriptor> <session-descriptor> <cookie-name>JSESSIONID1</cookie-name> </session-descriptor> </weblogic-web-app>
4,2.5.16版本jdk要求1.7。1.8版本编译后部署有问题
web.xml中
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
修改为
org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter
5.struts2对multipart/form-data解析需要自定义方法解析,否则丢失表单参数。
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="requestParser" class="com.***.omp.***.util.MyMultiPartRequest" scope="default"/>
由于xwork做了大改,AbstractMultiPartRequest类的一些属性和方法也修改了。MyMultiPartRequest类也需要相应调整。
import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadBase; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.RequestContext; import org.apache.commons.fileupload.disk.DiskFileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.dispatcher.LocalizedMessage; import org.apache.struts2.dispatcher.multipart.AbstractMultiPartRequest; import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest; import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile; import org.apache.struts2.dispatcher.multipart.UploadedFile; public class MyMultiPartRequest extends AbstractMultiPartRequest{ static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class); // maps parameter name -> List of FileItem objects protected Map<String, List<FileItem>> files = new HashMap<String, List<FileItem>>(); // maps parameter name -> List of param values protected Map<String, List<String>> params = new HashMap<>(); // any errors while processing this request protected List<LocalizedMessage> errors = new ArrayList<LocalizedMessage>(); // protected long maxSize; // private Locale defaultLocale = Locale.ENGLISH; /** * Creates a new request wrapper to handle multi-part data using methods adapted from Jason Pell\'s * multipart classes (see class description). * * @param saveDir the directory to save off the file * @param request the request containing the multipart * @throws java.io.IOException is thrown if encoding fails. */ public void parse(HttpServletRequest request, String saveDir) throws IOException { try { setLocale(request); processUpload(request, saveDir); } catch (FileUploadBase.SizeLimitExceededException e) { if (LOG.isWarnEnabled()) { LOG.warn("Request exceeded size limit!", e); } LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{e.getPermittedSize(), e.getActualSize()}); if (!errors.contains(errorMessage)) { errors.add(errorMessage); } } catch (Exception e) { if (LOG.isWarnEnabled()) { LOG.warn("Unable to parse request", e); } LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{}); if (!errors.contains(errorMessage)) { errors.add(errorMessage); } } } protected void setLocale(HttpServletRequest request) { if (defaultLocale == null) { defaultLocale = request.getLocale(); } } protected LocalizedMessage buildErrorMessage(Throwable e, Object[] args) { String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName(); LOG.debug("Preparing error message for key: [{}]", errorKey); return new LocalizedMessage(this.getClass(), errorKey, e.getMessage(), args); } private void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException { for (FileItem item : parseRequest(request, saveDir)) { if (LOG.isDebugEnabled()) { LOG.debug("Found item " + item.getFieldName()); } if (item.isFormField()) { processNormalFormField(item, request.getCharacterEncoding()); } else { processFileField(item); } } } private void processFileField(FileItem item) { if (LOG.isDebugEnabled()) { LOG.debug("Item is a file upload"); } // Skip file uploads that don\'t have a file name - meaning that no file was selected. if (item.getName() == null || item.getName().trim().length() < 1) { LOG.debug("No file has been uploaded for the field: " + item.getFieldName()); return; } List<FileItem> values; if (files.get(item.getFieldName()) != null) { values = files.get(item.getFieldName()); } else { values = new ArrayList<FileItem>(); } values.add(item); files.put(item.getFieldName(), values); } private void processNormalFormField(FileItem item, String charset) throws UnsupportedEncodingException { if (LOG.isDebugEnabled()) { LOG.debug("Item is a normal form field"); } List<String> values; if (params.get(item.getFieldName()) != null) { values = params.get(item.getFieldName()); } else { values = new ArrayList<String>(); } // note: see http://jira.opensymphony.com/browse/WW-633 // basically, in some cases the charset may be null, so // we\'re just going to try to "other" method (no idea if this // will work) if (charset != null) { values.add(item.getString(charset)); } else { values.add(item.getString()); } params.put(item.getFieldName(), values); item.delete(); } private List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException { DiskFileItemFactory fac = createDiskFileItemFactory(saveDir); ServletFileUpload upload = new ServletFileUpload(fac); upload.setSizeMax(maxSize); // 设置监听器 FileUploadListener progressListener = new FileUploadListener(servletRequest); upload.setProgressListener(progressListener); return upload.parseRequest(createRequestContext(servletRequest)); } private DiskFileItemFactory createDiskFileItemFactory(String saveDir) { DiskFileItemFactory fac = new DiskFileItemFactory(); // Make sure that the data is written to file fac.setSizeThreshold(0); if (saveDir != null) { fac.setRepository(new File(saveDir)); } return fac; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames() */ public Enumeration<String> getFileParameterNames() { return Collections.enumeration(files.keySet()); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String) */ public String[] getContentType(String fieldName) { List<FileItem> items = files.get(fieldName); if (items == null) { return null; } List<String> contentTypes = new ArrayList<String>(items.size()); for (FileItem fileItem : items) { contentTypes.add(fileItem.getContentType()); } return contentTypes.toArray(new String[contentTypes.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String) */ public UploadedFile[] getFile(String fieldName) { List<FileItem> items = files.get(fieldName); if (items == null) { return null; } List<UploadedFile> fileList = new ArrayList<>(items.size()); for (FileItem fileItem : items) { File storeLocation = ((DiskFileItem) fileItem).getStoreLocation(); if (fileItem.isInMemory() && storeLocation != null && !storeLocation.exists()) { try { storeLocation.createNewFile(); } catch (IOException e) { LOG.error("Cannot write uploaded empty file to disk: {}", storeLocation.getAbsolutePath(), e); } } fileList.add(new StrutsUploadedFile(storeLocation)); } return fileList.toArray(new UploadedFile[fileList.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String) */ public String[] getFileNames(String fieldName) { List<FileItem> items = files.get(fieldName); if (items == null) { return null; } List<String> fileNames = new ArrayList<String>(items.size()); for (FileItem fileItem : items) { fileNames.add(getCanonicalName(fileItem.getName())); } return fileNames.toArray(new String[fileNames.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String) */ public String[] getFilesystemName(String fieldName) { List<FileItem> items = files.get(fieldName); if (items == null) { return null; } List<String> fileNames = new ArrayList<String>(items.size()); for (FileItem fileItem : items) { fileNames.add(((DiskFileItem) fileItem).getStoreLocation().getName()); } return fileNames.toArray(new String[fileNames.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String) */ public String getParameter(String name) { List<String> v = params.get(name); if (v != null && v.size() > 0) { return v.get(0); } return null; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames() */ public Enumeration<String> getParameterNames() { return Collections.enumeration(params.keySet()); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String) */ public String[] getParameterValues(String name) { List<String> v = params.get(name); if (v != null && v.size() > 0) { return v.toArray(new String[v.size()]); } return null; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors() */ public List<LocalizedMessage> getErrors() { return errors; } /** * Returns the canonical name of the given file. * * @param filename the given file * @return the canonical name of the given file */ // private String getCanonicalName(String filename) { // int forwardSlash = filename.lastIndexOf("/"); // int backwardSlash = filename.lastIndexOf("\\\\"); // if (forwardSlash != -1 && forwardSlash > backwardSlash) { // filename = filename.substring(forwardSlash + 1, filename.length()); // } else if (backwardSlash != -1 && backwardSlash >= forwardSlash) { // filename = filename.substring(backwardSlash + 1, filename.length()); // } // // return filename; // } /** * Creates a RequestContext needed by Jakarta Commons Upload. * * @param req the request. * @return a new request context. */ private RequestContext createRequestContext(final HttpServletRequest req) { return new RequestContext() { public String getCharacterEncoding() { return req.getCharacterEncoding(); } public String getContentType() { return req.getContentType(); } public int getContentLength() { return req.getContentLength(); } public InputStream getInputStream() throws IOException { InputStream in = req.getInputStream(); if (in == null) { throw new IOException("Missing content in the request"); } return req.getInputStream(); } }; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() */ public void cleanUp() { Set<String> names = files.keySet(); for (String name : names) { List<FileItem> items = files.get(name); for (FileItem item : items) { LOG.debug("Removing file {} {}", name, item ); if (!item.isInMemory()) { item.delete(); } } } } }
修改完成,以为完事大吉,但又出问题了。发现上传功能后台接受表单的参数会叠加,比如第一次传name是123,第二次再传123,name变成了123,123.经过定位,排除了前端问题。后端debug发现是每次参数都会叠加,目测是单例问题。但struts2默认是多例的,为什么会出现这个问题呢。配置没改,multi数据解析类也只是改了一些兼容性问题。难道我记错了?于是尝试把action上能加@scope("prototype")的地方都尝试加下,发现没解决问题,继续debug。发现自定义的MyMultiPartRequest类每次解析都会保存上一次的内容。原因找到了,这个类是单例的。看了下配置,bean的scope是“dufault”,默认是单例?不会呀。改成多例试试,问题解决。查了下原因,Struts2单独使用是多例,但交给spring管理后默认是单例了?百撕不得其解。
问题来了。之前的版本为什么没出现这个问题?未完待续。。。
以上是关于记一次struts2漏洞修复带来的问题的主要内容,如果未能解决你的问题,请参考以下文章
记一次漏洞修复:DES和Triple DES 信息泄露漏洞(CVE-2016-2183)
漏洞修复系列如何不安装软件或应用,直接查看软件内依赖包的版本信息?记一次漏洞修复实践分享