Cybersecurity
June 27, 2022

One-Day at a Time: From security advisory to functional exploit

a woman writing on a white boardlong exposure image of man walking by blue panels

Understanding the Vulnerability in Networkd-Dispatcher

The vulnerability is found within networkd-dispatcher, a dispatcher daemon for systemd.

  • This daemon operates across D-Bus, an inter-process communication (IPC) mechanism used in many Unix operating systems.
  • By sending a specific signal across the bus, we can hit the vulnerable PropertiesChanged code path within networkd-dispatcher.
  • To fully exploit the issue, we need to chain together multiple vulnerabilities—including a race condition, a symlink race, and a path traversal.

Understanding Your Target

As mentioned above, taking the time to fully understand the exploit is crucial to avoid wasted effort later. This is especially important when developing quick-response exploits for lesser-used software, where even minor quirks in the mechanism can prevent successful exploitation.

At Immersive Labs, the biggest challenge we faced was understanding how the D-Bus mechanism works well enough to effectively exploit the vulnerability. IPC can be a complex topic, so thorough research is vital.

Luckily, most applications of D-Bus are open-source, meaning there’s extensive documentation available on how the mechanism operates and how it can be interacted with.

The version of D-Bus used on Ubuntu and many other Linux-based operating systems is called dbus, developed by the freedesktop.org project. This implementation includes:

  • dbus-daemon, the main daemon
  • libdbus, the reference implementation

By studying the documentation alongside other sources, we built a general understanding of how D-Bus operates.

How D-Bus Works

D-Bus improves IPC (Inter-Process Communication) by allowing processes to communicate over a shared bus, rather than directly connecting with each other. This standardized approach makes it easier to share information across different processes, while language-specific bindings abstract the lower-level functionality.

Most D-Bus implementations use two types of buses:

  1. System Bus – A global bus used by system services and daemons.
  2. Session Bus – A user-specific bus, mostly used by individual applications.

For networkd-dispatcher, the system bus is used. This can be seen in Fig. 1, where the Python API is used to instantiate a reference to the bus (line 252).

Unique Identifiers in D-Bus

Each individual connection to the bus has a unique randomized identifier called a bus name, such as :1.1021. A process can also request a well-known name, which is more human-readable, like org.freedesktop.myProcess.

These names allow processes to communicate across specific channels without needing to identify the randomized bus name first.

  • The randomized bus name functions similarly to an IP address—a unique identifier used to distinguish different processes.
  • The well-known name is comparable to a domain name, providing a human-friendly wrapper around the identifier.

In this case, networkd-dispatcher operates over the well-known bus org.freedesktop.network1, as seen in Fig. 1 on line 254.

How Communication Happens Over D-Bus

Processes communicate over D-Bus in two main ways:

  1. Invoking object methods
  2. Broadcasting signals

Because this vulnerability only involves signal handling, we can ignore object methods for now.

Signals in D-Bus follow a publish/subscribe pattern, meaning:

  • One process broadcasts a signal (the publisher).
  • Another process listens for specific signals (the subscriber).

Each signal contains:

  • The object path – Used by dbus to refer to object instances.
  • The signal name – Specifies which signal receivers should handle it. In this case, the name is PropertiesChanged (Fig. 1, line 255).
  • The signal interface – A named group of methods and signals.
  • Parameters – Any additional data passed along with the signal.

Each of these attributes will play a crucial role later when we look at how to interact with the bus to exploit the vulnerability.

Building an Exploit Environment for Networkd-Dispatcher Vulnerability

Fig. 1: The Function Used to Register a Signal Handler on the System Bus

Crafting an Environment

One way to reduce debugging time is by setting up a suitable development environment for our exploit. The specific setup will vary depending on the exploit being developed, but in our case, there are two key aspects we need to monitor:

  • The data being sent across the bus – This helps us spot any unexpected values.
  • The execution flow within the networkd-dispatcher script – Allowing us to track which code paths we’re hitting and insert debug statements where necessary.

Thankfully, there are ways to observe both of these.

Some quick searching on debugging D-Bus will lead us to the utility dbus-monitor, which allows us to inspect data traveling across a specified bus. We can also apply filters to refine the results. For example, limiting ourselves to signals sent over the org.freedesktop.network1 bus helps cut down unnecessary noise from unrelated processes.

