Remote Code Execution in Citrix ADC

Author

Web Application Security Expert

Many of you have probably heard of the CVE-2019-19781 vulnerability that I discovered at the end of last year. It is a critical vulnerability in Citrix ADC that allows unauthorized users to execute arbitrary operating system commands.

It caused quite a stir when Citrix released its guidelines for addressing the vulnerability since approximately 80,000 companies from around the globe were threatened by the problem. Another reason why the vulnerability attracted so much attention because Citrix ADC is installed on the border between external and internal organization networks. Thus, when a hacker exploits the CVE-2019-19781 vulnerability, he or she simultaneously gains access to the targeted company’s internal network and is able to develop attacks on the private segment of the network.

Positive Technologies analysis of CVE-2019-19781 of December 23, 2019

How I found the CVE-2019-19781

While conducting a black-box analysis on Citrix ADC, I discovered that an unauthorized user could use path traversal to access static files, which should require authorization (/vpn/../vpns/style.css):

This behavior caught my attention, and I decided to proceed the research on a live Citrix ADC appliance.

First, I analyzed the Apache web server configuration file (/etc/httpd.conf), which is responsible for the application’s web interface:

Alias /vpns/portal/scripts/ /netscaler/portal/scripts/
<LocationMatch "/vpns/portal/scripts/.*\.pl$">
  SetHandler perl-script
  PerlResponseHandler ModPerl::Registry
  Options +ExecCGI
  PerlSendHeader On
  allow from all
</LocationMatch>

As illustrated above, the paths that fall under the pattern /vpns/portal/scripts/.*\.pl$ are processed by the function ModPerl::Registry . As it turned out, Perl scripts could be run from the folder /netscaler/portal/scripts/ without authorization.

Next, I began to analyze the scripts that could be called at the URL /vpn/../vpns/portal/scripts/[scriptName].pl:

Almost every script calls the csd function from the NetScaler::Portal::UserPrefs module (/netscaler/portal/modules/NetScaler/Portal/UserPrefs.pm). This csd function works with the HTTP headers NSC_USER and NSC_NONCE. The second header, for our current purposes, is irrelevant. However, the first header, NSC_USER, is taken to be used as a file name. If a file with the name taken from NSC_USER does not exist, then it is created with a defined structure. If it does exist, then the $doc variable is assigned based on the parsed file.

sub csd {
	my $self = shift;
	my $skip_read = shift || "";
    my $cgi = new CGI;
    my $username = Encode::decode('utf8', $ENV{'HTTP_NSC_USER'}) || errorpage("Missing NSC_USER header.");

    $self->{username} = $username;  

    my %session;
    my $nsc_nonce = Encode::decode('utf8', $ENV{'HTTP_NSC_NONCE'}) || errorpage("Missing NSC_NONCE header.");
    my $id = unpack ('H*',$nsc_nonce);
...
    $self->{session} = %session;
	$self->{filename} = NetScaler::Portal::Config::c->{bookmark_dir} . Encode::encode('utf8', $username) . '.xml';
	if($skip_read eq 1) {
		return;
	}

    my $doc = $session{doc} || "";
    if ($doc eq "" || $doc->{timestamp} < $self->timestamp()){
      $doc = $self->fileread($username);
  	  $session{doc}= $doc;
    }
    if ($doc->{style}->{theme} ne "custom" && ! -e NetScaler::Portal::Config::c->{theme_dir} . $doc->{style}->{theme}){
      delete $doc->{'style'};
      $self->filewrite($doc);
    }
    
    $self->{doc} = $doc;
    return $self->{doc};
}

sub filewrite {
	my $self = shift;
	my $doc = shift;
	$doc = $self->{doc};
	my $parser = XML::Simple->new();
	$datafile = NetScaler::Portal::Config::c->{bookmark_dir} . Encode::encode('utf8', $self->{username}) . ".xml";
...
	open(FILEWRITE, "> $datafile") || die "can't open $datafile $!";
...
	print FILEWRITE $parser->XMLout($doc,  xmldecl => '<?xml version="1.0" encoding="UTF-8"?>', RootName => 'user', valueattr => [ 'username', 'theme' ]);
...
}

If path traversal is used in the filename, then we can create a file with the .xml extension in any system directory where we have writing permissions. To confirm this, we’ll input the string ../../../../tmp/myTestFile as the NSC_USER header value and check whether a file by that name is in the /tmp/ directory:

At this stage, we are able to create a file with an .xml extension, but are unable to control the file’s contents.

