CVE 2019 9827代码审计

简介

Hawt Hawtio是一款用于管理Java内容的模块化Web控制台程序。 Hawt Hawtio 2.5.0及之前版本中存在代码问题漏洞。该漏洞源于网络系统或产品的代码开发过程中存在设计或实现不当的问题。

环境搭建

源码包下载地址:

https://oss.sonatype.org/content/repositories/public/io/hawt/hawtio-default/2.5.0/hawtio-default-2.5.0.war

通过tomcat部署:

去tomcat的后台,然后选择WAR file to deply栏目,点击选择hawtio-default-2.5.0.war上传,最后deplay部署即可:

image-20240709175157949

布置以后,会出现布置好的应用,点击应用进入即可:

image-20240709175244400

image-20240709175254892

漏洞分析

源码获取方式:

  • 可以通过反编译获取本程序的源码,
  • 或者通过 github 的 tree 分支来获取源码。

先测试一下漏洞点:

1
http://127.0.0.1:8080/hawtio-default-2.5.0/proxy/http://localhost:7777/1.txt

image-20240709175639767

hawtio-system-2.5.0.jar直接反编译

image-20240709175850746

通过漏洞描述可以看出问题出现在proxy,所以直接搜索ProxyServlet之类的字眼

image-20240709185519716

找到相关文件:hawtio-system/src/main/java/io/hawt/web/proxy/ProxyServlet.java

service方法开始分析

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
        ProxyAddress proxyAddress = this.parseProxyAddress(servletRequest);
        if (proxyAddress != null && proxyAddress.getFullProxyUrl() != null) {
            if (proxyAddress instanceof ProxyDetails) {
                ProxyDetails details = (ProxyDetails)proxyAddress;
                if (!this.whitelist.isAllowed(details)) {
                    LOG.debug("Rejecting {}", proxyAddress);
                    ServletHelpers.doForbidden(servletResponse, ForbiddenReason.HOST_NOT_ALLOWED);
                    return;
                }
            }

            String method = servletRequest.getMethod();
            String proxyRequestUri = proxyAddress.getFullProxyUrl();

            URI targetUriObj;
            try {
                targetUriObj = new URI(proxyRequestUri);
            } catch (URISyntaxException var25) {
                LOG.error("URL '{}' is not valid: {}", proxyRequestUri, var25.getMessage());
                servletResponse.setStatus(404);
                return;
            }

            Object proxyRequest;
            if (servletRequest.getHeader("Content-Length") == null && servletRequest.getHeader("Transfer-Encoding") == null) {
                proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
            } else {
                HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
                eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), (long)servletRequest.getContentLength()));
                proxyRequest = eProxyRequest;
            }

            this.copyRequestHeaders(servletRequest, (HttpRequest)proxyRequest, targetUriObj);
            String username = proxyAddress.getUserName();
            String password = proxyAddress.getPassword();
            if (Strings.isNotBlank(username) && Strings.isNotBlank(password)) {
                String encodedCreds = Base64.encodeBase64String((username + ":" + password).getBytes());
                ((HttpRequest)proxyRequest).setHeader("Authorization", "Basic " + encodedCreds);
            }

            Header proxyAuthHeader = ((HttpRequest)proxyRequest).getFirstHeader("Authorization");
            if (proxyAuthHeader != null) {
                String proxyAuth = proxyAuthHeader.getValue();
                HttpSession session = servletRequest.getSession();
                if (session != null) {
                    String previousProxyCredentials = (String)session.getAttribute("proxy-credentials");
                    if (previousProxyCredentials != null && !previousProxyCredentials.equals(proxyAuth)) {
                        this.cookieStore.clear();
                    }

                    session.setAttribute("proxy-credentials", proxyAuth);
                }
            }

            this.setXForwardedForHeader(servletRequest, (HttpRequest)proxyRequest);
            CloseableHttpResponse proxyResponse = null;
            int statusCode = 0;

            try {
                if (this.doLog) {
                    this.log("proxy " + method + " uri: " + servletRequest.getRequestURI() + " -- " + ((HttpRequest)proxyRequest).getRequestLine().getUri());
                }

                LOG.debug("proxy {} uri: {} -- {}", new Object[]{method, servletRequest.getRequestURI(), ((HttpRequest)proxyRequest).getRequestLine().getUri()});
                proxyResponse = this.proxyClient.execute(URIUtils.extractHost(targetUriObj), (HttpRequest)proxyRequest);
                statusCode = proxyResponse.getStatusLine().getStatusCode();
                if (statusCode != 401 && statusCode != 403) {
                    if (this.doResponseRedirectOrNotModifiedLogic(servletRequest, servletResponse, proxyResponse, statusCode, targetUriObj)) {
                        return;
                    }
                } else {
                    if (this.doLog) {
                        this.log("Authentication Failed on remote server " + proxyRequestUri);
                    }

                    LOG.debug("Authentication Failed on remote server {}", proxyRequestUri);
                }

                servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());
                this.copyResponseHeaders(proxyResponse, servletResponse);
                this.copyResponseEntity(proxyResponse, servletResponse);
            } catch (Exception var26) {
                if (proxyRequest instanceof AbortableHttpRequest) {
                    AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest)proxyRequest;
                    abortableHttpRequest.abort();
                }

                LOG.debug("Proxy to " + proxyRequestUri + " failed", var26);
                if (!(var26 instanceof ConnectException) && !(var26 instanceof UnknownHostException)) {
                    if (var26 instanceof ServletException) {
                        servletResponse.sendError(502, var26.getMessage());
                    } else if (var26 instanceof SecurityException) {
                        servletResponse.setHeader("WWW-Authenticate", "Basic");
                        servletResponse.sendError(statusCode, var26.getMessage());
                    } else {
                        servletResponse.sendError(500, var26.getMessage());
                    }
                } else {
                    servletResponse.setStatus(404);
                }
            } finally {
                if (proxyResponse != null) {
                    EntityUtils.consumeQuietly(proxyResponse.getEntity());

                    try {
                        proxyResponse.close();
                    } catch (IOException var24) {
                        LOG.error("Error closing proxy client response: {}", var24.getMessage());
                    }
                }

            }

        } else {
            servletResponse.setStatus(404);
        }
    }