Monitoring networkd-dispatcher itself is simpler. Since it’s a Python script, we can insert debug statements into key functions along our target code path. Running the script manually in a separate terminal allows us to view relevant debug logs as needed.

With our exploit development environment set up, we’re ready to dig into some of the technical bits and see what we can achieve.

If at First You Don’t Succeed, Try, Try Again

Developing exploits is an iterative process. It’s highly unlikely that the first script you write (or even the first step of the script) will work perfectly. More often than not, a working exploit requires dozens of small tweaks, tests, and modifications.

A good strategy is to break the exploitation process into smaller stages, ensuring that each stage functions correctly before moving on. In this case, we know that one of the most critical steps is getting the vulnerable function in networkd-dispatcher to receive one of our crafted signals.

Choosing the Right Tool to Send a Signal

With our exploit environment ready, the next key question is:

💡 How are we going to send a signal to trigger the vulnerability?

Surprisingly, this took our team at Immersive Labs quite some time to figure out. Initial attempts included:

  • Using the C bindings provided by GLib to connect to the bus and broadcast a signal.
  • Trying command-line utilities such as dbus-send and gdbus.
  • Lighting a fire on the desk and sending smoke signals through a USB port.

While some of these attempts were not wholly unsuccessful (e.g., C bindings allowed for successful bus connection), they all had drawbacks. Some were too complex for our purposes, while others were not configurable enough.

Finding the right balance between simplicity and configurability is key, and depends largely on the exploit at hand. For lower-level protocols, you may need to write your own connectors or client software to send data in exactly the right format.

A Simple Solution: Python D-Bus Bindings

Taking a step back and re-reading the advisory, a keen eye will notice that the exploit script in the latter half is written in Python. A little more research into D-Bus bindings for Python leads to an elegant solution—a minimalistic script that demonstrates all the code needed to connect to a bus and emit a signal.

Using this as a boilerplate, we can tweak the code to send a PropertiesChanged signal over the org.freedesktop.network1 bus. By modifying the signal name, bus name, and inserting the necessary parameters, we arrive at the following code:

Fig. 2: An initial PoC for sending a signal to networkd-dispatcher.Running this as the systemd-network user (as specified in the advisory) gives us the following result on our debug instance of networkd-dispatcher:

Fig. 3: The signal data received by networkd-dispatcher.Bingo! The program successfully received our signal and debug printed the data contained within it. While there’s an error complaining about mismatching parameter types, the important part of receiving the signal worked as expected, and we can now read some more of the networkd-dispatcher source code to see where we need to go next.The following function contains the handling code for the signal, implementing checks to ensure the signal data received was formatted as expected:

Fig. 4: The _receive_signal function used to handle and verify signal data.We have about 40 lines of code here implementing various checks. Our target is to reach the handle_state function, which eventually leads to the vulnerable subprocess. Popen calls inside run_hooks_for_state.The code block may look intimidating at first, but by breaking down each individual check, we can get a better idea of what needs to be done:

  • Lines 396-399: The first parameter typ is checked against a constant string value, returning from the function if there is a mismatch.
  • Lines 400-403: The fourth parameter path (the object path sent with the signal) is again checked against a constant string, making sure that the parameter begins with /org/freedesktop/network1/link/_.
  • Lines 407-422: The path parameter has the data after the above string constant checked, expecting a number idx. This number is then checked against the self.iface_names_by_idx list, eventually logging an error and returning if not found.
  • Lines 424-425: The second parameter data is treated as a dict, and the script attempts to retrieve the OperationalState and AdministrativeState values from it.
  • Lines 427-428: The OperationalState and AdministrativeState values are checked for validity, and if at least one of them contains a value then we call the target function handle_state.

