mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-01 02:24:47 -07:00
d16391186f
The attack field was parsed but never read, so every module ran the clusterbomb cross-product. Honor it: pitchfork pairs path[i] with payload[i] and stops at the shorter list, clusterbomb stays the default. Unknown attack values are rejected at parse time instead of silently ignored.
145 lines
5.1 KiB
Go
145 lines
5.1 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package modules
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/dropalldatabases/sif/internal/httpx"
|
|
)
|
|
|
|
func reqURLs(reqs []*httpRequest) []string {
|
|
urls := make([]string, len(reqs))
|
|
for i, r := range reqs {
|
|
urls[i] = r.URL
|
|
}
|
|
sort.Strings(urls)
|
|
return urls
|
|
}
|
|
|
|
func TestGenerateHTTPRequestsAttack(t *testing.T) {
|
|
const target = "http://t"
|
|
paths2 := []string{"{{BaseURL}}/a?x={{payload}}", "{{BaseURL}}/b?x={{payload}}"}
|
|
pay2 := []string{"1", "2"}
|
|
cross := []string{"http://t/a?x=1", "http://t/a?x=2", "http://t/b?x=1", "http://t/b?x=2"}
|
|
paired := []string{"http://t/a?x=1", "http://t/b?x=2"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
paths []string
|
|
payloads []string
|
|
attack string
|
|
want []string
|
|
}{
|
|
{"clusterbomb default crosses all", paths2, pay2, "", cross},
|
|
{"clusterbomb explicit crosses all", paths2, pay2, "clusterbomb", cross},
|
|
{"pitchfork pairs by index", paths2, pay2, "pitchfork", paired},
|
|
{"pitchfork stops at fewer payloads", append(paths2, "{{BaseURL}}/c?x={{payload}}"), pay2, "pitchfork", paired},
|
|
{"pitchfork stops at fewer paths", paths2, []string{"1", "2", "3"}, "pitchfork", paired},
|
|
{"attack is case insensitive", paths2, pay2, "Pitchfork", paired},
|
|
{"no payloads ignores attack", []string{"{{BaseURL}}/a", "{{BaseURL}}/b"}, nil, "pitchfork", []string{"http://t/a", "http://t/b"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := &HTTPConfig{Paths: tt.paths, Payloads: tt.payloads, Attack: tt.attack}
|
|
got := reqURLs(generateHTTPRequests(target, cfg))
|
|
want := append([]string(nil), tt.want...)
|
|
sort.Strings(want)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("attack %q:\n got %v\nwant %v", tt.attack, got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAttack(t *testing.T) {
|
|
for _, ok := range []string{"", "clusterbomb", "pitchfork", "Pitchfork", "CLUSTERBOMB"} {
|
|
if err := validateAttack(ok); err != nil {
|
|
t.Errorf("validateAttack(%q) = %v, want nil", ok, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"sniper", "batteringram", "bogus"} {
|
|
if err := validateAttack(bad); err == nil {
|
|
t.Errorf("validateAttack(%q) = nil, want error", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseAttackValidation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
write := func(name, body string) string {
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
good := write("good.yaml", "id: ok\ntype: http\nhttp:\n attack: pitchfork\n paths: [\"{{BaseURL}}/\"]\n")
|
|
if _, err := ParseYAMLModule(good); err != nil {
|
|
t.Fatalf("valid attack rejected: %v", err)
|
|
}
|
|
|
|
bad := write("bad.yaml", "id: bad\ntype: http\nhttp:\n attack: sniper\n paths: [\"{{BaseURL}}/\"]\n")
|
|
if _, err := ParseYAMLModule(bad); err == nil {
|
|
t.Fatal("invalid attack accepted")
|
|
}
|
|
}
|
|
|
|
// TestExecuteHTTPModulePitchfork drives the executor end to end and confirms
|
|
// pitchfork only fires the index-paired requests, not the full cross product.
|
|
func TestExecuteHTTPModulePitchfork(t *testing.T) {
|
|
var mu sync.Mutex
|
|
var hits []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
hits = append(hits, r.URL.Path+"?"+r.URL.RawQuery)
|
|
mu.Unlock()
|
|
_, _ = w.Write([]byte("ok"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
def := &YAMLModule{
|
|
ID: "pf",
|
|
Type: TypeHTTP,
|
|
HTTP: &HTTPConfig{
|
|
Attack: "pitchfork",
|
|
Paths: []string{"{{BaseURL}}/a?x={{payload}}", "{{BaseURL}}/b?x={{payload}}"},
|
|
Payloads: []string{"1", "2"},
|
|
Matchers: []Matcher{{Type: "word", Part: "body", Words: []string{"ok"}}},
|
|
},
|
|
}
|
|
|
|
opts := Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)}
|
|
if _, err := ExecuteHTTPModule(context.Background(), srv.URL, def, opts); err != nil {
|
|
t.Fatalf("ExecuteHTTPModule: %v", err)
|
|
}
|
|
|
|
mu.Lock()
|
|
got := append([]string(nil), hits...)
|
|
mu.Unlock()
|
|
sort.Strings(got)
|
|
want := []string{"/a?x=1", "/b?x=2"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("pitchfork hit %v, want %v (clusterbomb would also hit /a?x=2 and /b?x=1)", got, want)
|
|
}
|
|
}
|