Pentest Notes

Web & API — Pentest Notes

This is my working checklist for web app and API tests, roughly in the order I actually run it. I don't treat it as gospel: some targets are all API and no browser, some are the opposite. But the phases are here so I don't get tunnel vision on one shiny bug and forget to check access control. Everything below is lab and authorised-only. Where a technique has its own write-up, I've linked it.

0/26 done

01Recon & mapping

Fingerprint the stack before you touch anything

whatweb -a 3 https://target
curl -sSI https://target

Look for: Server and X-Powered-By headers, framework cookies (PHPSESSID, JSESSIONID, .AspNet), CSP and security headers, and anything that leaks a version.

Gotcha: Headers lie, especially behind a WAF or reverse proxy. I confirm the real framework with a favicon hash (favfreak) or an error page before I trust what the header says.

#recon#fingerprint

Map the attack surface: subdomains and hosts

subfinder -d target.com -all -silent | httpx -silent -title -sc -td
amass enum -passive -d target.com

Look for: Forgotten staging, dev and admin subdomains, old app versions, and anything with a different tech stack from the main site. The weird one is usually the way in.

Gotcha: Scope. A subdomain resolving to a third party (S3, a SaaS) is often out of scope and someone else's asset. Check the rules of engagement before you poke it.

#recon#subdomains

Content discovery: find what isn't linked

ffuf -w raft-medium-directories.txt -u https://target/FUZZ -mc all -fc 404
feroxbuster -u https://target -w raft-medium-files.txt -x php,bak,old,zip

Look for: /admin, /api, /.git, /backup, .bak and .old copies of live files, and status codes other than 404. A 403 means it's there and you're just not allowed yet.

Gotcha: Filter by size and words, not just status. A soft-404 page returns 200 for everything and will drown you in false positives. Auto-calibrate (-ac in ffuf) first.

#recon#fuzzing

Read the JavaScript, it tells you everything

katana -u https://target -jc -silent | grep -Ei 'api|graphql|/v[0-9]'
curl -s https://target/main.js | grep -Eo '/[a-zA-Z0-9_/-]+' | sort -u

Look for: Hidden API routes, feature flags, hardcoded keys, S3 buckets, internal hostnames, and endpoints the UI never calls but the code still serves.

Gotcha: Source maps (.js.map) are gold and people forget to strip them in production. If they're there, you get the readable source back.

#recon#javascript

Mine parameters the app forgot it accepts

arjun -u https://target/api/user -m GET
paramspider -d target.com

Look for: Undocumented params like debug, admin, id, redirect, file, role. Hidden parameters are where the least-tested code lives.

Gotcha: A param that changes the response length but not the visible page is worth a proper look. That's often a debug or internal toggle.

#recon#parameters

02Authentication & sessions

Attack the login the boring ways first

# username enumeration via response/timing differences
ffuf -w users.txt -u https://target/login -X POST -d 'user=FUZZ&pass=x' -H 'Content-Type: application/x-www-form-urlencoded' -mr 'invalid password'

Look for: Different messages for 'user not found' vs 'wrong password', different response times, and whether there's any rate limiting or lockout at all.

Gotcha: Rate limiting on /login but not on /api/login or the mobile endpoint is the classic gap. Always test every auth entry point, not just the one the browser uses.

#auth#bruteforce

Pull apart the JWT

# decode it (header.payload.signature)
echo '<token>' | cut -d. -f1 | base64 -d
# then try the classics in Burp: alg:none, alg confusion RS256->HS256, kid injection

Look for: The alg, whether exp is enforced, and any role/scope claim you can flip ("role":"user" -> "admin").

Gotcha: Decoding is not verifying. The whole point is whether the server actually checks the signature. Test alg:none and a resigned token with a guessed/empty secret. There's a JWT decoder in the labs if you want to eyeball one quickly.

#auth#jwt

Break the password reset flow

# request a reset, then inspect the token and the Host handling
curl -s -X POST https://target/reset -d 'email=victim@corp.com' -H 'Host: attacker.com'

Look for: Guessable/sequential reset tokens, tokens that don't expire or aren't single-use, and Host-header poisoning that sends the reset link to your domain.

Gotcha: Password reset is where account takeover usually actually happens, not the login page. Also check whether changing the password kills existing sessions. Often it doesn't.

#auth#account-takeover

Test session and MFA logic

# after login, does the session id rotate? compare pre/post cookies
# can you replay a session after logout?

