Vulnerability Writeup by Andrew Fasano
December 12, 2016
A system running Intel's McAfee VirusScan Enterprise for Linux can be compromised by remote attackers due to a number of security vulnerabilities. Some of these vulnerabilities can be chained together to allow remote code execution as root.
Date | Event |
---|---|
June 23, 2016 | Vulnerabilities reported to CERT/CC. Public disclosure scheduled for August 23 |
July 19, 2016 | McAfee asks for extension until September, or possibly December |
September 2016 | No contact from McAfee |
October 2016 | No contact from McAfee |
November 2016 | No contact from McAfee |
December 5, 2016 | McAfee informed of December 12th publication date |
December 9, 2016 | McAfee publishes security bulletin and assigns CVE IDs |
December 12, 2016 | This post published |
December 12, 2016 | CERT/CC release Vulnerability Node VU#245327 describing these vulnerabilities |
At a first glance, Intel's McAfee VirusScan Enterprise for Linux has all the best characteristics that vulnerability researchers love: it runs as root, it claims to make your machine more secure, it's not particularly popular, and it looks like it hasn't been updated in a long time. When I noticed all these, I decided to take a look.
Before getting into the details of the vulnerabilities in this product, it helps to have a quick understanding of the system architecture.
This product contains two separate services; one running as root and one running as an unprivileged user called nails. The main scanner service runs as root and listens on a local Unix socket at /var/opt/NAI/LinuxShield/dev/nails_monitor
. The webserver runs as the nails user and listens on 0.0.0.0:55443
.
The webserver is essentially a UI on top of the scanner service. When a user makes a request to the webserver, the request is reformatted, sent to the root service and then the user is shown the response rendered in an html template. The web interface doesn't do much to limit what data a malicious user can send to the root service.
These ten vulnerabilities are described in this section:
When chaned together, these vulnerabilities allow a remote attacker to execute code as root.
When browsing to many sections of the web interface, an html file path is specified in the tplt parameter, in the figure shown above tplt is set to tasks.html. Running strace on the webserver shows that this causes it to open /opt/NAI/LinuxShield/apache/htdocs/0409/tasks.html
as shown here
If the tplt parameter is set to a different page, such as ..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd (the string ../../../../../../etc/passwrd urlencoded), the response is a badly formatted page with an error variable set to 14. The JavaScript function lookupErrorCode
maps error 14 to the string "Badly Formed Web Template." If the tplt variable is set to a relative path that doesn't exist, the error variable is set to to 10 which corresponds to the string "cannot open file ".
The two different error messages can reveal to an unauthorized remote user if files by a given name exist on the system
This leads to the question of what is different between the valid web templates (such as tasks.html) and invalid template files (such as /etc/passwd)
Looking at various template files used by the webserver, it's easy to see that valid template files either contain the magic string __REPLACE_THIS__
or has the two tags [%
and %]
with any string between them.
If an attacker is able to place these strings into a file on the system (which may be trivial for log files), the attacker could then use the webserver to remotely read the entire file. A limitation of this vulnerability is that the files are being read by the nails user.
There are no CSRF-tokens accompanying any forms on the web interface which allows attackers to submit authenticated requests when an authenticated user browsers to an attacker-controlled, external domain. Seeing this basic of a vulnerability in an AntiVirus product in 2016 is quite surprising. The lack of CSRF-tokens is one of the ways that a remote attacker can exploit a vulnerability that should only be exposed to authenticated users.
When tplt is set to NailsConfig.html or MonitorHost.html, parameters info:7 and info:5 both place untrusted user input in a string being passed to the JavaScript function formatData
. A typical value for info:7 is a list of strings such as single,show,serverUtcOffset=-25200
. This is then placed into a single-quoted string passed to formatData
. If the info:7 parameter is set to something like single'.prototype.constructor=eval('alert("xss"'))+'
the eval function will evaluate arbitrary malicious JavaScript before formatData is called.
This payload can then be modified to alert the message "xss"
Starting a scan of the system requires filling out 4 pages worth of forms
When the final page of the form is submitted, a large request is sent to the server. A subset of the parameters posted are shown here:
The nailsd.profile.ODS_9.scannerPath variable contains the path that the system will execute to run the scan. Modifying the value to /bin/sh
generates the following error in the web interface
Attaching strace shows that this parameter is passed directly to execve from a process running as root.
By changing this variable to an executable on the system, an authenticated user can have that binary executed by the root user. It would be easy to combine it with other XSS or CSRF vulnerabilities to exploit this without authentication.
This can't easily be extended into arbitrary code execution because there are multiple arguments are passed to the binary. However, the scannerPath variable is not the only variable passed directly from the webserver to execve; while some values are hard-coded, four are entirely attacker-controlled leading to the following command:
[scannerPath] –e [enginePath] –D [datPath] –L [engineLibDir] –p 21 –i 2 –I 0
One vector to exploit this is though /bin/sh
which will load a shell script specified by the -e
argument and execute it. A local user could use this to escalate privileges, but a remote attacker would need a way to place a malicious shell script onto the system.
The web interface allows users to specify an update server and request updates from it. Since I wanted to find a way for a remote user to write a file to the system this seemed like it might be a useful feature.
To find how the update server was used, I cloned McAfee's update repository locally and then reconfigure the server to download updates from my server.
Two requests are made as part of the update process. First there was a request to /SiteStat.xml
, then a request to /catalog.z
. The SiteStat file is just a standard XML file that says if a site is enabled and what version of the catalog it is serving. Presumably an update will only be downloaded if this is newer than whatever version the application had last used to update. The catalog.z
file looks like an McAfee ePolicy Orchestrator file which is mostly binary data. I made the choice to to assume that this used good crypto and that the update was signed so there would be no way to push down a malicious update to compromise a system. Instead, I wanted to use this to push down a shell script to later execute with the previous vulnerability.
The log files claim that the update process consists of: downloading a file, verifying its integrity, unzipping, and installing.
Since this application isn't single-threaded, we can exploit this logic by making the program download a large file to /opt/McAfee/cma/scratch/update/catalog.z
, and either before the download finishes or before the verification runs, we can use Vulnerability 5 to execute it.
It's trivial to generate a shell script that will take a while to download, but will execute a given payload when run before the download is finished. This can be done by creating a script that contains a desired payload and then appending the payload with a large comment.
Combining vulnerabilities 5 and 6 now gives us a privilege escalation allowing us to go from the nails user to root. Using CSRF or XSS, it would be possible to use these vulnerabilities to remotely privesc to root.
In an attempt to develop an XSS and CSRF exploit, I threw together a simple cookie stealer and took a cookie from an authenticated user. But when I tried to use the cookie from my "attacker" machine, my authentication was denied:
After confirming that the token worked on the original machine, I thought that the authentication tokens might be limited to a specific IP address. This would make writing an exploit more difficult, but it could still all be done via XSS using JavaScript in a victim's browser.
When a user authenticates through the website, a message is passed via a unix-socket to the root service. The root service validates the credentials and returns its results to the webserver. To find what was going wrong when a remote machine used my cookie, I used socat
to man-in-the-middle the socket to see the messages.
This script restarts nails and intercepts all of its communications on the socket
Now we could see what was different between the two requests
Valid request
< 2015/07/30 11:14:28.119036 length=70 from=0 to=69 +OK welcome to the NAILS Monitor Service <19224.2214.1438280068.161>\r > 2015/07/30 11:14:28.119326 length=54 from=0 to=53 auth 2259618965-19224.2214.1438280068.161-2259618965\r < 2015/07/30 11:14:28.119399 length=31 from=70 to=100 +OK successful authentication\r > 2015/07/30 11:14:28.137344 length=66 from=54 to=119 cred 127.0.0.1/nails/1438280067/1438279968-checksum//0 127.0.0.1\r < 2015/07/30 11:14:28.137530 length=20 from=101 to=120 +OK credentials OK\r
Invalid request
< 2015/07/30 11:14:28.119036 length=70 from=0 to=69 +OK welcome to the NAILS Monitor Service <19224.2214.1438280068.161>\r > 2015/07/30 11:14:28.119326 length=54 from=0 to=53 auth 2259618965-19224.2214.1438280068.161-2259618965\r < 2015/07/30 11:14:28.119399 length=31 from=70 to=100 +OK successful authentication\r > 2015/07/30 11:14:28.137344 length=66 from=54 to=119 cred 127.0.0.1/nails/1438280067/1438279968-checksum//0 [ATTACKER IP]\r < 2015/07/30 11:14:28.137530 length=20 from=101 to=120 +ERR bad credentials\r
It looks like the webserver is sending the requester's IP address in addition to their cookie when it makes an AUTH request. Although it's a bit unusual, it's not a terrible security decision.
Our cookie is being sent via a text-based protocol and after our cookie, there's some number of spaces and the IP address. But if we modify this to make our cookie end with a space followed by the victim's IP address and then a number of spaces, it will be parsed incorrectly.
Instead of having the message sent on the socket be
AUTH [cookie] [ATTACKER IP]
We'll modify our cookie so the message sent is
AUTH [stolen cookie + VICTIM IP ] [ATTACKER IP]
The service incorrectly parses this line and believes that it's reading a cookie sent from the victim's IP address.
The full communication ends up looking like
< 2015/07/30 11:14:28.119036 length=70 from=0 to=69 +OK welcome to the NAILS Monitor Service <19224.2214.1438280068.161>\r > 2015/07/30 11:14:28.119326 length=54 from=0 to=53 auth 2259618965-19224.2214.1438280068.161-2259618965\r < 2015/07/30 11:14:28.119399 length=31 from=70 to=100 +OK successful authentication\r > 2015/07/30 11:14:28.137344 length=66 from=54 to=119 cred 127.0.0.1/nails/1438280067/1438279968-checksum//0 127.0.0.1 10.0.0.130\r < 2015/07/30 11:14:28.137530 length=20 from=101 to=120 +OK credentials OK\r
After seeing the previous cookie-parsing logic fail, I wanted to test how well the other cookie validation logic worked.
Here are a few sample values for the nailsSessionId
cookies that were generated by logging in and out for the nails account
127.0.0.1/nails/1459548338/1459548277-checksum//0
127.0.0.1/nails/1459549661/1459549629-checksum//0
127.0.0.1/nails/1459549695/1459549629-checksum//0
Only two parts of the cookie seems to change between typical login attempts. The cookie format seems to be
[host]/[username]/[SECRET1]/[SECRET2]-checksum//[Zero]
Where typical values are as follow:
Variable | Description | Observed Values |
---|---|---|
[host] | An IP Address | 0.0.0.0 or 127.0.0.1 |
[username] | The username of the logged in user. | nails |
[SECRET1] | Unix time at which the cookie was assigned | 1435067777 |
[SECRET2] | Unix time at which the server was started | 1435066996 |
[Zero] | The number 0 | 0 |
While using a time stamp for a secret value is a bad idea since it could be brute forced, using two in conjunction would normally make this difficult. Fortunately, that's not the case here. Some basic testing found that the acceptable values for these fields differed significantly from what they were typically set to:
Variable | Acceptable Value |
---|---|
[host] | IP address request is sent from |
[username] | Any string |
[SECRET1] | Any number |
[SECRET2] | Unix time at which the server was started |
[Zero] | Blank |
This leaves us with one value to brute force; the time at which the server was started at. Starting at the current date and decrementing it until we've successfully authenticated can be done by modifying the DATE value in the following cookie:
[Attacker IP]/n/0/[DATE]-checksum//
Users can export a CSV of all log data from the "System Events" page by clicking an export button which just makes a GET request.
When this request is sent, one of the parameters is info%3A0
. This parameter typically holds the value multi%2Capplication%2Fvnd.ms-excel
. The server responds to this request with a header Content-Type: application/vnd.ms-excel
. An attacker can create a link that responds with arbitrary headers by simply urlencoding newlines plus additional headers.
sqlite_master
table in CSV form simply by visiting a URL. The query select * from sqlite_master;
is embedded in the URL localhost:55443/0409/nails?pg=proxy&tplt=-&addr=127.0.0.1%3A65443&mon%3A0=db+select+_show%3D%24*++_output%3Dcsv+_table%3Dsqlite_master+&info%3A0=multi%2Capplication%2Fvnd.ms-excel.
The database isn't used for authentication, just to track which files have been scanned and the event log. After exploiting other vulnerabilities to compromise a machine, an attacker could use SQL injections to modify the event log to clean up their tracks.
The schema of this database is:
"*" "31-Dec-1969 16:00:00 (-08:00 UTC)","nailsInfo","nailsInfo","4","create table nailsInfo(attrib varchar(32) not null unique, -- name of the attribute val varchar(32), -- string value i_val integer -- integer value )" "31-Dec-1969 16:00:00 (-08:00 UTC)","(nailsInfo autoindex 1)","nailsInfo","3","" "31-Dec-1969 16:00:00 (-08:00 UTC)","counters","counters","5","create table counters(i_lastUpdated integer not null, -- time the counters were last updated i_scanned integer not null, -- Number of objects scanned i_totalScanCpu integer not null, -- Total CPU used for scanning (microseconds) i_excludes integer not null, -- Number of excluded files i_ok integer not null, -- Number of files scanned to be ok i_infected integer not null, -- Number of objects that have been infected i_infections integer not null, -- Number of of infections i_cleaned integer not null, -- Number of objects that have been cleaned i_cleanAttempts integer not null, -- Number of objects that have been queued for cleaning i_cleanRequests integer not null, -- Number of clean requests from the scan sources i_repaired integer not null, -- Number of repairs made i_possiblyCleaned integer not null, -- Number of partial repairs made i_errors integer not null, -- Number of failed scans not clean and not infected i_timeouts integer not null, -- Number of scans that have timed out i_denied integer not null, -- Number of process denied access i_deleted integer not null, -- Number of cleans that resulted in deleting the file i_renamed integer not null, -- Number of cleans that resulted on renaming the file i_quarantined integer not null, -- Number of cleans that resulted on quarantining the file i_corrupted integer not null, -- Number of corrupted items detected by scanning i_encrypted integer not null, -- Number of encrypted items detected by scanning i_uptime integer not null, -- Number of seconds since we started i_wait integer not null, -- Number of objects waiting to be scanned i_busy integer not null, -- Number of objects being scanned i_adds integer not null, -- Number of objects that have been added to a queued entry i_cacheSize integer not null, -- Number of entries in the cache i_cacheHits integer not null, -- Number of cache hits i_cacheMisses integer not null, -- Number of cache misses i_cacheInserts integer not null -- Number of cache insertions )" "31-Dec-1969 16:00:00 (-08:00 UTC)","schedule","schedule","9","create table schedule(i_taskId integer primary key, -- an auto-increment column taskName varchar(64) not null unique, -- the name of the task timetable varchar(255) not null, -- the encoded string of when it runs taskType varchar(16) not null, -- upgrade, scan, report taskInfo varchar(255), -- information specific to the task taskResults varchar(255), -- results of the task i_lastRun integer, -- time last run status varchar(8), -- status of last run progress varchar(255), -- progress string i_duration integer, -- current duration of the task run i_nextRun integer, -- time next run i_recurrenceCounter integer, -- count scheduler invocations by cron i_taskPid integer -- pid of the task being run )" "31-Dec-1969 16:00:00 (-08:00 UTC)","(schedule autoindex 1)","schedule","8","" "31-Dec-1969 16:00:00 (-08:00 UTC)","errorClass","errorClass","12","create table errorClass(errorClsNm varchar(16) not null unique)" "31-Dec-1969 16:00:00 (-08:00 UTC)","(errorClass autoindex 1)","errorClass","11","" "31-Dec-1969 16:00:00 (-08:00 UTC)","repository","repository","15","create table repository(siteList blob, status int)" "31-Dec-1969 16:00:00 (-08:00 UTC)","scanLog","scanLog","16","create table scanLog(i_logId integer primary key, -- an auto-increment column origin varchar(8) not null, -- access or demand i_taskId integer, -- references schedule.i_taskId i_objId integer, -- an id to relate scan events on the same object i_tim integer not null, -- UTC time it happened fileName varchar(255), path varchar(255), action varchar(16), virusName varchar(64), virusType varchar(16), -- Unknown, Virus, App, Joke, Killed, Test, Trojan, Wannabee userName varchar(32), processName varchar(32) )" "31-Dec-1969 16:00:00 (-08:00 UTC)","eventLog","eventLog","18","create table eventLog(i_logId integer primary key, -- an auto-increment column origin varchar(8) not null, -- system or task i_taskId varchar(64), -- references schedule.i_taskId i_objId integer, -- an id to relate events on the same object i_tim integer not null, -- UTC time it happened errorClsNm varchar(16), -- references errorClass.errorClsNm i_errorCode integer, -- the error code errorType varchar(8), -- info or error description varchar(255) )"
To execute code as the root user on a remote machine:
Exploiting this vulnerability depends on the existence of a valid login token which is generated whenever a user logs into the web interface. These tokens are valid for approximately an hour after login.