CVE-2022-22954 VMware Workspace ONE Access Server-side Template Injection RCE

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

freemarker ssti

# 安装环境

r师给的镜像 identity-manager-21.08.0.1-19010796_OVF10.ova,导入ova的时候要设置下fqdn,不然安装时链接数据库会报错。

image.png

# 分析

这个老外的推特中有一点点可以参考的信息

image.png

有两个报错信息,我们先找到这个模板所在。

看路由是在catalog-portal app下,cd到/opt/vmware/horizon/workspace/webapps/catalog-portal,然后把jar包拖出来解压之后,grep -irn "console.log"

发现在lib/endusercatalog-ui-1.0-SNAPSHOT-classes.jar!/templates/customError.ftl:61这个地方存在模板注入

image.png

freemarker官网文档中给出了安全问题的提示

https://freemarker.apache.org/docs/ref_builtins_expert.html#ref_builtin_eval

image.png

确认了这个地方就是freemarker ssti的地方。

接着看哪个路由可以渲染这个模板,找到了com.vmware.endusercatalog.ui.web.UiErrorController#handleGenericError

image.png

这个函数没有requestMapping,其中errorObj由参数传入,查找函数调用,寻找从requestMapping进来的控制器能调用到这个函数的。

endusercatalog-ui-1.0-SNAPSHOT-classes.jar这个jar包是一个spring项目

image.png

有几个控制器,其中UiErrorController控制器有两个requestMapping

image.png

这两个路由均可以走到getErrorPage

image.png

getErrorPage会根据handleUnauthorizedError和handleGenericError两个函数拿到需要渲染的模板

其中handleUnauthorizedError只有一个分支可以进入handleGenericError

image.png

到这里,想要控制errorObj,则整个数据流向如图

image.png

我们需要让其走到handleGenericError才可以rce。

但是此时有一个问题,如果直接访问这两个requestMapping,我们无法控制javax.servlet.error.message,也就无法控制errorObj,所以找一找哪个控制器跳转过来的。

com.vmware.endusercatalog.ui.web.UiApplicationExceptionResolver类中,通过@ExceptionHandler注解标明这是一个异常处理类。

image.png

当程序直接抛出Exception类型的异常时会进入handleAnyGenericException,最终都会返回/ui/view/error,并且设置了errorObj所需要的Attribute

1
2
3
request.setAttribute("javax.servlet.error.status_code", responseCode);
request.setAttribute("javax.servlet.error.message", errorJson);
request.setAttribute("javax.servlet.error.exception_type", ex.getClass());

errorJson来自于LocalizationParamValueException异常的getArgs。

image.png

即自身args属性,通过构造函数的第二个参数传入

image.png

如果我们可以控制抛出异常的参数,就可以把freemarker的payload传入errorObj。

然后我找到了com.vmware.endusercatalog.ui.web.WorkspaceOauth2CodeVerificationController#authorizeError

image.png

尝试构造一下

1
https://id.test.local/catalog-portal/ui/oauth/verify?error=1&error_description=a

image.png

直接302了,调试发现errorMessage确实已经有我们的恶意值1了,但是被sendRedirect,而不是handleGenericError。

image.png

上文讲到必须要handleGenericError才能return customError。调试发现

isSpecificUnauthError(excpClass)为false,this.isMdmOnlyUnauthorizedAccessError(request, excpClass)也为false。

1
2
3
    private boolean isSpecificUnauthError(String exceptionClass) {
        return Predicates.or(new Predicate[]{this::isDeviceRecordNotFoundError, this::isUserMismatchError, this::isMdmAuthUnhandledError, this::isDeviceStateInvalidError, this::isExternalUserIdNotFoundError}).apply(exceptionClass);
    }

isSpecificUnauthError过不去,因为com.vmware.endusercatalog.ui.web.WorkspaceOauth2CodeVerificationController#authorizeError抛出的异常是AuthorizationCodeFailedRetrievalException,并非DeviceRecordNotFoundException、UserMismatchException、MdmAuthUnhandledException、DeviceStateInvalidException、ExternalUserIdNotFoundException之一,这个死绕不过去。

isMdmOnlyUnauthorizedAccessError中this.isMdmOnlyMode(request)永为false

image.png

因为((TenantAdapters)adapters).isMdmOnlyMode()一直追溯到com.vmware.endusercatalog.repositories.TenantAdapters#getAdapterAttributesOptional

image.png

当程序配置好之后this.adapters就有了AdapterType.WORKSPACE

1
2
3
    public boolean isWorkspaceConfigured() {
        return this.getAdapterAttributesOptional(AdapterType.WORKSPACE).isPresent();
    }

而取反之后为false。

1
2
3
    public boolean isMdmOnlyMode() {
        return !this.isWorkspaceConfigured();
    }

所以isMdmOnlyUnauthorizedAccessError判断永为false,所以这条路走不通了。

回头看com.vmware.endusercatalog.ui.UiApplication,注解声明自动装配com.vmware.endusercatalog.auth

image.png

com.vmware.endusercatalog.auth.interceptor.AuthContextPopulationInterceptor

image.png

build函数

1
2
3
        public AuthContext build() {
            return new AuthContext(this);
        }

withDeviceId和withDeviceType分别设置自身的deviceId和deviceType字段。然后build()函数new了一个AuthContext,跟进到com.vmware.endusercatalog.auth.AuthContext#AuthContext构造函数

image.png

这里抛出了一个InvalidAuthContextException异常,参数也可控,if判断只需要让this.deviceId、this.deviceType不为空即可。

image.png

# payload

image.png

有个坑,host可以为localhost,可以为域名,但是不能为ip,因为ip对不上fqdn。

# 后利用

写shell在/opt/vmware/horizon/workspace/webapps/catalog-portal/tomcat目录下,发现post会校验csrf,导致哥斯拉连不上,打入一个listener的内存马就可以了。

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