通过parseProxyAddress函数获取 URL 地址,跟进:

image-20240709190029613

通过getPathInfo()方法进行同名方法调用,需要注意的是这里获取了Authorization字段,只是获取了里面的username和password进行base64解码直接获取,没有进行身份验证

image-20240709190058946

ProxyDetails方法中获取了/proxy/路径后内容,并且进行相应的协议、路径、端口解析,需要注意,这里只能用http协议,否则在catch中会将端口80或443拼接,最终请求错误

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

    public ProxyDetails(String pathInfo) {
        this.scheme = "http";
        this.path = "";
        this.port = 80;
        this.hostAndPort = pathInfo.replace(" ", "%20");
        if (this.hostAndPort != null) {
            while(this.hostAndPort.startsWith("/")) {
                this.hostAndPort = this.hostAndPort.substring(1);
            }

            if (this.hostAndPort.startsWith("http/")) {
                this.scheme = "http";
                this.hostAndPort = this.hostAndPort.substring(5);
            } else if (this.hostAndPort.startsWith("https/")) {
                this.scheme = "https";
                this.hostAndPort = this.hostAndPort.substring(6);
            }

            int idx = this.hostAndPort.indexOf("@");
            if (idx > 0) {
                this.userName = this.hostAndPort.substring(0, idx);
                this.hostAndPort = this.hostAndPort.substring(idx + 1);
                idx = this.indexOf(this.userName, ":", "/");
                if (idx > 0) {
                    this.password = this.userName.substring(idx + 1);
                    this.userName = this.userName.substring(0, idx);
                }
            }

            this.host = this.hostAndPort;
            int schemeIdx = this.indexOf(this.hostAndPort, "://");
            if (schemeIdx > 0) {
                this.scheme = this.hostAndPort.substring(0, schemeIdx);
                this.hostAndPort = this.hostAndPort.substring(schemeIdx + 3);
            } else {
                schemeIdx = this.indexOf(this.hostAndPort, ":/");
                if (schemeIdx > 0) {
                    this.scheme = this.hostAndPort.substring(0, schemeIdx);
                    this.hostAndPort = this.hostAndPort.substring(schemeIdx + 2);
                }
            }

            idx = this.indexOf(this.hostAndPort, ":", "/");
            if (idx > 0) {
                this.host = this.hostAndPort.substring(0, idx);
                String portText = this.hostAndPort.substring(idx + 1);
                idx = portText.indexOf("/");
                if (idx >= 0) {
                    this.path = portText.substring(idx);
                    portText = portText.substring(0, idx);
                }

                if (Strings.isNotBlank(portText)) {
                    try {
                        this.port = Integer.parseInt(portText);
                        this.hostAndPort = this.host + ":" + this.port;
                    } catch (NumberFormatException var6) {
                        this.port = "http".equals(this.scheme) ? 80 : 443;
                        this.path = "/" + portText + this.path;
                        this.hostAndPort = this.host;
                    }
                } else {
                    this.hostAndPort = this.host;
                }
            }

            this.stringProxyURL = this.scheme + "://" + this.hostAndPort + this.path;
            if (LOG.isDebugEnabled()) {
                LOG.debug("Proxying to " + this.stringProxyURL + " as user: " + this.userName);
            }

        }
    }