Now that we know each condition that needs to be met, we can try and find values for each of the signal parameters which will cumulatively satisfy all conditions.Starting with the easiest first, the first parameter typ only has a simple string comparison check, so if we set our first parameter inside the PoC script to org.freedesktop.network1.Link we can pass it.Next we have the data parameter –  again, this isn’t too hard to satisfy, as we simply need to provide a dict with the OperationalState and AdministrativeState keys present. These keys will have specific values used later to exploit the vulnerability, but for now it can be anything.The last parameter, path, requires a bit more inspection. The first part is easy; we simply make sure that the object path we provide starts with /org/freedesktop/network1/link/_. The second part has some unknowns, though – we need to match a value found in self.iface_names_by_idx, but we don’t know what exists in the array. However, thanks to the magic of print statements and our handy debugging setup, we can just get the script to tell us what we want to know! Observe below the result when we add in a call to print the list:

Fig. 5: Results from printing the iface_names_by_idx list.Great! So now we just have to provide a value matching one of these indices, like /org/freedesktop/network1/link/_1 for ‘lo’, right? Well, not quite; there’s a bit of magic going on within lines 407 and 408 that we’ll need to account for. Now, don’t ask me why it works this way, but this is essentially what’s happening:

  • We take the two digits found after the _, such as 99 in /org/freedesktop/network1/link/_99.
  • These two digits are cast to a base-16 integer, so 99 would be converted to 0x63.
  • This base 16 integer is then converted to a character using the chr function, which converts an ASCII value to its equivalent character.
  • This character is then cast back to an integer, and this integer is the final index used.

Again, it’s unclear why this approach works, but for the purposes of our exploit, we don’t really need to know. All we need is to find a valid number that corresponds to one of the values in iface_names_by_idx. Thankfully, this task isn’t too difficult, as it basically involves finding the hex value for one of the characters. For example, 2 is an index in the list, the hex representation of the character 2 in ASCII is 0x32, so our final value for the object path will be /org/freedesktop/network1/link/_32.Let’s try running our updated PoC with the new object path and argument values and see if we hit the target run_hooks_for_state function:

Fig. 6: Hitting the vulnerable function.Our script works as expected, passing all checks for the PropertiesChanged signal and following the correct code path straight down to the vulnerable function! Take a quick look at the updated PoC below, then we’ll start prodding at the real vulnerabilities within the script.

Fig. 7: The updated PoC script used to reach run_hooks_for_state.Developing the exploitBelieve it or not, that’s the hardest part over for this vulnerability. Exploiting the vulnerabilities themselves is relatively straightforward, with the most difficult bit being the process of reaching the vulnerable function. The vulnerabilities themselves aren’t trivial, but the wealth of information on the exploitation process within the original advisory certainly helps to smooth out the process.Let’s have a recap of the Exploitation section of the advisory, which outlines the steps needed to execute arbitrary code as root:

  • Set up a directory inside a writable folder such as /tmp/nimbuspwn, then insert a symlink to /sbin inside this folder called /tmp/nimbuspwn/poc.d.
  • For each root-owned executable in /sbin, create a payload file with the same name as the executable inside /tmp/nimbuspwn.
  • Broadcast a signal with an OperationalState value using directory traversal to target your directory, for example ../../../tmp/nimbuspwn/poc.
  • Quickly exploit the race condition and change the /tmp/nimbuspwn/poc.d symlink to target /tmp/nimbuspwn.

If you’ve played many CTFs, you’ll likely have come across these vulnerabilities (TOCTOU race condition, symlink editing, and directory traversal) in some form or another. This is where practice and repetition can come in handy, as you’ll have an innate sense of how such vulnerabilities are exploited, making it quicker to put together a mental plan of how you’ll write the code to perform the exploit.Let’s take this step by step again and analyze how the process above could be automated, allowing for a fully functional exploit script.To make the temporary directory and symlink, we can use the os library within Python to perform much of the needed functionality. Instead of hard-coding a static directory name, we’ll make use of the random library to generate a different directory for each attempt. This is to ensure that networkd-dispatcher forces an execution attempt every time rather than ignoring repeated attempts.

Fig. 8: The function used to create our temporary directory and symlink.Next up, we’ll tackle the symlinking of executables from /sbin. To do this, we’ll make use of the os module again, using the os.walk function to iterate through filenames, appending each to a files list so that we can keep track of them. Once we have all the filenames, we can use the os.access and os.stat functions to check each one is executable and root-owned, respectively. If so, the filename is added to another list, executables. Finally, for each of these executables, we create a new file with the same name inside our temporary directory and write the specified payload to it.

