Web Security & Ethical Hacking · Lesson 21 of 23
Penetration Testing Basics — What Pentesters Actually Do
Vulnerability Scanning vs. Penetration Testing
These terms are often used interchangeably, but they describe different activities with different depth and purpose.
Vulnerability scanning is automated. A tool crawls your application, checks known patterns (CVE signatures, misconfiguration patterns, outdated software versions), and reports findings. It is fast, cheap, and produces a lot of output — including false positives. It finds the low-hanging fruit: missing security headers, known CVE in a library, exposed admin panel.
Penetration testing is manual (or semi-automated with human-guided tooling). A tester actively tries to exploit vulnerabilities to demonstrate real-world impact. It requires understanding the application's logic, not just its surface. A scanner cannot find IDOR (it doesn't know that orderId=456 belongs to a different user). A human tester can.
The workflow for testing your own API:
- Automated scan first — find and fix obvious issues
- Manual testing — probe business logic, authorization, authentication
- Document findings — reproducible steps, severity, remediation
- Retest after fixes — verify the vulnerability is actually closed
Legal Requirements First
This cannot be overstated: only test systems you own or have explicit written permission to test.
Testing a system without permission — even if you discover a real vulnerability — is illegal in most jurisdictions. This includes:
- Systems run by third parties your company uses
- Production systems of your own company unless you have written authorization from appropriate management
- Other users' accounts on your own system (you own the platform, not their data)
For your own development/staging API: you have blanket permission. For production: get written authorization before running any active scanning tools.
Bug bounty programs provide explicit scope and legal safe harbor for testing. If a company runs one (HackerOne, Bugcrowd), their scope document defines what you are and aren't authorized to test.
Setting Up OWASP ZAP
OWASP ZAP (Zed Attack Proxy) is a free, open-source security scanning and manual testing tool. It sits between your browser/client and your API as a proxy, recording traffic and scanning it.
Installation
Download from zaproxy.org. Available for Windows, macOS, Linux.
Automated API Scan with ZAP
ZAP can scan an API directly using its OpenAPI/Swagger spec:
# Docker-based ZAP scan against a local API with OpenAPI spec
docker run --rm \
-v $(pwd)/zap-reports:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t http://host.docker.internal:5000/swagger/v1/swagger.json \
-f openapi \
-r zap-report.html \
-x zap-report.xmlZAP crawls all documented endpoints, sends fuzzing payloads, and reports findings. Common automated findings:
- Missing security headers
- Information disclosure in error responses
- Cookie flags (SameSite, Secure, HttpOnly)
- SQL injection patterns
- CORS misconfigurations
Using ZAP as a Proxy for Manual Testing
Configure your HTTP client to use ZAP as proxy (localhost:8080). All requests flow through ZAP, which records them in the Sites tree. You can then:
- Re-send and modify requests in the Request editor
- Spider the API from observed traffic
- Active scan specific endpoints you've identified manually
- Fuzz specific parameters
Burp Suite Community Edition for Manual Testing
Burp Suite is the industry standard for manual web/API security testing. The Community edition (free) includes the core tools needed for most manual testing.
Setup
Download from portswigger.net. Configure your HTTP client or browser to use Burp's proxy at localhost:8080. Import Burp's CA certificate into your trust store to intercept HTTPS.
For API testing without a browser, configure curl or your test HTTP client:
# Route curl through Burp proxy
curl --proxy http://localhost:8080 \
--cacert ~/burp-ca.crt \
-H "Authorization: Bearer $TOKEN" \
https://localhost:5001/api/orders/123Intercepting and Modifying Requests
Burp's Proxy → Intercept tab captures requests before they're sent. You can modify any part — headers, body, path parameters — then forward the modified request.
Use case: capture a legitimate GET /api/orders/123 request, change 123 to 456, forward, observe response. If the response returns order 456 (belonging to another user) — that's IDOR.
Repeater
Repeater lets you send the same request multiple times with modifications. Workflow:
- Intercept a request
- Send to Repeater (Ctrl+R)
- Modify and resend as many times as needed
- Compare responses side by side
This is your primary tool for manually testing authorization: send a request with a valid token, then with an invalid token, then with no token, then with a different user's token.
Intruder — Fuzzing Inputs
Intruder automates sending many variations of a request. In Community edition, it is rate-limited (which actually works in your favor for responsible testing).
Use case — fuzzing an ID parameter:
- Send
GET /api/orders/§123§to Intruder - Mark
123as the payload position (§) - Set payload list: sequential numbers 1–1000
- Start attack — Intruder sends 1000 requests, each with a different ID
- Sort responses by status code and length — 200 responses with non-empty bodies indicate resources you can access
Testing Authentication
Test Matrix for Auth Endpoints
For each protected endpoint, test systematically:
# 1. Valid token — should succeed
curl -H "Authorization: Bearer $VALID_TOKEN" https://api/orders/123
# Expected: 200 OK
# 2. No token — should fail
curl https://api/orders/123
# Expected: 401 Unauthorized
# 3. Malformed token
curl -H "Authorization: Bearer not.a.real.token" https://api/orders/123
# Expected: 401 Unauthorized
# 4. Expired token
curl -H "Authorization: Bearer $EXPIRED_TOKEN" https://api/orders/123
# Expected: 401 Unauthorized
# 5. Token for different audience (alg confusion test)
# Create a JWT signed with HS256 using the RS256 public key as HMAC secret
# Expected: 401 UnauthorizedTesting for Algorithm Confusion (JWT)
Algorithm confusion attacks exploit APIs that trust the alg header in the JWT:
import jwt
import base64
# Read the RS256 public key (which is public knowledge)
with open("public_key.pem") as f:
public_key = f.read()
# Create a token signed with HS256 using the public key as the HMAC secret
malicious_payload = {"sub": "user-123", "role": "admin"}
malicious_token = jwt.encode(
malicious_payload,
public_key,
algorithm="HS256"
)
print(malicious_token)
# Send this token to the API — if it accepts it, the API has algorithm confusion vulnerabilityExpected result: 401 Unauthorized. If the API returns 200, it is accepting an HS256 token when it should only accept RS256 — a critical authentication bypass.
Testing for alg: none
# Craft a JWT with no signature (alg: none)
# Header: {"alg":"none","typ":"JWT"}
# Payload: {"sub":"admin-user-id","role":"admin"}
# Signature: (empty)
NONE_TOKEN="eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbi11c2VyLWlkIiwicm9sZSI6ImFkbWluIn0."
curl -H "Authorization: Bearer $NONE_TOKEN" https://api/admin/users
# Expected: 401 UnauthorizedTesting Authorization — IDOR
IDOR (Insecure Direct Object Reference) is one of the most common and impactful API vulnerabilities. The test is simple: access a resource belonging to another user.
Setup
You need two test accounts: User A and User B, both created in your test environment.
# Authenticate as User A
TOKEN_A=$(curl -s -X POST https://api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"usera@test.com","password":"TestPass1!"}' \
| jq -r '.accessToken')
# Authenticate as User B
TOKEN_B=$(curl -s -X POST https://api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"userb@test.com","password":"TestPass1!"}' \
| jq -r '.accessToken')
# User A creates an order
ORDER_ID=$(curl -s -X POST https://api/orders \
-H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":2}' \
| jq -r '.id')
# User B tries to access User A's order — this should be denied
curl -s -w "\nHTTP Status: %{http_code}" \
-H "Authorization: Bearer $TOKEN_B" \
https://api/orders/$ORDER_ID
# Expected: HTTP Status: 403 or 404
# FAIL if: HTTP Status: 200 with User A's dataTest IDOR on every resource type in your API: users, orders, invoices, files, messages. Also test write operations — can User B update or delete User A's order?
Testing Injection — SQL Injection and SQLMap
Manual SQL Injection Test
Insert SQL metacharacters and observe behavior:
# Test string parameter for SQL injection
curl "https://api/products?search=laptop'"
# If response is 500 Internal Server Error with SQL error — vulnerable
# If response is 400 Bad Request or empty results — likely protected
# Test with classic payload
curl "https://api/products?search=laptop' OR '1'='1"
# If this returns ALL products — vulnerable to injectionSQLMap (Automated SQL Injection Testing)
SQLMap is a specialized tool for detecting and exploiting SQL injection. Use it on your own API:
# Test a GET endpoint with a potentially injectable parameter
sqlmap -u "https://api/products?search=laptop" \
-H "Authorization: Bearer $TOKEN" \
--level=2 \
--risk=1 \
--batch \
--output-dir=./sqlmap-results
# Test a POST endpoint
sqlmap -u "https://api/search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data='{"query":"laptop"}' \
--level=2 \
--batchExpected result: SQLMap reports "not injectable" for all parameters. Any confirmed injection finding is critical.
Command Injection Testing
If your API calls system commands (file conversion, image processing, report generation), test for command injection:
# Basic command injection payloads
curl -X POST https://api/convert \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filename":"report.pdf; whoami"}'
curl -X POST https://api/convert \
-H "Content-Type: application/json" \
-d '{"filename":"report.pdf | sleep 5"}'
# If the response takes 5+ extra seconds — time-based command injectionTesting Rate Limiting
Rate limiting is often declared but not actually enforced. Test it:
#!/bin/bash
# Send 20 rapid requests and count 429 responses
SUCCESS=0
RATE_LIMITED=0
for i in $(seq 1 20); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
https://api/orders)
if [ "$STATUS" -eq "200" ]; then
SUCCESS=$((SUCCESS + 1))
elif [ "$STATUS" -eq "429" ]; then
RATE_LIMITED=$((RATE_LIMITED + 1))
fi
done
echo "Success: $SUCCESS, Rate limited: $RATE_LIMITED"
# If RATE_LIMITED = 0 after 20 rapid requests — rate limiting is not workingTest separately:
- Rate limiting on the login endpoint (more restrictive — 5 per minute per IP)
- Rate limiting on the general API (looser — 100 per minute per user)
- Whether rate limiting resets correctly after the window expires
Responsible Disclosure vs. Bug Bounty Programs
If you find a vulnerability in someone else's system (even accidentally):
Responsible disclosure: Contact the organization directly through their security contact (security@company.com, HackerOne private disclosure). Give them a reasonable time to fix before publishing (typically 90 days, following Google Project Zero standard). Provide enough detail to reproduce but don't publish exploit code while the vulnerability is unpatched.
Bug bounty programs: Companies like HackerOne and Bugcrowd host formal programs with:
- Explicit authorized scope (what you can and cannot test)
- Legal safe harbor (protection from prosecution for in-scope testing)
- Monetary rewards for valid findings
- Clear disclosure timeline
If a company has a bug bounty program, use it. The explicit scope and legal protection make it far safer than informal disclosure.
Summary
Testing your own API's security is a mandatory part of the development process, not optional. Start with automated ZAP scanning to catch obvious issues. Use Burp Suite Repeater for methodical manual testing of authentication and authorization. Test IDOR explicitly with two test accounts. Run SQLMap against any parameter-driven queries. Verify rate limiting actually works with a simple load script. Document every finding with reproduction steps, expected vs. actual behavior, and severity. Fix, retest, close.