Let us examine the newbm.pl script, which is also in the directory of interest. This script accepts POST parameters as input and writes the values of certain parameters, such as url, title, and desc to the file (with the filename from the NSC_USER header).

...
my $user = NetScaler::Portal::UserPrefs->new();
my $doc = $user->csd();
...
my $newurl = Encode::decode('utf8', $cgi->param('url'));
my $newtitle = Encode::decode('utf8', $cgi->param('title'));
my $newdesc = Encode::decode('utf8', $cgi->param('desc'));
my $UI_inuse = Encode::decode('utf8', $cgi->param('UI_inuse'));
...
my $newBM = {   url => $newurl,
    title => $newtitle,
    descr => $newdesc,
    UI_inuse => $UI_inuse,
};
...
if ($newBM->{url} =~ /^\\/){
  push @{$doc->{filesystems}->{filesystem}}, $newBM;
} else { # bookmark
  push @{$doc->{bookmarks}->{bookmark}}, $newBM;
}
undef(@{$doc->{escbk}->{bookmark}});
undef(@{$doc->{escbk}->{filesystem}});
$user->filewrite($doc);

Now we are able to not only create XML files in arbitrary locations, but can also partially control their contents.

Continuing towards the RCE, let us examine the web server configuration again. Note that one other path (/vpns/portal/) is processed by the Perl function NetScaler::Portal::Handler (/netscaler/portal/modules/NetScaler/Portal/Handler.pm):

<Location /vpns/portal/>
  SetHandler perl-script
  PerlResponseHandler NetScaler::Portal::Handler
  PerlSendHeader On
</Location>

The handler function takes the segment of the path name following the last “/” symbol and uses it as a filename to search the /netscaler/portal/templates/ folder in an attempt to render the file using the Template Toolkit library:

sub handler {
  my $r = shift;
...
  my $doc = $user->csd();
  
  my $tmplfile = $r->path_info();
  $tmplfile =~ s[^/][];
  my $template = Template->new({INCLUDE_PATH =>  NetScaler::Portal::Config::c->{template_dir},CACHE_SIZE => 64, COMPILE_DIR=> NetScaler::Portal::Config::c->{template_compile_dir}, COMPILE_EXT => '.ttc2'});
...
  $template->process($tmplfile, $doc) || do {
    my $error = $template->error();
    my $lcError = lc($error);
    if ( $error->type() eq "file" && $lcError  =~ /^file error/ && $lcError =~ /.not found$/ ) {
      return NOT_FOUND;
    }
    print NetScaler::Portal::UserPrefs::html_escape_string($error), "\n";
  };

  return OK;
}

Thus, if we are able to load our file into the folder containing templates, we are also able to call its render.

The rest of the exploitation is complicated by the fact that the Template Toolkit library does not allow Perl code to be run with standard methods. For instance, the [% PERL %] directive cannot be used:

Considering these restrictions, I decided to search for vulnerabilities in standard library plugins. Let’s direct our attention to the Datafile plugin (/usr/local/lib/perl5/site_perl/5.14.2/mach/Template/Plugin/Datafile.pm). This file is quite small, so the calling the standard open function with two arguments immediately grabs our attention. This is unsafe and can lead to an RCE:

sub new {
    my ($class, $context, $filename, $params) = @_;
    my ($delim, $line, @fields, @data, @results);
    my $self = [ ];
    local *FD;
    local $/ = "\n";
...
    open(FD, $filename)
...
}

Let’s try to exploit the vulnerability locally and, as a test, create a file that we’ll name testRCE in the /tmp/ folder:

Now we are able to create files in arbitrary locations in the system and partially control their contents and the vulnerability in the Template Toolkit library. We can use all of this to execute arbitrary commands from unauthorized users.

Exploitation

Let’s create a file with a payload that will let us exploit the vulnerability in the Template Toolkit library later on:

Next we will render the file:

Now we can execute the script (web shell), which we created earlier, and execute an arbitrary OS command:

This Citrix vulnerability is the first vulnerability I have found that garnered significant attention online. It stirred a wave of discussion and has even been referred to as one of the most critical vulnerabilities found in recent times. In the near future, I intend to share information about several other critical vulnerabilities that affect popular products. As soon as patches are released and downloaded by a majority of users, articles about these vulnerabilities will be published. Thank you for reading, update your software and stay safe!

Timeline

  • 5 December, 2019 — Reported to Citrix
  • 19 December, 2019 — Mitigation steps released from Citrix
  • 24 January , 2020 — Latest patch released from Citrix

The advisory from Citrix: https://support.citrix.com/article/CTX267027