A penetration test runs in six practical phases: scoping and rules of engagement, reconnaissance, scanning and enumeration, exploitation, post-exploitation, and reporting. Most of the value, and most of the risk, sits in the first and last phases, not in the exciting middle.
To make that concrete, we will follow one engagement from start to finish against Acme Fintech, a fictional payments start-up at acme-fintech.test. Everything below ran in a lab built for this post: private 10.x addresses, a .test domain that cannot exist on the public internet, and seeded vulnerabilities. Only test systems you own or have written permission to test.
Which methodology do penetration testers follow?
Two public references describe the phases, and it helps to see how they line up.
NIST SP 800-115, Technical Guide to Information Security Testing and Assessment, describes penetration testing in four phases: Planning, Discovery, Attack and Reporting, with the Attack phase looping back into Discovery as new access reveals new systems. It dates from 2008, but its structure still holds.
The Penetration Testing Execution Standard (PTES) splits the work into seven sections: Pre-engagement Interactions, Intelligence Gathering, Threat Modelling, Vulnerability Analysis, Exploitation, Post Exploitation and Reporting.
| Practical phase (this post) | NIST SP 800-115 | PTES |
|---|---|---|
| 1. Scoping and rules of engagement | Planning | Pre-engagement Interactions |
| 2. Reconnaissance | Discovery | Intelligence Gathering, Threat Modelling |
| 3. Scanning and enumeration | Discovery | Vulnerability Analysis |
| 4. Exploitation | Attack | Exploitation |
| 5. Post-exploitation | Attack (and back to Discovery) | Post Exploitation |
| 6. Reporting | Reporting | Reporting |
Neither framework is a checklist you tick in order. Real tests loop: something you find in phase 4 sends you back to phase 2 with a new question.
Phase 1: Scoping and rules of engagement
Acme Fintech’s CTO wants “a pentest before our Series A”. That is not a scope. The scoping call turns it into something testable and legally safe.
What we agreed with Acme (fictional):
- In scope:
app.acme-fintech.test(customer web app),api.acme-fintech.test(REST API used by the mobile app), and the lab’s stand-in ‘external’ range10.10.20.0/28. - Out of scope: the card processor’s systems (a third party Acme does not own), staff personal devices, and denial-of-service testing.
- Test accounts: two customer accounts (
tester-a,tester-b) and one support-staff account, created by Acme for the test. - Window: weekdays 09:00–18:00 WAT, with a named contact on each side and an emergency phone number.
- Data handling: if we reach real customer data, we stop, take the minimum evidence (a record count and one redacted sample), and call the contact.
- Source IPs: our testing addresses are listed so Acme’s monitoring team can tell us apart from a real attacker.
All of that goes into a signed rules of engagement document alongside the contract. The signature matters: it is the difference between a penetration test and an offence. If the client cannot confirm they own an asset, it comes out of scope until they can.
Beginner lesson: most junior testers get into trouble here, not in exploitation. Re-read the scope before every session and keep it open in a second window.
Phase 2: Reconnaissance
Reconnaissance means learning about the target before touching it hard. It is split into passive work (public sources, no direct contact) and light active work (DNS lookups and fetching the homepage like any visitor would).
In a real engagement, passive sources include certificate transparency logs, public code repositories, job adverts that name the tech stack, and DNS records. In our lab, Acme’s internal DNS answers for the .test zone:
dig +short app.acme-fintech.test
dig +short api.acme-fintech.test
dig +short TXT acme-fintech.test
10.10.20.5
10.10.20.6
“v=spf1 include:_spf.mailhost.example.com -all”
Then a polite look at what the web servers say about themselves:
curl -sI https://app.acme-fintech.test
HTTP/2 200
server: nginx/1.24.0
content-type: text/html; charset=utf-8
x-powered-by: Express
set-cookie: sid=s%3A9f2c...; Path=/; HttpOnly; Secure
Two notes go straight into our working file: the app is Node.js behind nginx, and it leaks x-powered-by: Express. Not a vulnerability on its own, but it tells us what kind of bugs to look for.
PTES puts threat modelling here, and it is worth five minutes even on a small test. For a fintech the obvious question is: what would a criminal want? Answer: other customers' money and data. So the API’s authorisation checks become our top priority.
Phase 3: Scanning and enumeration
Now we map what is listening and what it runs. A full TCP port scan of the in-scope range, with service detection:
sudo nmap -sS -sV -p- --min-rate 1000 -oA acme-full 10.10.20.0/28
Nmap scan report for app.acme-fintech.test (10.10.20.5)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13 (Ubuntu Linux; protocol 2.0)
443/tcp open ssl/http nginx 1.24.0
Nmap scan report for api.acme-fintech.test (10.10.20.6)
PORT STATE SERVICE VERSION
443/tcp open ssl/http nginx 1.24.0
9000/tcp open http Node.js Express framework
Nmap done: 16 IP addresses (2 hosts up) scanned in 41.87 seconds
-oA saves output in all three Nmap formats, which you will want as evidence later. The Nmap reference guide explains every flag used here. Port 9000 on the API host is interesting: it looks like the Node app answering directly, bypassing nginx.
Next, content discovery on the web app with a wordlist:
ffuf -u https://app.acme-fintech.test/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/common.txt \
-mc 200,301,302,403 -rate 50
admin [Status: 302, Size: 28, Words: 4, Lines: 1]
api-docs [Status: 200, Size: 3104, Words: 211, Lines: 77]
login [Status: 200, Size: 4410, Words: 512, Lines: 98]
.env [Status: 403, Size: 153, Words: 3, Lines: 8]
We kept the request rate low (-rate 50) because the rules of engagement exclude anything that looks like denial of service. api-docs is public Swagger documentation, which hands us a full list of API endpoints, including GET /v1/accounts/{accountId}/statements.
A vulnerability scanner might run here too. Treat scanner output as leads, not findings: every item needs manual confirmation before it reaches the report.
Phase 4: Exploitation
Exploitation means proving that a weakness is real and showing its impact, with the smallest action that does so.
Log in as tester-a, whose account ID is 100481, and request our own statements:
curl -s -H “Authorization: Bearer $TOKEN_A” \
https://api.acme-fintech.test/v1/accounts/100481/statements | jq ‘.[0]’
{
“accountId”: 100481,
“owner”: “tester-a”,
“period”: “2026-09”,
“closingBalance”: “15000.00”
}
Now change one number, to tester-b's account (which Acme also created for us), still using tester-a’s token:
curl -s -H “Authorization: Bearer $TOKEN_A” \
https://api.acme-fintech.test/v1/accounts/100482/statements | jq ‘.[0]’
{
“accountId”: 100482,
“owner”: “tester-b”,
“period”: “2026-09”,
“closingBalance”: “8200.00”
}
The API checks that we are logged in but not that the account belongs to us. This is broken object level authorisation, number one on the OWASP API Security Top 10 (2023). Note what we did not do: we did not loop through thousands of IDs pulling real customers' statements. Two test accounts prove the flaw; harvesting real data proves nothing extra and breaks the data-handling rule.
If you want to practise this exact class of bug legally, PortSwigger’s free Web Security Academy access-control labs are excellent.
Phase 5: Post-exploitation
Post-exploitation asks: now that we have a foothold, how far could a real attacker go, and what does that mean for the business?
Back in phase 3 we saw Express listening directly on port 9000. Requesting .env, which nginx blocks on the public port (a quick check of https://api.acme-fintech.test/.env returned 403), directly from Express on port 9000:
curl -s http://10.10.20.6:9000/.env | sed -E ‘s/=.*/=[redacted]/’
NODE_ENV=[redacted]
DB_HOST=[redacted]
DB_USER=[redacted]
DB_PASSWORD=[redacted]
JWT_SECRET=[redacted]
We redacted the values on our side as we captured them, so secrets never land in our notes in clear text. The finding is severe on its own: with the JWT signing secret, an attacker could forge a token for any user, including support staff.
The rules of engagement now decide what happens next. Acme’s say we may demonstrate impact but must not touch production data. So we:
- Confirm the JWT secret is valid by forging a token for our own test account only, and checking it is accepted.
- Stop. We do not log into the database, even though we have the credentials.
- Call Acme’s named contact the same day, because exposed production secrets need rotating now, not in two weeks when the report lands.
This is where NIST’s Attack phase loops back to Discovery: the new access would open new targets (the database host), and the scope tells us whether we may follow it. Here, we may not.
Cleanup is part of this phase too: remove any test files or accounts you created, and list everything you changed in the report.
Phase 6: Reporting
The report is the product. Acme is not paying for the moment the IDOR worked; it is paying for a document that lets developers fix the problem and lets the CTO explain the risk to investors.
A good finding has a consistent shape:
| Field | Example for the BOLA finding |
|---|---|
| Title | Customers can read other customers' statements (broken object level authorisation) |
| Severity | Rated with CVSS v4.0 plus a business-context note |
| Affected asset | GET /v1/accounts/{accountId}/statements on api.acme-fintech.test |
| Description | What is wrong, in plain English first, technical detail second |
| Evidence | The two requests above, with timestamps and redacted tokens |
| Impact | Any logged-in customer could read any other customer’s statements |
| Remediation | Check on every request that the account ID belongs to the authenticated user; add automated tests for it |
| Retest status | Open, pending fix |
Write an executive summary a non-technical director can read in two minutes, then the detailed findings for engineers. Our separate guide, How to write a penetration testing report, includes a full sample built on this same fictional company.
Most engagements end with a retest: once Acme fixes the issues, we repeat the exact requests and update each finding’s status.
How long does each phase take?
It depends on scope, but as a rough shape for a small web and API test: scoping happens before the test window opens, recon and scanning take a meaningful share of the first days, exploitation and post-exploitation take the middle, and reporting takes longer than beginners expect. Budget real time for writing; a rushed report undoes a careful test.
What beginners get wrong about the phases
- Skipping recon. Jumping straight to exploits means missing things like a public
api-docspage. - Trusting the scanner. Unconfirmed scanner output in a report damages your credibility fast.
- Proving too much. Impact is shown with the minimum action. Dumping a database is not better evidence than two test accounts.
- Keeping secrets in notes. Redact as you capture.
- Leaving the report until the end. Write findings up while the evidence is fresh.
If you are wondering which tools to learn for each phase, see Penetration testing tools: a beginner’s toolkit, and for the career route itself, How to become a penetration tester from scratch.