Fig. 9: The function used to create symlinks for /sbin executables.When it comes to triggering the signal over the correct bus, we can reuse most of the code we wrote during the initial signal broadcasting research. All we need is to slightly tweak the OperationalState and AdministrativeState values to insert the directory traversal payload and randomize the attempts, respectively.

Fig. 10: The function used to broadcast a signal across the system bus.Finally, we’ll write a little function to switch the symlinks after the signal has been sent. Again, this can again be done with os.symlink, with an extra call to os.remove to remove the previous symlink to /sbin.

Fig. 11: Switching the symlinks to exploit the race condition.In the main section, we can string all of these functions together, along with some code for generating the random attempt numbers, and check if the vulnerability was exploited successfully. A few calls to time.sleep are made to ensure the timings of each function are correct.

Fig. 12: Putting everything together – fingers crossed!With our script in a (hopefully) functional state, let’s give it a try and see if we can pop a root shell:

Fig. 13: Successfully exploiting the vulnerability and obtaining a root shell.What a beautiful sight! Our script worked as expected, generating a root shell for us in only two attempts. The full exploit script can be found on GitHub, which includes some small bits not covered here. For some more information on the vulnerability, you can also read the AttackerKB article.Disclosing responsiblyNow that we have an exploit script for a recently disclosed vulnerability that’s likely to be unpatched on many machines, the obvious step is to post it on Twitter with some cool hashtags, right?Unless you have a personal vendetta against blue teamers, this may not be the best way to release a one-day exploit script. Instead, consider the following points before releasing an exploit PoC to the public:

  • What is the severity of the vulnerability/impact of the exploit?
  • How likely is it that vulnerable instances have been patched?
  • Will blue teamers be able to quickly react to any malicious use of the exploit? (e.g., are we dropping this on a Friday at 4:59pm?)
  • Are there other public exploits already available, or is this the first one?

In the case of Nimbuspwn, we had some decisions to make. While the vulnerability was relatively low-impact and required certain conditions to exploit, there weren’t any other public exploits available, and the vulnerability had only been disclosed the day before.In the end, we decided to release the PoC, albeit waiting until after the weekend to avoid any panic patching. As well as this, with help from some of the blue teamers here at Immersive Labs, we put together a Sigma rule to assist with the detection of Nimbuspwn exploitation, hoping to even out any extra pressure put on blue teamers.exit()While that was a bit of a long read, it hopefully covered each step of the process and helped to shed some light on how it’s all linked together.

Getting used to making one-days (as well as exploit development in general) takes time and practice, so don’t get discouraged if it feels a bit overwhelming at first. Reading write-ups and blog posts from others as well as trying to re-create exploits for older, unpatched software are all great ways to practice.If you’d like a more guided approach and have access to the Immersive Labs platform, take a look at some of the series within our Exploit Development and Reverse Engineering categories. As well as this, if you want to try out the Nimbuspwn exploit script in a controlled environment, have a look at the Nimbuspwn (Offensive) lab in the CTI series.

Trusted by top companies worldwide
to enhance cybersecurity

Trusted by some of the world’s biggest brands, we’re committed to taking your cybersecurity readiness to the next level - and we’re just getting started.

What Our Customers
Are Saying About Immersive

Realistic simulation of current threats is the only way to test and improve response readiness, and to ensure that the impact of a real attack is minimized. Immersive’s innovative platform, combined with Kroll’s extensive experience, provides the closest thing to replication of a real incident — all within a safe virtual environment.

Paul Jackson
Regional Managing Director, APAC Cyber Risk, Kroll

The speed at which Immersive produces technical content is hugely impressive, and this turnaround has helped get our teams ahead of the curve, giving them hands-on experience with serious vulnerabilities, in a secure environment, as soon as they emerge.

TJ Campana
Head of Global Cybersecurity Operations, HSBC

We no longer worry about managing infrastructure, leaving us free to build great courses.

Daniel Duggan
Director, Zero-Point Security

Ready to Get Started?
Get a Live Demo.

Simply complete the form to schedule time with an expert that works best for your calendar.