Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 14 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 29 additions & 5 deletions ck/cmd/cachekeyhunter/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"flag"
"fmt"
"os"

Expand All @@ -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 <url> -w <headers> [-q <params>]\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),
Expand Down
21 changes: 17 additions & 4 deletions ck/internal/scan/baseline.go
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 22 additions & 18 deletions ck/internal/scan/variants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
})
}

Expand All @@ -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 {
Expand Down