A story of three CVE's in Ubuntu DesktopVaisha Bernard - October 27, 2020
I had some time off and decided to hunt for some bugs in my Ubuntu Desktop installation. My target was going to be the D-Bus interface.
D-Bus (Desktop-Bus) is an Inter-Process Communication (IPC) and remote procedure call (RPC) mechanism that allows communication between multiple processes running concurrently on the same machine. It is used in many Linux distributions nowadays.
On Linux desktop environments we have a single system bus, used for communication between user processes and system processes, and for each logon session we have a session bus, for communication between processes in a single desktop session.
My target was the system bus. System processes that open up an interface for communication from userland, that sounds like trouble!
My first goal was understanding the syntax and feeling of the D-Bus. There is an excellent interactive tool called D-Feet to make life easier.
There is a clear hierarchy in the D-Bus specification. Objects are processes that expose themselves on the D-Bus. An object can implement multiple interfaces and each interface can have multiple methods. D-Bus uses interfaces to provide a namespacing mechanism for methods. An interface also has Properties, which are typed variables that can often be read and sometimes also changed.
D-Feet showed which Interfaces are implemented and which methods are available. In the background it uses the Introspect method of the org.freedesktop.DBus.Introspectable interface that is implemented by many objects to do just that.
Apart from D-Feet, the most straightforward way to interact with the D-Bus is via the dbus-send shell command. For example the next command calls the ListNames method on the org.freedesktop.DBus Interface and generates the list that D-Feet shows on the left.
For me the most intuitive way to handle multiple interactive calls to D-Bus methods is the python-dbus module. It's easy to script any interaction with a D-Bus method.
Many methods are protected with PolicyKit, to make sure the calling user has the right privileges to perform the action. You might have seen these popups, which are the result of a D-Bus method call that is protected by PolicyKit.
I was most interested in vulnerabilities that anyone could trigger without authentication, so I focused on methods that were not protected with PolicyKit.
Aptdaemon Information Disclosure (CVE-2020-15703)
The first bug that I found involves aptdaemon. Once you introspect the org.apt.debian object with D-Feet, you will notice a new running process in your processlist.
So aptdaemon is written in python. I could dive into the code, but this time my laziness reached record levels and I just wanted to know what syscalls happened in the background, so I spawned an strace on the process.
I started playing around with a couple of the methods and entering garbage. One method in particular which sounded interesting was the InstallFile method. It requires two arguments, a filepath of the package to install and a Boolean force. If you call the method, aptdaemon creates a D-Bus object called a transaction, which exposes new methods such as Simulate() and Run(). It also has several properties that are writable. Somehow we can simulate installing a .deb package file. I wrote a simple python script to experiment with that.
Well this does absolutely nothing. The Run() method which would actually install the .deb file requires authorization. But while playing around I noticed the locale property, which could be set as follows.
This results in the following error message.
The _parse_localename method, upon other things mainly checks if there is a "." In the locale name. The following call succeeds.
But my eye caught something interesting here in the strace output.
I changed the value to "/tmp.BB", and voila.
This looks like I can have it read any .mo locale file here. I spent a couple of hours reversing the .mo format and can now tell you all about the structure of the .po format which it is generated from, but I could not get it to do anything interesting.
Then I realized I could make a symlink called /tmp/LC_MESSAGES/aptdaemon.mo and point it to any file on the filesystem. For example to "/root/.bashrc".
That results in another error.
But it discloses information that I'm not supposed to be able to know, the existence of any file on the filesystem, for example in /root, where an unprivileged user should not be able to look into. A very small bug, but a bug nonetheless.
PackageKit Information Disclosure (CVE-2020-16121)
I found a similar bug in PackageKit. After the whole ordeal with aptdaemon, this one popped up immediately.
The org.freedesktop.PackageKit Interface on the /org/freedesktop/PackageKit object has a method CreateTransaction(). This creates a Transaction object which, among others, has the InstallFiles(), GetFilesLocal() and GetDetailsLocal() methods. All methods have a list of filepaths as their argument.
Again, this allows us to determine the existence of any file on the filesystem, but this time if a file exists we also get an error message that discloses the MIME type.
A simple python script demonstrates this.
This results in the error message: MIME type text/plain not supported.
Blueman Local Privilege Escalation or Denial of Service (CVE-2020-15238)
This bug is a little more interesting. Playing around with the D-Bus methods of the org.blueman.Mechanism interface I noticed I was never asked for authorization. This was going to be an interesting target.
The developer of the package later confirmed that there was an issue with the Debian package: It only recommends policykit-1 but blueman does not support "runtime-optional" Polkit-1 support. You have to decide during the build and as libpolkit-agent-1-dev is not a build dependency Polkit-1 support is always disabled. Thumbs up for the developer by the way, he jumped onto this bug immediately and pushed out a fix in no time, while also coordinating a release date between the Ubuntu and Debian security teams.
The DhcpClient() method soon caught my attention. It requires a single string as argument. Let's check with strace what syscalls are sent in the background. I used this oneliner to bring up the daemon and attach an strace process to it without worrying too much about the short time it's alive or the PID.
Then I fired off my first test.
There is a lot of output from the strace process, but filtering on "execve" provided a very interesting observation.
Oh my! That's a lot of execution happening in the background. It seems my parameter is used as an argument to dhclient, avahi-autopid and ip. Let's see what we can do with that.
I dove into the dhclient manual to see if there are any interesting arguments I could use. The following stood out.
Indeed if I run the command "dhclient -sf /tmp/eye" as root, the dhclient starts running in the background, requests a new DHCP lease and finally runs the shell script "/tmp/eye". Let's see what happens if we try this with our blueman-mechanism method.
Well, that failed. Let's see why. From the strace output we notice the following.
The dhclient binary has a very specific way of parsing arguments and as we can see the "-sf /tmp/eye" argument is parsed as a single flag that does not exist. Some binaries would allow "-sf=/tmp/eye" or "-sf/tmp/eye", but the pickiness of dhclient saved the day here, otherwise this would be a very critical bug.
Now let's see if we can inject into the arguments of the ip command.
dhclient complains, but the execution continues.
Here we see the injection into the ip command. This time our argument is split into multiple arguments, so we can play around a little more with arguments to the ip command.
That's funny, this is actually valid syntax and the interface is kept up. Now, how to get around the up that's added?
Ah! This error has dhclient exit with a different return code and the execution flow does not reach the ip command. So we're limited to 15 characters here. But ip also accepts shorthand versions so al is an alias for alias.
This indeed brings the interface down and creates an alias up for ens33. That is a DoS vulnerability as any low privileged user can trigger this. Let's see if we can find other interesting arguments to the ip command.
From the ip-link manual.
So I can attach an XDP object to any interface. That would require a little more than 15 characters. How to bring that down? Let's rename the interface first!
Ok, so now we can attach an XDP object to any interface. I could dive deeper into how XDP and eBPF works and if there would be any security issues related to the fact I can now attach such an object to any interface, but that's a huge new project. If there are any experts out there, please let me know if you manage to get code execution with this method!
Finally, I discovered that blueman also supports other DHCP clients. From the blueman code:
So if dhclient is not available, but dhcpcd is, we have another possibility to get code execution. Luckily (for us), dhcpcd also has the ability to run a script and is a lot less picky in its argument format. This leaves us with a Local Privilege Escalation oneliner that works on any Ubuntu or Debian system that has dhcpcd instead of dhclient.
Any unprivileged user can run this and any code put in the shellscript /tmp/eye is run as root!
There are always bugs out there. Programs like hackerone and bugcrowd are amazing opportunities for companies to tighten their security by offering security researchers substantial amounts of money for reported vulnerabilities. But the open source community needs our help as well. So once in a while, give a shot at it for fun instead of profit.
Thanks to the Ubuntu security team and Christopher Schramm, the developer of Blueman, for their quick and friendly response and hard work to fix these issues.