From aadb659e8a8317be728d44fc444f357370fa41b6 Mon Sep 17 00:00:00 2001 From: cat <65914446+selimozcann@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:04:51 +0400 Subject: [PATCH] Handle header and query variants from wordlists --- README.md | 38 ++++++++++++--------------------- ck/cmd/cachekeyhunter/main.go | 34 ++++++++++++++++++++++++----- ck/internal/scan/baseline.go | 21 ++++++++++++++---- ck/internal/scan/variants.go | 40 +++++++++++++++++++---------------- 4 files changed, 82 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 93522ce..c13e880 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,26 @@ -# cachekeyhunter +# CacheKeyHunter -CacheKeyHunter is a Go-based HTTP cache key scanner used to explore how headers and query parameters influence caching behaviour. +CacheKeyHunter is a CLI tool for exploring how reverse proxies and CDNs build their cache keys. It sends crafted requests to observe how headers and query parameters influence cached responses. -## Getting started +## Features -Clone the repository and build the scanner binary: +- **Header poisoning detection** – test individual headers to see if they alter the cache key +- **Query poisoning detection** – append query parameters and watch for cache key differences -``` +## Requirements + +- Go 1.22+ + +## Installation + +```bash git clone https://github.com/CacheKeyHunter/cachekeyhunter.git cd cachekeyhunter/ck go build ./cmd/cachekeyhunter ``` -## Wordlists - -Version 2 expands the header and query parameter wordlists used for cache key testing: - -- `wordlists/headers.txt` – common headers that may affect cache keys, such as `x-forwarded-proto`, `origin`, and `accept-encoding`. -- `wordlists/params.txt` – query parameters like `cb`, `utm_source`, `session`, and others. - -These wordlists help reveal how different inputs change caching behaviour and expose potential cache-poisoning vectors. - ## Usage -Run the scanner with a wordlist: - -``` -./cachekeyhunter -u https://example.com/page -w wordlists/headers.txt +```bash +go run ./cmd/cachekeyhunter -u https://example.com/page -w wordlists/headers.txt -q wordlists/params.txt ``` - -This command targets the URL and iterates through the header wordlist. Swap in `wordlists/params.txt` to test query parameters instead. - -## v2 improvements - -Version 2 adds broader header and parameter coverage to aid testing for cache key discrepancies. diff --git a/ck/cmd/cachekeyhunter/main.go b/ck/cmd/cachekeyhunter/main.go index 92b0280..d90e2bb 100644 --- a/ck/cmd/cachekeyhunter/main.go +++ b/ck/cmd/cachekeyhunter/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "os" @@ -10,23 +11,46 @@ import ( ) func main() { - url := os.Args[1] - base, err := scan.GetBaseLİne(url) + url := flag.String("u", "", "Target URL") + flag.StringVar(url, "url", "", "Target URL") + headers := flag.String("w", "", "Path to header wordlist") + flag.StringVar(headers, "wordlist", "", "Path to header wordlist") + params := flag.String("q", "", "Path to query param wordlist") + flag.StringVar(params, "query", "", "Path to query param wordlist") + + flag.Parse() + + if *url == "" || *headers == "" { + fmt.Fprintf(os.Stderr, "Usage: %s -u -w [-q ]\n", os.Args[0]) + flag.PrintDefaults() + os.Exit(1) + } + + if *params != "" { + fmt.Printf("Starting cache key scan on %s using headers %s and params %s\n", *url, *headers, *params) + } else { + fmt.Printf("Starting cache key scan on %s using headers %s\n", *url, *headers) + } + + base, err := scan.GetBaseline(*url) if err != nil { fmt.Println("Baseline error:", err) return } - variants := scan.GenerateVariants() + variants := scan.GenerateHeaderVariants(*headers) + if *params != "" { + variants = append(variants, scan.GenerateQueryVariants(*params)...) + } for _, v := range variants { - sig, err := scan.DoVariant(url, v) + sig, err := scan.DoVariant(*url, v) if err != nil { continue } severity, detail := scan.Compare(base, sig) finding := types.Finding{ - URL: url, + URL: *url, Severity: severity, Detail: detail, Evidence: fmt.Sprintf("Variant: %s", v.Name), diff --git a/ck/internal/scan/baseline.go b/ck/internal/scan/baseline.go index b394e9a..9c8f08c 100644 --- a/ck/internal/scan/baseline.go +++ b/ck/internal/scan/baseline.go @@ -1,14 +1,27 @@ package scan import ( + urlpkg "net/url" + "github.com/selimozcann/cachekeyhunter/ck/internal/httpx" "github.com/selimozcann/cachekeyhunter/ck/internal/types" ) -func GetBaseLİne(url string) (types.Signals, error) { - return httpx.DoRequest(url, nil) +func GetBaseline(target string) (types.Signals, error) { + return httpx.DoRequest(target, nil) } -func DoVariant(url string, v types.Variant) (types.Signals, error) { - return httpx.DoRequest(url, v.Headers) +func DoVariant(target string, v types.Variant) (types.Signals, error) { + u, err := urlpkg.Parse(target) + if err != nil { + return types.Signals{}, err + } + if len(v.Query) > 0 { + q := u.Query() + for k, val := range v.Query { + q.Set(k, val) + } + u.RawQuery = q.Encode() + } + return httpx.DoRequest(u.String(), v.Headers) } diff --git a/ck/internal/scan/variants.go b/ck/internal/scan/variants.go index 0cbdd51..f3a7b7a 100644 --- a/ck/internal/scan/variants.go +++ b/ck/internal/scan/variants.go @@ -10,27 +10,14 @@ import ( "github.com/selimozcann/cachekeyhunter/ck/internal/types" ) -func GenerateVariants() []types.Variant { +func GenerateHeaderVariants(wordlistPath string) []types.Variant { var variants []types.Variant - headers, _ := loadLines("wordlists/headers.txt") - for _, h := range headers { + lines, _ := loadLines(wordlistPath) + for _, line := range lines { variants = append(variants, types.Variant{ - Name: fmt.Sprintf("%s: %s", h, constants.DefaultExampleDomain), - Headers: map[string]string{h: constants.DefaultExampleDomain}, - }) - } - - params, _ := loadLines("wordlists/params.txt") - for _, p := range params { - parts := strings.SplitN(p, "=", 2) - if len(parts) != 2 { - continue - } - key, val := parts[0], parts[1] - variants = append(variants, types.Variant{ - Name: fmt.Sprintf("Query %s=%s", key, val), - Query: map[string]string{key: val}, + Name: fmt.Sprintf("%s: %s", line, constants.DefaultExampleDomain), + Headers: map[string]string{line: constants.DefaultExampleDomain}, }) } @@ -52,6 +39,23 @@ func GenerateVariants() []types.Variant { return variants } +func GenerateQueryVariants(wordlistPath string) []types.Variant { + var variants []types.Variant + + lines, _ := loadLines(wordlistPath) + for _, line := range lines { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key, val := parts[0], parts[1] + variants = append(variants, types.Variant{ + Name: fmt.Sprintf("Query %s=%s", key, val), + Query: map[string]string{key: val}, + }) + } + } + return variants +} + func loadLines(path string) ([]string, error) { f, err := os.Open(path) if err != nil {