The Ivanti excitement continues! After an authentication bypass and command injection to kick off the year, Ivanti are following with a second authentication bypass and a privilege escalation. On January 22 Ivanti released this advisory describing the two new vulnerabilities in Ivanti Connect Secure, CVE-2024-21888 (privilege escalation) and CVE-2024-21893 (authentication bypass).
As this was another critical authentication bypass and the unpatched command injection vulnerability was well known at this point, our security research team immediately began work to ensure that customers of our Attack Surface Management platform were notified if they were affected.
In this blog post, we detail our reverse engineering process to find and exploit this new authentication bypass. For details on how we obtained a copy of Ivanti Connect Secure and the source code please see our previous post which covers obtaining the VM Image and extracting the filesystem.
Where to Start?
The advisory from Ivanti mentioned a "server-side request forgery vulnerability in the SAML component of Ivanti Connect Secure". This gave us a really good starting point because there are only a few SAML endpoints available. Like most of the authentication functionality, SAML exists under the <span class="code_single-line">/dana-na/auth/</span> path and is handled by several Perl CGI scripts. We started with <span class="code_single-line">/dana-na/auth/saml-consumer.cgi</span> and found out pretty quickly that it hands off the request to <span class="code_single-line">DSAuth::SAMLConsumer::process</span> without really doing any checks or validation.
From our work on the previous vulnerabilities, we knew the
DSAuth perl module was just a wrapper around some C and C++ libraries. We decompiled
DSAuth.so with Ghidra and found that it imported most of its functions from the following libraries.
We already had <span class="code_single-line">libdsplibs.so</span> decompiled and so quickly searched for <span class="code_single-line">SAMLConsumer</span> with Ghidra. We also could have used <span class="code_single-line">objdump</span> to print the symbol table, add the <span class="code_single-line">-C</span> to demangle the C++ names and grep for <span class="code_single-line">SAMLConsumer</span> to verify it was in there.
Unfortunately, all the <span class="code_single-line">DSAuth::SAMLConsumer::process</span> function did was put some variables in a table and then send them to a "saml sso par server".
We didn’t know what a “saml sso par server” was and at this stage decided it would be best to change strategies. We decided to configure our ICS VM to use SAML authentication, then we could capture some proper requests and see what the intended behaviour was.
In retrospect, we should have searched the filesystem for
saml. Because the SAML server is sitting right there, but we only realised this later.
Finding the SAML Server
Using Mock SAML and stumbling through multiple guides on how to configure ICS for SAML we were able to get it mostly working. We never got authentication to work, but we were able to get to a stage where ICS would send us to Mock SAML and Mock SAML would send us back with a <span class="code_single-line">SAMLResponse</span> that ICS would process and attempt to verify.
This gave us a SAML payload to modify and try different things with. While trying some XXE payloads we came across an error: <span class="code_single-line">Unknown issuer value in response</span>. We searched the filesystem for any matches to see where this error was coming from as it wasn't in anything we had seen so far.
We found it in <span class="code_single-line">/home/bin/saml-server</span> which sounded like the server <span class="code_single-line">DSAuth::SAMLConsumer::process</span> connected to. We decompiled <span class="code_single-line">saml-server</span> and searched for the error message. There weren’t many helpful function names, however most had some kind of logging that included the function name. Below we can see the error message and the function name <span class="code_single-line">validateSAMLResponse</span>.
Finding the SSRF
Now that we had the SAML server, we began working backwards from <span class="code_single-line">validateSAMLResponse</span> to see if we could get to the beginning of the SAML processing. From there we could look for anything that would indicate SSRF or XXE. While looking at the <span class="code_single-line">processPost</span> function we saw the following.
We had previously seen an SSRF in SAML processing when validating the XML payload against an XML Schema Definition. Although unlikely, we thought it was worth verifying. <span class="code_single-line">
xmltooling::ValidatorSuite::validate</span> was an imported from <span class="code_single-line">
libxmltooling.so.3</span>. We searched online and found that it as an open source package so we wouldn’t need to decompile it.
The version we found on the device was <span class="code_single-line">libxmltooling.so.3.0.2</span>and searching for “libxmltooling.so.3.0.2 cve” led us to a page of advisories, quite a few without CVEs. The most recent affected <span class="code_single-line">xmltooling < 3.2.4</span> and was rated low. However, the advisory sounded exactly like what we were looking for.
Including certain legal but “malicious in intent” content in the KeyInfo element defined by the XML Signature standard will result in attempts by the SP’s shibd process to dereference untrusted URLs.
We went to the XML Signature spec and began looking to see what we could put in a
KeyInfo element. The RetrievalMethod option seemed like the obvious choice. We could specify a URI that the application would retrieve the certificate information from.
Since we already had a full SAML payload with the <span class="code_single-line">KeyInfo</span> element in it, all we needed to do was replace <span class="code_single-line">X509Data</span> with <span class="code_single-line">RetrievalMethod</span>. We used a Burp Collaborator URL to create the following KeyInfo element.
We put this back in our SAML response, sent the request and were pleased to see a hit in Collaborator.
We also got an error message from ICS saying it failed to process the SAML payload.
Remote Code Execution
At this stage we were pretty confident we had the SSRF. Converting the SSRF to an authentication bypass and then RCE was comparatively much simpler.
We knew from the previous authentication bypass that the REST API was a python server behind a web proxy and all the authentication was done by the proxy. If we could use the SSRF to directly call the python server, we would be able to exploit the same command injection vulnerability as before.
The configuration file at <span class="code_single-line">/root/home/config/config_restserver.spec.cfg</span> showed the rest server was running on port 8090. So we modified our previous command injection payload which contained a python reverse shell. The previous payload exploited the path traversal as follows.
It was modified to remove the path traversal and target <span class="code_single-line">http://127.0.0.1:8090</span>.
The full SAML payload was as follows. Since the vulnerability was just in the signature parsing, we could remove many of the other SAML elements.
This was then encoded and sent to <span class="code_single-line">/dana-na/auth/saml-consumer.cgi</span>.
We then caught our reverse shell.
A More Reliable Endpoint
While verifying the exploit we came across a common problem where if SAML was not configured the application would always respond with <span class="code_single-line">Missing/Invalid sign-in URL</span>. However, since we knew that the vulnerability existed in the signature verification and XML parsing it was possible that the exploit didn’t need to be part of the login flow. Any flow that processed SAML could be vulnerable.
We had a look at the other SAML endpoints and found <span class="code_single-line">/dana-na/auth/saml-logout.cgi</span>. Looking at the code it also called <span class="code_single-line">DSAuth::SAMLConsumer::process</span> which was promising.
We had to change some of the parameters and compress the SAML payload with deflate before base64 encoding, but otherwise the exploit worked with no modification. This version was much more reliable and worked on targets that did not appear to be configured for SAML authentication. A simple one-liner to correctly compress and encode the payload is as follows.
This can then be sent with a GET request. The <span class="code_single-line">
SpId</span> parameter is added to bypass a small check, the value is unused.
Like the previous Ivanti vulnerabilities this too has the potential for a big impact. The vulnerability is present on a large number of devices and doesn’t appear to require any specific configuration.
The mitigations from the previous vulnerability don’t appear address the root cause of the command injection vulnerability. As such more mitigations must be applied to mitigate this new bypass.
Fortunately Ivanti has released a patch which should address all the vulnerabilities. Those running Ivanti are recommended to factory reset their devices and apply the patch. The full details are available here
As always, customers of our Attack Surface Management platform were the first to know when this vulnerability affected them. We continue to perform original security research in an effort to inform our customers about zero-day vulnerabilities in their attack surface.