McAfee Virus Scan for Linux
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
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 |
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:
- CVE-2016-8016: Remote Unauthenticated File Existence Test
- CVE-2016-8017: Remote Unauthenticated File Read (with Constraints)
- CVE-2016-8018: No Cross-Site Request Forgery Tokens
- CVE-2016-8019: Cross Site Scripting
- CVE-2016-8020: Authenticated Remote Code Execution & Privilege Escalation
- CVE-2016-8021: Web Interface Allows Arbitrary File Write to Known Location
- CVE-2016-8022: Remote Use of Authentication Tokens
- CVE-2016-8023: Brute Force Authentication Tokens
- CVE-2016-8024: HTTP Response Splitting
- CVE-2016-8025: Authenticated SQL Injection
When chained 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 have 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 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 through /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 a McAfee ePolicy Orchestrator file which is mostly binary data. I made the choice 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 seem 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 timestamp 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 URL encoding 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:
- Brute force authentication token using Vulnerability 7 and Vulnerability 8.
- Start running a malicious update server.
- Send request with authentication token to update the update server using Vulnerability 7.
- Force target to create a malicious script on their system using Vulnerability 6.
- Send malformed request with authentication token to start virus scan but execute malicious script instead by using Vulnerability 5 and Vulnerability 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.