Skeleton Cookie: Breaking into Safeguard with CVE-2024-45488
Join us as we reveal how CVE-2024-45488 can let attackers gain access to your corporate password vault and uncover hidden secrets of Microsoft DPAPI.
Read ArticleIvanti Connect Secure versions prior to 22.7R2.1 and 22.7R2.2, and Ivanti Policy Secure versions prior to 22.7R1.1, contain a CRLF injection vulnerability which could be exploited by an authenticated administrator to execute arbitrary code with root
privileges.
An attacker with administrative access to the web application, potentially gained through exploitation of previous vulnerabilities or credential compromise, could execute arbitrary code on the underlying system with root
privileges.
This vulnerability allows an authenticated administrator to execute arbitrary code with root
privileges on the underlying system.
While the vulnerability requires authentication, past vulnerabilities, such as CVE-2020-8260 and CVE-2020-8243, which also required authentication, were exploited in the wild. However, Ivanti have advised that they are not aware of exploitation of this vulnerability in the wild prior to public disclosure.
Update to ICS version 22.7R2.1, 22.7R2.2, or Ivanti Policy Secure 22.7R1.1.
Follow the mitigation guidance published by Ivanti to ensure administrative interfaces are not exposed to the interface, thus limiting the opportunity for attackers to exploit vulnerabilities such as the one described in this advisory.
ICS versions prior to 22.7R2.1.
Ivanti Connect Secure provides functionality for administrative users to generate new certificates via the admin web application at the URL: /dana-admin/cert/admincert.cgi
The following screenshot shows the admincert.cgi
page:
Admin Certificate Generation Page
When clicking on the “New CSR” option, the user is then prompted to fill out some details which will be used to generate a CSR, via the CGI script at /dana-admin/cert/admincertnewcsr.cgi
New Certificate Signing Request Form
The POST
request that this form generates looks like the below:
POST /dana-admin/cert/admincertnewcsr.cgi HTTP/1.1
Host: 192.168.2.92
Cookie: DSSignInURL=/admin; SUPPORTCHROMEOS=1; PHC_DISABLED=1; DSBrowserID=f5afd4b930af818747c03bc6f2adaf59; id=state_16f05e4e8ed9a95921f9e5561b63b977; DSSIGNIN=url_admin; DSPERSISTMSG=; DSDID=00064f3b9acd709a; DSFirstAccess=1705398262; DSLastAccess=1705398282; DSLaunchURL=2F64616E612D61646D696E2F636572742F61646D696E636572742E636769; DSID=4b2c0f96862dc17c23622ef26a7a6db8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 594
Origin: https://192.168.2.92
Referer: https://192.168.2.92/dana-admin/cert/admincertnewcsr.cgideletecsr=&selectedCSRIds=&xsauth=4e2fea1b8e12585a4744fc72820de0cc
Connection: close
xsauth=4e2fea1b8e12585a4744fc72820de0cc&commonName=vpn.pcs.local&organizationName=Example+Corp&organizationalUnitName=IT+Group&localityName=SomeCity&stateOrProvinceName=California&countryName=US&emailAddress=test%40example.com&keytype=RSA&keylength=1024&eccurve=prime256v1&random=aaaaaaaaaaaaaaaaaaaa&newcsr=yes&certType=device&btnCreateCSR=Create+CSR
Reviewing the source code for the admincertnewcsr.cgi
, which is written in Perl, we can see that main subroutine takes these values from the POST
request.
The commonName
is validated against a regex, but the other values appear to have no validation checks:
Common Name Validation
These user-controlled values are added to a new object from: DSCert::NewCSR()
. This is a Perl module which can be found at /home/perl/DSCert.pm
. This module contains Swig wrappers, which call into native code in DSCert.so
:
DSCert::NewCSR
DSCert.so
imports the DSCert::NewCSR::NewCSR
function from libdsplibs.so
:
NewCSR Function
Once a CSR object has been created in admincertnewcsr.cgi
, it then calls DSCert::Admin::addCSR
addCSR Function
Since we know this is exported by libdsplibs.so
, we can go and decompile the code in IDA to see how the CSR is generated.
There is some additional validation in this function for certain parameters, such as the country code:
Validation of the Country Code
However, a number of parameters are passed unfiltered, and written into a CSR config file at /home/runtime/tmp/csrconf.<pid>
CSR Config File
This CSR config is then passed to DSCert::runOpenssl
, which executes the command:
openssl req config /home/runtime/tmp/csrconf.<pid> -new -utf8 -out /home/runtime/tmp/csr.<pid>
DSCert::runOpenssl
As documented in the OpenSSL docs, there are various fields which can be passed within the configuration. However, of interest to an attacker is the engine option, which allows the user to specify an arbitrary “engine” to be used. As described here, the Engine API provides an interface for “adding alternative implementations of cryptographic primitives”, and has since been superseded in OpenSSL 3.0 by the Provider API.
From an attacker perspective, this feature essentially allows us to provide a path to an external library where OpenSSL will attempt to load the specified engine from. If an attacker can inject this option into a config file, then they can gain arbitrary code execution when the engine is loaded.
As mentioned previously, there is only limited validation carried out on the user-input before it is passed into the config file. Therefore, an attacker could inject CRLF characters in one of the POST
parameters to add their own section into the config file, specifying an arbitrary engine path.
To confirm that CRLF injection is possible we can do a simple test. First we make a request, injecting into the localityName
parameter, with a CRLF sequence after the value, followed by the value [foo]
. If this is successful, and the certificate doesn’t contain [foo]
in the locality, then it’s a good sign that CRLF injection is working and the [foo]
value is ignored, since it’s interpreted as an empty section:
CRLF Injection Test
As suspected, this is successful, and the locality shows that the [foo]
value was ignored.
Locality Value Shown in CSR Details
Next, we can attempt to inject a new engine section, and should expect an error to be raised, as we will pass a path to a non-existent engine library.
As shown below, this results in an error, since the /tmp/test.so
file doesn’t exist on disk:
CSR Generation Failure
This confirms that the server is vulnerable to CRLF injection.
To confirm whether this issue can be exploited to gain Remote Code Execution, we need to create an OpenSSL “engine” payload (which is simply a shared object file).
The Ivanti Connect Secure appliance is based on Centos 6.4, which is using a fairly old kernel and glibc
version. We compiled our test payload using the ubuntu:xenial
x86 Docker image.
Looking again at our PoC request, we will inject a CRLF sequence with a fake engine path like the following:
[default]
openssl_conf = openssl_init
[openssl_init]
engines = engine_section
[engine_section]
foo = foo_section
[foo_section]
engine_id = foo
dynamic_path = /tmp/test.so
init = 0
However, there is one issue we need to overcome first. The dynamic_path
option must reference a shared object file on disk. This means we need to find a way to get a file onto the target server so that it can be referenced in the OpenSSL config.
Interestingly, OpenSSL does not appear to mind:
dynamic_path
. It does not need to be executable..so
.The Ivanti Connect Secure appliance includes a feature called “Client Log Upload”, which allows users of the VPN client and/or Java Applet to upload client logs for diagnostic purposes.
When a user uploads client logs, these are stored in the folder /home/runtime/uploadlog/
, with a filename containing the timestamp and ending in .zip
, for example: log-20240115-035029.zip
. These files are uploaded via the /dana/uploadlog/uploadlog.cgi
CGI script.
This CGI does not verify whether the client logs content is valid, nor whether the file itself is even a ZIP file.
ZIP File Processing
The following screenshot shows the POST
request used to upload log files:
Upload Logs POST Request
The resulting .zip
file name is then shown in the response:
ZIP File Uploaded Successfully
Therefore, to get our payload onto the server at a known local path, we can simply upload a fake client log via uploadlog.cgi
. We can then look up the file name and infer the path. This can then be referenced in our injected config, for example:
..
[foo_section]
engine_id = foo
dynamic_path = /home/runtime/uploadlog/log-20240115-035029.zip
init = 0
Note: client logs must first be enabled, and a low-privileged user account must be used to upload them. However, assuming the attacker has administrative access to the web application, both of these prerequisites can be satisfied directly by the attacker (i.e. by enabling client-log upload and/or adding a Local low-privileged user if required). Client logs can be enabled in: Users -> User Roles -> Users (Role) -> General -> Session Options -> Enable Upload
Now we can make a POST
request containing our CRLF and OpenSSL payload. As shown in the below screenshot, this results in successful exploitation, and a reverse shell running under the root
user is established:
Reverse Shell - Running as root User
Join us as we reveal how CVE-2024-45488 can let attackers gain access to your corporate password vault and uncover hidden secrets of Microsoft DPAPI.
Read ArticleSafeGuard for Privileged Passwords (SPP) virtual appliance images contain a hard-coded cryptographic key (CWE-321). An attacker can exploit this key …
Read Article