返回主代码中,然后判断其是否为空,如果不为空,通过whitelist.isAllowed()判断该 URL 是否在白名单里,跟进 whitelist

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

    public ProxyWhitelist(String whitelistStr, boolean probeLocal) {
        if (Strings.isBlank(whitelistStr)) {
            this.whitelist = new CopyOnWriteArraySet();
            this.regexWhitelist = Collections.emptyList();
        } else {
            this.whitelist = new CopyOnWriteArraySet(this.filterRegex(Strings.split(whitelistStr, ",")));
            this.regexWhitelist = this.buildRegexWhitelist(Strings.split(whitelistStr, ","));
        }

        if (probeLocal) {
            LOG.info("Probing local addresses ...");
            this.initialiseWhitelist();
        } else {
            LOG.info("Probing local addresses disabled");
            this.whitelist.add("localhost");
            this.whitelist.add("127.0.0.1");
        }

        LOG.info("Initial proxy whitelist: {}", this.whitelist);
        this.mBeanServer = ManagementFactory.getPlatformMBeanServer();

        try {
            this.fabricMBean = new ObjectName("io.fabric8:type=Fabric");
        } catch (MalformedObjectNameException var4) {
            throw new RuntimeException(var4);
        }
    }

判断 URL 是否为 localhost、127.0.0.1或者用户自己更新的白名单列表,如果不是返回 false。

返回到 service(),向下走:

image-20240709190751511

BasicHttpEntityEnclosingRequest()拥有RequestLineHttpEntity以及Header,这里用的是 entity,HttpEntity即为消息体,包含了三种类型:数据流方式、自我包含方式以及封装模式(包含上述两种方式),这里就是一个基于HttpEntity的, HttpRequest接口实现,类似于urlConnection

所以 service()的主要作用就是:获取请求,然后HttpServiceHttpClient传来的请求通过向下转型成BasicHttpEntityEnclosingRequest,再调用HttpEntity,最终得到请求流内容。

虽然对传入的 URL 进行了限制,但是没有对端口、协议进行相应的限制,从而导致了 SSRF 漏洞。

修复方案

image-20240709192618960

未经验证的用户禁止访问该页面。

参考

https://xz.aliyun.com/t/7186

https://hackerqwq.github.io/2021/11/10/javaweb%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0-SSRF/#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9827

https://github.com/hawtio/hawtio/compare/hawtio-2.5.0...hawtio-2.9.1

https://github.com/hawtio/hawtio/tree/hawtio-2.5.0/

0%