McAfee Virus Scan for Linux

Vulnerability Writeup by Andrew Fasano
December 12, 2016

Overview

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.

Versions Affected

The vulnerabilities described here are present from at least v1.9.2 (released 2/19/2015) through version 2.0.2, (released 4/22/16). The only difference from the older release appears to be updating to a newer version of libc which makes exploiting these vulnerabilities easier.

Timeline

DateEvent
June 23, 2016Vulnerabilities reported to CERT/CC. Public disclosure scheduled for August 23
July 19, 2016McAfee asks for extension until September, or possibly December
September 2016No contact from McAfee
October 2016No contact from McAfee
November 2016No contact from McAfee
December 5, 2016McAfee informed of December 12th publication date
December 9, 2016McAfee publishes security bulletin and assigns CVE IDs
December 12, 2016This post published
December 12, 2016CERT/CC release Vulnerability Node VU#245327 describing these vulnerabilities

Intro

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.


System Architecture

Before getting into the details of the vulnerabilities in this product, it helps to have a quick understanding of the system architecture.

Services

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.

Interprocess Communication

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.

Vulnerabilities

These ten vulnerabilities are described in this section:

  1. CVE-2016-8016: Remote Unauthenticated File Existence Test
  2. CVE-2016-8017: Remote Unauthenticated File Read (with Constraints)
  3. CVE-2016-8018: No Cross-Site Request Forgery Tokens
  4. CVE-2016-8019: Cross Site Scripting
  5. CVE-2016-8020: Authenticated Remote Code Execution & Privilege Escalation
  6. CVE-2016-8021: Web Interface Allows Arbitrary File Write to Known Location
  7. CVE-2016-8022: Remote Use of Authentication Tokens
  8. CVE-2016-8023: Brute Force Authentication Tokens
  9. CVE-2016-8024: HTTP Response Splitting
  10. CVE-2016-8025: Authenticated SQL Injection

When chaned together, these vulnerabilities allow a remote attacker to execute code as root.


Vulnerability 1 (CVE-2016-8016): Remote Unauthenticated File Existence Test

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)


Vulnerability 2 (CVE-2016-8017): Remote Unauthenticated File Read (with Constraints)

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.


Vulnerability 3 (CVE-2016-8018): No Cross-Site Request Forgery Tokens

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.


Vulnerability 4 (CVE-2016-8019): Cross Site Scripting

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"


Vulnerability 5 (CVE-2016-8020): Authenticated Remote Code Execution & Privilege Escalation

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.


Vulnerability 6 (CVE-2016-8021): Web Interface Allows Arbitrary File Write to Known Location

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.


Vulnerability 7 (CVE-2016-8022): Remote Use of Authentication Tokens

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


Vulnerability 8 (CVE-2016-8023): Brute Force Authentication Tokens

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// 

Vulnerability 9 (CVE-2016-8024): HTTP Response Splitting

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.


Vulnerability 10 (CVE-2016-8025): Authenticated SQL Injection

This system uses a SQLite database to store data about settings and previous scans. Every entry point to this database I looked at was vulnerable to SQL injections. This application appears to translate URL arguments into SQLite commands. Using the CSV-export function mentioned in the discussion of vulnerability 9, we can dump the 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)
			)"



Bringing it all Together: Remote Code Execution as Root

To execute code as the root user on a remote machine:

  1. Brute force authentication token using Vulnerability 7 and Vulnerability 8.
  2. Start running malicious update server.
  3. Send request with authentication token to update update server using Vulnerability 7.
  4. Force target to create malicious script on their system using Vulnerability 6.
  5. Send malformed request with authentication token to start virus scan but execute malicious script instead by using Vulnerability 5 and Vulnerability 6.
  6. The malicious script is then run by the root user on the victim 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.

Demo