Skip to content

Commit 8a0d8a5

Browse files
committed
add readme
1 parent e9d6f49 commit 8a0d8a5

7 files changed

Lines changed: 195 additions & 18 deletions

File tree

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type Config struct {
1919
URLList string
2020
Cookies string
2121
Auth string
22+
Domain string
2223
}
2324

2425
func ParseArgs() Config {
@@ -40,6 +41,7 @@ func ParseArgs() Config {
4041
auth := flag.String("auth", "", "Header Authorization (ex: Bearer <token>)")
4142
cookies := flag.String("cookies", "", "Cookies HTTP à inclure (ex: sessionid=abc123; token=xyz)")
4243
contentType := flag.String("contenttype", "application/json", "Type de contenu à envoyer")
44+
domain := flag.String("domain", "", "Nom de domaine pour collecter automatiquement des URLs avec gau/wayback/paramspider")
4345

4446
flag.Parse()
4547

@@ -56,5 +58,6 @@ func ParseArgs() Config {
5658
URLList: *urlList,
5759
Cookies: *cookies,
5860
Auth: *auth,
61+
Domain: *domain,
5962
}
6063
}

fuzz/fuzzer.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
func StartFuzzing(cfg config.Config) {
1616
allPayloads := payloads.GetAllPayloads(cfg.Category, cfg.Encodings, cfg.Wordlist)
1717

18-
// 🔄 Décodage des headers JSON en brut
1918
var rawHeaders map[string]string
2019
if cfg.Headers != "" {
2120
if err := json.Unmarshal([]byte(cfg.Headers), &rawHeaders); err != nil {
@@ -27,7 +26,6 @@ func StartFuzzing(cfg config.Config) {
2726
rawHeaders = make(map[string]string)
2827
}
2928

30-
// ✅ AJOUT cookies + auth
3129
if cfg.Cookies != "" {
3230
rawHeaders["Cookie"] = cfg.Cookies
3331
}
@@ -57,7 +55,6 @@ func StartFuzzing(cfg config.Config) {
5755
return
5856
}
5957

60-
// 📏 Baseline pour chaque URL
6158
sem := make(chan struct{}, cfg.Threads)
6259
var wg sync.WaitGroup
6360

@@ -83,7 +80,6 @@ func StartFuzzing(cfg config.Config) {
8380
utils.SetBaseline(baselineResp)
8481
fmt.Printf("🧬 Baseline enregistrée (%d chars, status %d)\n", len(baselineResp), baselineStatus)
8582

86-
// Fuzzing
8783
for _, payload := range allPayloads {
8884
wg.Add(1)
8985
sem <- struct{}{}
@@ -108,7 +104,6 @@ func StartFuzzing(cfg config.Config) {
108104
headers[k] = strings.Replace(v, "FUZZ", p, -1)
109105
}
110106

111-
// Détection point d'injection
112107
injectionPoint := "URL"
113108
if cfg.RawBody != "" && strings.Contains(cfg.RawBody, "FUZZ") {
114109
injectionPoint = "Body"

main.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,64 @@ package main
33
import (
44
"api-fuzzer/config"
55
"api-fuzzer/fuzz"
6+
"api-fuzzer/recon"
67
"fmt"
8+
"log"
9+
"os"
10+
"strings"
711
)
812

913
func main() {
1014
cfg := config.ParseArgs()
1115

12-
fmt.Println("🎯 Fuzzing:", cfg.URL)
16+
if cfg.URL == "" && cfg.URLList == "" && cfg.Domain != "" {
17+
fmt.Println("🔍 Domaine détecté, lancement de la reconnaissance avec gau + waybackurls + paramspider...")
18+
19+
gauUrls, err := recon.RunGau(cfg.Domain)
20+
if err != nil {
21+
log.Fatalf("❌ Erreur gau : %v", err)
22+
}
23+
24+
waybackUrls, err := recon.RunWaybackurls(cfg.Domain)
25+
if err != nil {
26+
log.Fatalf("❌ Erreur waybackurls : %v", err)
27+
}
28+
29+
paramspiderUrls, err := recon.RunParamSpider(cfg.Domain)
30+
if err != nil {
31+
log.Fatalf("❌ Erreur paramspider : %v", err)
32+
}
33+
34+
allUrls := recon.MergeAndDeduplicate(gauUrls, waybackUrls, paramspiderUrls)
35+
36+
if len(allUrls) == 0 {
37+
log.Fatalf("⚠️ Aucune URL récupérée pour %s", cfg.Domain)
38+
}
39+
40+
fuzzedUrls := recon.InjectFuzzInUrls(allUrls)
41+
42+
tmpFile := "fuzzed-urls.txt"
43+
err = os.WriteFile(tmpFile, []byte(strings.Join(fuzzedUrls, "\n")), 0644)
44+
if err != nil {
45+
log.Fatalf("❌ Erreur écriture fichier d'URL : %v", err)
46+
}
47+
48+
cfg.URLList = tmpFile
49+
fmt.Printf("✅ %d URLs fuzzées prêtes pour le fuzzing.\n", len(fuzzedUrls))
50+
}
51+
52+
fmt.Println("🎯 Fuzzing:")
53+
if cfg.URL != "" {
54+
fmt.Println("URL unique:", cfg.URL)
55+
} else if cfg.URLList != "" {
56+
fmt.Println("Liste d'URLs:", cfg.URLList)
57+
} else {
58+
log.Fatal("❌ Vous devez fournir soit -url, -urllist, ou -domain")
59+
}
60+
1361
fmt.Println("📦 Payload category:", cfg.Category)
1462
if cfg.Wordlist != "" {
15-
fmt.Println("📃 Using custom wordlist:", cfg.Wordlist)
63+
fmt.Println("📃 Utilisation de la wordlist personnalisée:", cfg.Wordlist)
1664
}
1765

1866
fuzz.StartFuzzing(cfg)

readme.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# API Fuzzer - Go
2+
3+
Un outil puissant et extensible de fuzzing API écrit en Go, conçu pour détecter automatiquement des vulnérabilités (XSS, LFI, RCE, IDOR...) sur vos endpoints.
4+
5+
---
6+
7+
## 🚀 Fonctionnalités
8+
9+
- Supporte plusieurs méthodes HTTP (GET, POST, PUT, etc.)
10+
- Injection intelligente de payloads dans les paramètres d'URL, les headers, ou le corps JSON
11+
- Reconnaissance automatique des endpoints avec intégration de `gau` (et bientôt `waybackurls`, `paramspider`)
12+
- Fuzzing multi-threads pour une rapidité optimale
13+
- Support des encodages variés (plain, url, base64...)
14+
- Personnalisation complète via wordlists, headers, cookies, authentification
15+
- Facile à intégrer dans une pipeline CI/CD
16+
- Sortie claire et détaillée pour faciliter l’analyse
17+
18+
---
19+
20+
## 📥 Installation
21+
22+
Vous devez avoir Go installé (>=1.18) et les outils suivants pour la reconnaissance (optionnels mais recommandés) :
23+
24+
- [gau](https://github.com/lc/gau)
25+
- [waybackurls](https://github.com/tomnomnom/waybackurls)
26+
- [ParamSpider](https://github.com/devanshbatham/ParamSpider)
27+
28+
Clonez le repo :
29+
30+
```bash
31+
git clone https://github.com/tonpseudo/api-fuzz.git
32+
cd api-fuzzer
33+
go build -o api-fuzzer

recon/recon.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package recon
2+
3+
import (
4+
"bufio"
5+
"net/url"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
func RunGau(domain string) ([]string, error) {
11+
cmd := exec.Command("gau", domain)
12+
stdout, err := cmd.StdoutPipe()
13+
if err != nil {
14+
return nil, err
15+
}
16+
if err := cmd.Start(); err != nil {
17+
return nil, err
18+
}
19+
20+
var urls []string
21+
scanner := bufio.NewScanner(stdout)
22+
for scanner.Scan() {
23+
line := scanner.Text()
24+
if strings.HasPrefix(line, "http") {
25+
urls = append(urls, line)
26+
}
27+
}
28+
cmd.Wait()
29+
return urls, nil
30+
}
31+
32+
func RunWaybackurls(domain string) ([]string, error) {
33+
cmd := exec.Command("waybackurls", domain)
34+
stdout, err := cmd.StdoutPipe()
35+
if err != nil {
36+
return nil, err
37+
}
38+
if err := cmd.Start(); err != nil {
39+
return nil, err
40+
}
41+
42+
var urls []string
43+
scanner := bufio.NewScanner(stdout)
44+
for scanner.Scan() {
45+
line := scanner.Text()
46+
if strings.HasPrefix(line, "http") {
47+
urls = append(urls, line)
48+
}
49+
}
50+
cmd.Wait()
51+
return urls, nil
52+
}
53+
54+
func RunParamSpider(domain string) ([]string, error) {
55+
cmd := exec.Command("paramspider", "-d", domain, "--exclude", "png,jpg,jpeg,gif,css,svg,woff,woff2")
56+
stdout, err := cmd.StdoutPipe()
57+
if err != nil {
58+
return nil, err
59+
}
60+
if err := cmd.Start(); err != nil {
61+
return nil, err
62+
}
63+
64+
var urls []string
65+
scanner := bufio.NewScanner(stdout)
66+
for scanner.Scan() {
67+
line := scanner.Text()
68+
line = strings.TrimSpace(line)
69+
if strings.HasPrefix(line, "http") && strings.Contains(line, "=") {
70+
urls = append(urls, line)
71+
}
72+
}
73+
cmd.Wait()
74+
return urls, nil
75+
}
76+
77+
func MergeAndDeduplicate(lists ...[]string) []string {
78+
unique := make(map[string]struct{})
79+
for _, list := range lists {
80+
for _, u := range list {
81+
unique[u] = struct{}{}
82+
}
83+
}
84+
var merged []string
85+
for u := range unique {
86+
merged = append(merged, u)
87+
}
88+
return merged
89+
}
90+
91+
func InjectFuzzInUrls(urls []string) []string {
92+
var fuzzed []string
93+
94+
for _, raw := range urls {
95+
parsed, err := url.Parse(raw)
96+
if err != nil {
97+
continue
98+
}
99+
q := parsed.Query()
100+
for param := range q {
101+
q.Set(param, "FUZZ")
102+
}
103+
parsed.RawQuery = q.Encode()
104+
fuzzed = append(fuzzed, parsed.String())
105+
}
106+
107+
return fuzzed
108+
}

utils/analyzer.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,20 @@ import (
66
"strings"
77
)
88

9-
// Liste d'erreurs classiques à détecter dans les réponses
109
var knownErrorPatterns = []string{
1110
"syntax error", "unexpected", "mysql_fetch", "SQL syntax", "unterminated",
1211
"NullReferenceException", "Traceback (most recent call last)",
1312
"Stacktrace", "Warning:", "Fatal error", "eval()", "System.IndexOutOfRangeException",
1413
"org.springframework", "You have an error in your SQL syntax", "MongoError",
1514
}
1615

17-
var baselineLength = -1 // longueur de la baseline (réponse sans injection)
16+
var baselineLength = -1
1817

19-
// Calcule si une réponse semble vulnérable selon plusieurs critères
2018
func IsResponseSuspicious(statusCode int, responseBody, injectedPayload string) (bool, string) {
2119
score := 0
2220
reasons := []string{}
2321
lower := strings.ToLower(responseBody)
2422

25-
// 1. Status code
2623
if statusCode >= 500 {
2724
score += 30
2825
reasons = append(reasons, "Status Code 5xx")
@@ -31,13 +28,11 @@ func IsResponseSuspicious(statusCode int, responseBody, injectedPayload string)
3128
reasons = append(reasons, "Status Code 4xx")
3229
}
3330

34-
// 2. Payload reflété
3531
if injectedPayload != "" && strings.Contains(responseBody, injectedPayload) {
3632
score += 30
3733
reasons = append(reasons, "Payload reflété (possible XSS/SSTI)")
3834
}
3935

40-
// 3. Erreurs connues
4136
for _, pattern := range knownErrorPatterns {
4237
if strings.Contains(lower, strings.ToLower(pattern)) {
4338
score += 40
@@ -46,7 +41,6 @@ func IsResponseSuspicious(statusCode int, responseBody, injectedPayload string)
4641
}
4742
}
4843

49-
// 4. Écart de longueur (si baseline connue)
5044
if baselineLength > 0 {
5145
diff := math.Abs(float64(len(responseBody) - baselineLength))
5246
if diff > 100 {
@@ -62,7 +56,6 @@ func IsResponseSuspicious(statusCode int, responseBody, injectedPayload string)
6256
return false, ""
6357
}
6458

65-
// Enregistre la longueur de la réponse de base (sans payload) pour la comparer ensuite
6659
func SetBaseline(responseBody string) {
6760
baselineLength = len(responseBody)
6861
}

utils/request.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ func SendRequest(method, target string, body map[string]interface{}, headers map
1717

1818
switch method {
1919
case "GET", "DELETE":
20-
// Encoder les params en query string
2120
params := url.Values{}
2221
for k, v := range body {
2322
params.Add(k, fmt.Sprintf("%v", v))
@@ -30,7 +29,6 @@ func SendRequest(method, target string, body map[string]interface{}, headers map
3029
}
3130
req, err = http.NewRequest(method, fullURL, nil)
3231
default:
33-
// Body JSON
3432
jsonData, _ := json.Marshal(body)
3533
req, err = http.NewRequest(method, target, bytes.NewBuffer(jsonData))
3634
req.Header.Set("Content-Type", "application/json")
@@ -41,7 +39,6 @@ func SendRequest(method, target string, body map[string]interface{}, headers map
4139
return 0, ""
4240
}
4341

44-
// ➕ Ajouter les headers personnalisés
4542
for key, value := range headers {
4643
req.Header.Set(key, value)
4744
}

0 commit comments

Comments
 (0)