Polkit is a system service installed by default on most Linux distributions. It's used when determining if a user has access to something, requires a password or something else. Besides graphical sections, pkexec can be used as an alternate to sudo.
The dbus-send command can also be used to trigger polkit. This tool can manually be used to simulate D-Bus messages that the graphical interface would use to send.
The architecture uses a daemon to communicate with the privilege side of things. This daemon takes requests from the Authentication Agent and dbus-send, which properly forward them to accounts-daemon and polkit. The dbus-daemon enables all four processes to communicate securely, making it a key part to the puzzle.
When creating a new user, the flow is shown below:
- dbus-send sends a request to the
account-daemon. This is funneled through the dbus-daemon first. This includes a special bus id from the sender.
- The accounts daemon asks
polkit if the connection id is authorized for the create user action.
polkit asks dbus-daemon for the UID of the connection. The daemon sends back a list of admins who can perform the action or a 0 if the user is root.
- The agent asks for a username/password to see if the action is allowed. This is sent from the authentication agent to
polkit.
- If
polkit approves this, the action is performed.
With the flow above, there is a bug when killing the debus-send process. In step 3 (from above), polkit asks the debus-daemon about a particular UID. But, if the process is deleted, then UID does not exist. Although this should return an error message, this gets handled in the worst way possible!
In step 3, if the UID is 0 (signalling root), then no other checks for authentication are performed. When the UID does not exist, 0 is also returned! This means that the killed process is the same as root!
Besides racing the kill the process before the lookup, there are also multiple checks on the UID. So, the proper code path needs to be hit, which takes a few tries when racing it.
The vulnerable code path sets an error flag and returns a TRUE/FALSE value. The bug is that TRUE is returned instead of FALSE, but sets the error flag. The developers
fixed the code by returning FALSE instead of TRUE.
In order to a Linux distro to be vulnerable, the error flag needs to be ignored. This was the case in Ubuntu, Debian and Red Hat Linux.
The bug was not some crazy memory corruption bug. Instead, it was a logic bug that had existed within the code for 7 years. This is a fascinating example that security bugs will not never go away, even when memory corruption bugs are long gone.