Look for: Session fixation (id doesn't change on login), tokens still valid after logout, MFA that can be skipped by hitting the post-MFA endpoint directly.

Gotcha: MFA bypass is often a flow bug: complete step one, then request the authenticated resource directly and see if step two was ever really required.

#auth#session#mfa

03Access control

IDOR / BOLA: change the id, keep the session

# as user A, request user B's object
curl -s https://target/api/orders/1002 -H 'Authorization: Bearer <A_token>'

Look for: Any object reference you can increment, swap or guess (numeric ids, UUIDs in the response you can reuse, filenames). If you get B's data with A's token, that's the bug.

Gotcha: This is the number one API bug (OWASP API #1) and the easiest to miss because the UI never shows the request. Test read AND write: GET is bad, PUT/DELETE on someone else's object is worse.

#access-control#idor#bola

BFLA: call the function you're not allowed to

# take an admin-only request and replay it as a low-priv user
curl -s -X POST https://target/api/admin/users -H 'Authorization: Bearer <lowpriv>' -d '{...}'

Look for: Admin endpoints that only hide the button, not the route. Method-based gaps too: GET is blocked but POST/PUT isn't.

Gotcha: Map the admin UI while logged in as admin, save every request, then replay them all as a normal user. Function-level auth is usually enforced inconsistently across endpoints.

#access-control#bfla

Mass assignment: send fields you were never given

# add privileged fields to a normal update
curl -s -X PUT https://target/api/me -d '{"name":"x","role":"admin","isVerified":true}'

Look for: The API binding the whole JSON body to the model, so role, credit, isAdmin or verified get set even though the form never showed them.

Gotcha: You find the field names from the GET response, the JS, or the mobile app. If the object returns a field, try writing it back.

#access-control#mass-assignment

04Injection

SQL injection: prove it, then let a tool grind

# manual first: ' and a boolean/time payload
curl -s "https://target/item?id=1' AND SLEEP(3)-- -"
# then sqlmap on the exact request
sqlmap -r request.txt --batch --risk 2 --level 3

Look for: Time delays, boolean differences, DB errors, and any parameter that reaches a query (search, filters, sort, id).

Gotcha: Don't lead with sqlmap on prod. Confirm manually, understand the injection, then scope the tool. I broke down injection classes properly in the write-up.

#injection#sqli → related write-up

NoSQL and operator injection

# auth bypass with a query operator
curl -s -X POST https://target/login -H 'Content-Type: application/json' -d '{"user":"admin","pass":{"$ne":null}}'

Look for: JSON APIs on Mongo and friends where {"$ne":null} or {"$gt":""} slips past auth or filters.

Gotcha: Switch the content type. A form-encoded body might be safe but the same endpoint accepting JSON lets you send operators. Always try both.

#injection#nosqli

SSTI: from reflection to RCE

# fingerprint the engine
curl -s 'https://target/hello?name={{7*7}}'
# if 49 comes back, escalate per engine (Jinja2, Twig, Freemarker)

Look for: Maths that gets evaluated ({{7*7}} -> 49, ${7*7}, #{7*7}). Reflected user input in a server-side template is the tell.

Gotcha: {{7*7}} vs ${7*7} tells you the engine, which decides the RCE gadget. Get the engine right before you throw payloads.

#injection#ssti

Command injection and XXE

# command injection: chain a separator
curl -s 'https://target/ping?host=127.0.0.1;id'
# XXE: swap the XML body for an entity that reads a file
# <!DOCTYPE r [<!ENTITY x SYSTEM 'file:///etc/passwd'>]>

Look for: Anything that shells out (ping, convert, pdf, zip) and any endpoint that parses XML/SVG/DOCX. Blind versions leak over DNS/HTTP to your collaborator.

Gotcha: Blind is the norm now. Have an out-of-band listener (interactsh/Collaborator) ready before you test, or you'll miss the ones that don't reflect.

#injection#rce#xxe

05SSRF & file handling

SSRF: make the server fetch for you

# point a url param at your listener, then at internal ranges
curl -s 'https://target/fetch?url=http://YOUR-OOB'
curl -s 'https://target/fetch?url=http://169.254.169.254/latest/meta-data/'

Look for: Any feature that takes a URL: webhooks, image-from-url, PDF render, link preview, import. The cloud metadata endpoint (169.254.169.254) is the crown jewel.

Gotcha: Filters get bypassed with redirects, [::], decimal IPs, and @ tricks. And IMDSv2 needs a token header, so a blind SSRF that can't set headers won't reach it. Know which metadata version you're hitting.

#ssrf#cloud

File upload to code execution

# try a webshell with content-type and extension tricks
# shell.php , shell.php.jpg , shell.phtml , magic bytes + polyglot

Look for: Where the file lands and whether that path is executable. An upload is only RCE if you can reach it and the server runs it.

Gotcha: Even if you can't get code exec, an SVG or HTML upload served on the same origin is stored XSS. Don't write it off just because .php was blocked.

#files#upload#rce

Path traversal and LFI

curl -s 'https://target/download?file=../../../../etc/passwd'
# encodings when the naive one is filtered: %2e%2e%2f , ..%2f , ....//

Look for: File/path/template/download params. Reading /etc/passwd or a Windows file confirms it; from there, config files with creds are the real prize.

Gotcha: LFI plus a log or session file you can poison sometimes becomes RCE. And a null byte or double-encoding still works on older stacks more often than you'd think.

#files#lfi#traversal

06Client-side

XSS: reflected, stored, and DOM

# reflected canary
curl -s 'https://target/search?q=unix9987<b>'
# then a real payload if the canary renders unescaped

Look for: Where your input comes back unencoded: HTML body, attributes, JS context, or a DOM sink (innerHTML, document.write, location). Stored is worst; it hits other users.

Gotcha: The context decides the payload. Breaking out of an attribute needs a quote; a JS-context sink might not need any tags at all. And a strict CSP can neuter an otherwise-valid XSS, so check the header first.

#client-side#xss

CSRF and CORS

# CORS: does it reflect an arbitrary origin with credentials?
curl -s -I https://target/api/me -H 'Origin: https://evil.com'

Look for: State-changing requests with no CSRF token or SameSite protection; and Access-Control-Allow-Origin reflecting your origin alongside Allow-Credentials: true.

Gotcha: A reflected-origin CORS with credentials is basically a read-any-data bug. And SameSite=Lax (now the default) kills a lot of classic CSRF, so confirm the cookie's actual attributes rather than assuming.

#client-side#csrf#cors

Open redirect and clickjacking

curl -sI 'https://target/go?url=https://evil.com'
# clickjacking: is X-Frame-Options / frame-ancestors missing?

Look for: redirect, next, returnUrl, callback params that send you off-site, and pages with no framing protection.

Gotcha: Open redirect looks low severity alone, but it's the glue in a lot of chains: OAuth token theft, SSRF filter bypass, phishing that lands on the real domain first.

#client-side#redirect

07API-specific

GraphQL: introspect, then abuse

# pull the whole schema if introspection is on
curl -s -X POST https://target/graphql -H 'Content-Type: application/json' -d '{"query":"{__schema{types{name fields{name}}}}"}'

Look for: Introspection left enabled, queries that expose more than the UI, and nested queries you can nest deeply for a DoS.

Gotcha: GraphQL flattens access control: one endpoint, many objects, and auth is often checked per-resolver inconsistently. Test each type for BOLA, not just the app as a whole.

#api#graphql

Rate limits and resource abuse

# does the API cap page size / batch size?
curl -s 'https://target/api/items?limit=1000000'

Look for: Unbounded limit/page, batch endpoints, and expensive operations with no throttle. OWASP API #4 is unrestricted resource consumption.

Gotcha: This is easy to demonstrate responsibly (one big-but-reasonable request) and easy to take too far. Show the risk, don't knock the target over.

#api#dos

08Post-exploitation & reporting

Chain the low-sevs into something real

Look for: How the findings connect: open redirect + OAuth = token theft; IDOR + weak reset = full account takeover; SSRF + metadata = cloud creds. The report writes itself when you show the chain.

Gotcha: A pile of 'informational' findings is worth more assembled into one working attack path. That's the difference between a scanner report and a pentest.

#reporting#chaining

Capture clean evidence and clean up

Look for: Minimal, reproducible proof for each finding: the exact request, the response, and the impact in one sentence. Note anything you created (test accounts, uploaded files) so it can be removed.

Gotcha: Screenshot the request AND response with the URL visible. 'Trust me it worked' doesn't survive a client's dev team pushing back. And delete your test artefacts; leaving a webshell on a client box is how a pentest becomes an incident.

#reporting#cleanup

Nothing matches that filter. Clear the search, or tell me what to add.