mirror of
https://github.com/soypat/lneto.git
synced 2026-08-08 17:03:40 +00:00
1884cfc9b7
* add Exchange.WriteBodyString * Mux.MaxPathValues and other improvements * Mux PathValue improvemnt and fixes * MuxSlice more method muxing improvements * diagram out interesting approach to form parsing for clanker * refactor RequestParseForm and achieve greatness in API design * explicit naming of headerCapacityKV value in kvBuffer.Reset * fix Mux bug not matching paths correctly; httpraw HTTP V1 naming applied * rename many examples,use httphi in examples,remove useless maxAwaitingConn field * add ipv4.String * add ipv4 UnspecifiedAddr and BroadcastAddr * add ethernet.String
260 lines
8.8 KiB
Bash
Executable File
260 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# fuzz.sh points ffuf at an already running main-httplinux server. Start it
|
|
# yourself first, in another terminal, so its log and any crash stay visible:
|
|
#
|
|
# go run ./examples/http-linux -port 8080
|
|
# ./examples/http-linux/fuzz.sh # all cases
|
|
# ./examples/http-linux/fuzz.sh paths cookie # named cases only
|
|
# URL=http://localhost:9000 ./examples/http-linux/fuzz.sh
|
|
#
|
|
# Install ffuf with: go install github.com/ffuf/ffuf/v2@latest
|
|
#
|
|
# Every case ends by reporting the server is still up: a case that "finds
|
|
# nothing" because the process died is the failure this is looking for.
|
|
set -u
|
|
|
|
URL="${URL:-http://localhost:8080}"
|
|
# Matched to the server's FixedNumGoroutines: in worker mode the router owns one
|
|
# exchange per goroutine and refuses a connection outright when none is free, so
|
|
# that count is what bounds concurrency. Going above it is correct backpressure,
|
|
# but it reaches ffuf as a connection error and hides the response a case was
|
|
# looking for. Raise it to exercise the drop path.
|
|
THREADS="${THREADS:-4}"
|
|
|
|
WORDDIR="$(mktemp -d)"
|
|
trap 'rm -rf "$WORDDIR"' EXIT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wordlists. Kept here rather than pulled from SecLists so a run is repeatable
|
|
# and every entry is aimed at the parser: percent escapes, separators the
|
|
# grammar gives meaning to, and lengths that cross the server's fixed buffers.
|
|
# ---------------------------------------------------------------------------
|
|
cat >"$WORDDIR/paths.txt" <<'EOF'
|
|
admin
|
|
login
|
|
search
|
|
health
|
|
echo
|
|
upload
|
|
users
|
|
files
|
|
users/alice
|
|
users/bob
|
|
users/carol
|
|
users/mallory
|
|
users/al%69ce
|
|
users/%zz
|
|
users/%2e%2e%2f
|
|
users/alice/extra
|
|
files/
|
|
files/readme.txt
|
|
files/logo.png
|
|
files/notes.md
|
|
files/a/b/c
|
|
files/%2e%2e/%2e%2e/etc/passwd
|
|
EOF
|
|
|
|
cat >"$WORDDIR/queries.txt" <<'EOF'
|
|
go
|
|
go+lang
|
|
go%20lang
|
|
%21%40%23
|
|
%zz
|
|
%
|
|
%2
|
|
a=b
|
|
a&b
|
|
a;b
|
|
""
|
|
EOF
|
|
|
|
cat >"$WORDDIR/passwords.txt" <<'EOF'
|
|
hunter2
|
|
password
|
|
admin
|
|
letmein
|
|
hunter2%00
|
|
hunter2+
|
|
hun%74er2
|
|
EOF
|
|
|
|
cat >"$WORDDIR/tokens.txt" <<'EOF'
|
|
s3cr3t-session-token
|
|
admin
|
|
""
|
|
"s3cr3t-session-token"
|
|
s3cr3t-session-token; debug
|
|
s3cr3t-session-token;debug
|
|
=====
|
|
;;;;;
|
|
EOF
|
|
|
|
cat >"$WORDDIR/headers.txt" <<'EOF'
|
|
plain
|
|
with spaces
|
|
%00%01%02
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
EOF
|
|
|
|
cat >"$WORDDIR/names.txt" <<'EOF'
|
|
a.bin
|
|
report.pdf
|
|
../escape.txt
|
|
%2e%2e%2fescape.txt
|
|
EOF
|
|
|
|
# grow prints a line of n 'A's, for the cases that walk a value past a buffer.
|
|
grow() { printf 'A%.0s' $(seq "$1"); printf '\n'; }
|
|
{ for n in 8 64 512 1024 2048 4096 8192; do grow "$n"; done; } >"$WORDDIR/long.txt"
|
|
|
|
alive() {
|
|
if curl -s -o /dev/null --max-time 5 "$URL/health"; then
|
|
printf ' server alive\n\n'
|
|
else
|
|
printf ' *** SERVER DOWN after this case ***\n\n'
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
case_header() { printf '=== %s: %s\n' "$1" "$2"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cases. Each one drives a different part of the request through the parser.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# paths walks the mux: literal patterns, the "{id}" single-segment wildcard and
|
|
# the "{path...}" wildcard that swallows slashes. -mc all because a 404 from an
|
|
# unregistered path is a correct answer worth seeing next to the 200s.
|
|
fuzz_paths() {
|
|
case_header paths "mux patterns, wildcards and percent escapes in the path"
|
|
ffuf -u "$URL/FUZZ" -w "$WORDDIR/paths.txt" -t "$THREADS" -s -timeout 5 -mc all -fc 404
|
|
alive
|
|
}
|
|
|
|
# recursion follows the "{path...}" wildcard down, which is the pattern a
|
|
# directory scanner exercises hardest.
|
|
fuzz_recursion() {
|
|
case_header recursion "\"{path...}\" wildcard walked recursively"
|
|
ffuf -u "$URL/files/FUZZ" -w "$WORDDIR/paths.txt" -t "$THREADS" -s -timeout 5 \
|
|
-recursion -recursion-depth 2 -recursion-strategy greedy -mc all -fc 404
|
|
alive
|
|
}
|
|
|
|
# longpath pushes the request-target past RequestHeaderBufferSize. The server
|
|
# should answer 431 or drop the connection, never serve a mangled path.
|
|
fuzz_longpath() {
|
|
case_header longpath "request-target grown past the request header buffer"
|
|
ffuf -u "$URL/FUZZ" -w "$WORDDIR/long.txt" -t 4 -s -timeout 5 -mc all
|
|
alive
|
|
}
|
|
|
|
# query drives RequestQueryValue and the percent decoder, including escapes that
|
|
# do not decode, which must come back 400 and not half decoded.
|
|
fuzz_query() {
|
|
case_header query "query string values, valid and malformed escapes"
|
|
ffuf -u "$URL/search?q=FUZZ" -w "$WORDDIR/queries.txt" -t "$THREADS" -s -timeout 5 -mc all
|
|
ffuf -u "$URL/search?q=go&limit=FUZZ" -w "$WORDDIR/queries.txt" -t "$THREADS" -s -timeout 5 -mc all
|
|
case_header query "query value grown past the request header buffer"
|
|
ffuf -u "$URL/search?q=FUZZ" -w "$WORDDIR/long.txt" -t 4 -s -timeout 5 -mc all
|
|
alive
|
|
}
|
|
|
|
# form posts "application/x-www-form-urlencoded" bodies, the case the credential
|
|
# check answers 200 for and everything else 401.
|
|
fuzz_form() {
|
|
case_header form "urlencoded body pairs; 200 is the credential that works"
|
|
ffuf -u "$URL/login" -X POST -w "$WORDDIR/passwords.txt" \
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
-d 'user=admin&pass=FUZZ' -t "$THREADS" -s -timeout 5 -mc all -fc 401
|
|
case_header form "body grown past the form buffer, which may not grow"
|
|
ffuf -u "$URL/login" -X POST -w "$WORDDIR/long.txt" \
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
-d 'user=admin&pass=FUZZ' -t 4 -s -timeout 5 -mc all
|
|
case_header form "pair count driven past the form's fixed pair table"
|
|
ffuf -u "$URL/login?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=10&k=11&l=12&m=13&n=14&o=15&p=16&q=17" \
|
|
-X POST -w "$WORDDIR/passwords.txt" \
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
-d 'user=admin&pass=FUZZ' -t "$THREADS" -s -timeout 5 -mc all
|
|
alive
|
|
}
|
|
|
|
# cookie drives the Cookie header parser: quoting, valueless attributes and the
|
|
# separators the grammar splits on.
|
|
fuzz_cookie() {
|
|
case_header cookie "Cookie header values; 200 is the session that works"
|
|
ffuf -u "$URL/admin" -w "$WORDDIR/tokens.txt" -b 'session=FUZZ' \
|
|
-t "$THREADS" -s -timeout 5 -mc all -fc 403
|
|
case_header cookie "cookie grown past the cookie buffer"
|
|
ffuf -u "$URL/admin" -w "$WORDDIR/long.txt" -b 'session=FUZZ' -t 4 -s -timeout 5 -mc all
|
|
alive
|
|
}
|
|
|
|
# headers fuzzes a header field value and the field count, /echo handing back
|
|
# the header block as the parser stored it.
|
|
fuzz_headers() {
|
|
case_header headers "header field values echoed back through the parser"
|
|
ffuf -u "$URL/echo" -w "$WORDDIR/headers.txt" -H 'X-Fuzz: FUZZ' \
|
|
-t "$THREADS" -s -timeout 5 -mc all
|
|
case_header headers "header value grown past the request header buffer"
|
|
ffuf -u "$URL/echo" -w "$WORDDIR/long.txt" -H 'X-Fuzz: FUZZ' -t 4 -s -timeout 5 -mc all
|
|
alive
|
|
}
|
|
|
|
# multipart fuzzes the part header block: the filename parameter picks whether a
|
|
# part is streamed to a sink or discarded.
|
|
fuzz_multipart() {
|
|
case_header multipart "multipart part headers and filenames"
|
|
ffuf -u "$URL/upload" -X POST -w "$WORDDIR/names.txt" \
|
|
-H 'Content-Type: multipart/form-data; boundary=X' \
|
|
-d $'--X\r\nContent-Disposition: form-data; name="f"; filename="FUZZ"\r\n\r\ndata\r\n--X--\r\n' \
|
|
-t "$THREADS" -s -timeout 5 -mc all
|
|
case_header multipart "part header grown past the multipart buffer, expect 413"
|
|
ffuf -u "$URL/upload" -X POST -w "$WORDDIR/long.txt" \
|
|
-H 'Content-Type: multipart/form-data; boundary=X' \
|
|
-d $'--X\r\nContent-Disposition: form-data; name="f"; filename="FUZZ"\r\n\r\ndata\r\n--X--\r\n' \
|
|
-t 4 -s -timeout 5 -mc all
|
|
alive
|
|
}
|
|
|
|
# methods sends a method per registration and a few the server never names.
|
|
# "/echo" is registered without one, so any method reaches it; "/login" is
|
|
# POST only and everything else must 404 there.
|
|
fuzz_methods() {
|
|
case_header methods "registered, unregistered and extension methods"
|
|
printf 'GET\nPOST\nPUT\nDELETE\nPATCH\nHEAD\nOPTIONS\nTRACE\nPROPFIND\nBREW\n' >"$WORDDIR/methods.txt"
|
|
ffuf -u "$URL/echo" -w "$WORDDIR/methods.txt" -X FUZZ -t "$THREADS" -s -timeout 5 -mc all
|
|
ffuf -u "$URL/login" -w "$WORDDIR/methods.txt" -X FUZZ -t "$THREADS" -s -timeout 5 -mc all -fc 404
|
|
alive
|
|
}
|
|
|
|
# clusterbomb crosses a path wordlist with a query wordlist, so the two parsers
|
|
# are driven by unrelated inputs in the same request.
|
|
fuzz_clusterbomb() {
|
|
case_header clusterbomb "path and query fuzzed together, every combination"
|
|
ffuf -u "$URL/PATH?q=QUERY" -mode clusterbomb \
|
|
-w "$WORDDIR/paths.txt:PATH" -w "$WORDDIR/queries.txt:QUERY" \
|
|
-t "$THREADS" -s -timeout 5 -mc all -fc 404
|
|
alive
|
|
}
|
|
|
|
ALL=(paths recursion longpath query form cookie headers multipart methods clusterbomb)
|
|
|
|
main() {
|
|
command -v ffuf >/dev/null || {
|
|
echo "ffuf not found: go install github.com/ffuf/ffuf/v2@latest" >&2
|
|
exit 1
|
|
}
|
|
curl -s -o /dev/null --max-time 5 "$URL/health" || {
|
|
echo "no server at $URL: start it with 'go run ./examples/http-linux'" >&2
|
|
exit 1
|
|
}
|
|
local cases=("$@")
|
|
[ ${#cases[@]} -eq 0 ] && cases=("${ALL[@]}")
|
|
for c in "${cases[@]}"; do
|
|
"fuzz_$c" || { echo "unknown case: $c" >&2; exit 1; }
|
|
done
|
|
echo "all cases done, server still up"
|
|
}
|
|
|
|
main "$@"
|