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
81 changes: 81 additions & 0 deletions docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,42 @@ http:

each payload creates a separate request for each path.

payloads can also be **named sets**, one per fuzzing position, which lets a
single module vary more than one place in the request at once. write a mapping
instead of a list, and reference each set by its name:

```yaml
http:
paths:
- "{{BaseURL}}/?user={{user}}&role={{role}}"

payloads:
user:
- "admin"
- "root"
role:
- "1"
- "2"
```

set order is the order you declare them, which is what `attack` pairs on. a
plain list is still accepted and desugars to one set named `payload`, so
`{{payload}}` keeps working unchanged.

a set whose value is a single string is read from that file, one payload per
line, so a large wordlist does not have to live inside the module:

```yaml
payloads:
user: "/usr/share/sif/wordlists/users.txt"
```

requests are generated lazily, one combination at a time, so a module crossing
several large sets does not build the whole product in memory first.
`-fuzz-max-requests` (default 25000, 0 for unlimited) caps how many requests one
fuzzing module may send per target, so a combinatorial blowup stops itself
instead of running until the scan is killed.

#### attack

how paths and payloads combine into requests.
Expand All @@ -126,6 +162,9 @@ http:

- `clusterbomb` (default) - every path is tried with every payload
- `pitchfork` - path and payload are paired by index, stopping at the shorter list
- `batteringram` - one value at a time from the first set is broadcast into every
named position, stopping at that set's length. use it when the same value has
to appear in several places of one request at once

#### wordlist

Expand Down Expand Up @@ -310,6 +349,48 @@ both 32-bit forms are accepted, so values from shodan or any favicon-hash tool
drop in without conversion. pair it with a `status: 200` matcher so an error
page served for `/favicon.ico` is not hashed. a finding fires when the body
hashes to any listed value.
### dsl matcher

evaluate one or more boolean expressions against the response. the syntax and
variable names are nuclei's, so an expression written for a nuclei template
pastes in unchanged.

```yaml
matchers:
- type: dsl
dsl:
- "status_code == 200 && contains(body, 'admin')"
- "content_length > 1024"
```

the variables bound for every expression:

| variable | type | value |
|----------|------|-------|
| `status_code` | int | response status code |
| `body` | string | response body, after the 5 MB cap |
| `content_length` | int | length of `body` in bytes |
| `header` / `all_headers` | string | the response headers, one `Name: value` per line |
| `duration` | float | round-trip time in seconds |
| `host` | string | `host[:port]` of the request url |

named extractor values are bound too, so an expression can test something an
extractor pulled out of the same response; in a request chain it also sees the
variables earlier steps extracted. an extractor whose name collides with a
builtin shadows it, matching nuclei's mutation order.

helper functions are an allowlist, not a blocklist: string inspection and
transforms, `regex`/`regex_all`/`regex_any`, base64/hex/url/html encode and
decode, `md5`/`sha1`/`sha256`/`mmh3`, and numeric conversion. anything else
fails at load, so a dependency bump cannot quietly introduce a helper that reads
files, makes its own requests, or allocates without bound. expressions are also
capped at 4096 bytes.

expressions are compiled and checked when the module loads, so a typo fails the
module up front instead of silently never matching. at match time an expression
that errors or yields a non-boolean counts as a miss. multiple expressions
combine with AND by default, or with `condition: or`.

### combining matchers

multiple matchers are combined with AND logic by default.
Expand Down
16 changes: 16 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ sizes: `small`, `medium`, `large`
./sif -u https://example.com -dirlist medium
```

#### fuzzing budget

`-fuzz-max-requests <n>` - cap the requests a single fuzzing module may send per
target (default 25000, `0` for unlimited). a module crossing several payload sets
multiplies them out, so this is the stop that keeps one module from consuming the
whole scan.

```bash
./sif -u https://example.com -all-modules -fuzz-max-requests 5000
```

`-fuzz-global-max-requests <n>` - total fuzz requests across every module and
every target in the run (default 100000, `0` for unlimited). the per-module cap
above bounds one module; this bounds the whole scan, so a target list full of
fuzzing modules cannot multiply past it.

#### response filters

modern apps serve a catch-all 200 for unknown routes, so a naive scan reports
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ require (
github.com/charmbracelet/log v1.0.0
github.com/gocolly/colly/v2 v2.3.0
github.com/likexian/whois v1.15.7
github.com/projectdiscovery/dsl v0.8.20
github.com/projectdiscovery/goflags v0.1.74
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
github.com/projectdiscovery/nuclei/v3 v3.11.0
github.com/projectdiscovery/retryabledns v1.0.115
github.com/projectdiscovery/utils v0.11.1
Expand Down Expand Up @@ -272,7 +274,6 @@ require (
github.com/projectdiscovery/blackrock v0.0.1 // indirect
github.com/projectdiscovery/cdncheck v1.2.42 // indirect
github.com/projectdiscovery/clistats v0.1.4 // indirect
github.com/projectdiscovery/dsl v0.8.20 // indirect
github.com/projectdiscovery/fastdialer v0.5.11 // indirect
github.com/projectdiscovery/fasttemplate v0.0.2 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
Expand All @@ -282,7 +283,6 @@ require (
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d // indirect
github.com/projectdiscovery/gologger v1.1.71 // indirect
github.com/projectdiscovery/gostruct v0.0.2 // indirect
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e // indirect
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 // indirect
github.com/projectdiscovery/hmap v0.0.101 // indirect
github.com/projectdiscovery/httpx v1.9.0 // indirect
Expand Down
142 changes: 73 additions & 69 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,75 +21,77 @@ import (
)

type Settings struct {
Dirlist string
DirMatchCodes string // -mc dirlist: status codes to keep
DirFilterCodes string // -fc dirlist: status codes to drop
DirFilterSizes string // -fs dirlist: body sizes to drop
DirFilterWords string // -fw dirlist: word counts to drop
DirFilterRegex string // -fr dirlist: regex; body match drops response
Calibrate bool // -ac auto-calibrate the soft-404 baseline (dirlist, sql)
DirWordlist string // -w dirlist: custom wordlist (file path or url)
DirExtensions string // -e dirlist: extensions appended to each word
Dnslist string
Resolvers string // -resolvers dnslist: comma list overriding the bundled pool
Debug bool
LogDir string
NoScan bool
Ports string
Dorking bool
Git bool
Whois bool
Threads int
Concurrency int
Nuclei bool
JavaScript bool
Timeout time.Duration
URLs goflags.StringSlice
File string
ApiMode bool
Template string
CMS bool
Headers bool
SecurityHeaders bool
CloudStorage bool
SubdomainTakeover bool
Shodan bool
SecurityTrails bool
SQL bool
LFI bool
JWT bool
OpenAPI bool
Favicon bool
CORS bool
Redirect bool
XSS bool
Framework bool
Crawl bool
CrawlDepth int
TLSCert bool
TLSCertPort int
Passive bool
Probe bool
SARIF string // path to write a sarif 2.1.0 report to ("" = off)
Markdown string // path to write a markdown report to ("" = off)
JSONReport string // path to write a json findings report to ("" = off)
Silent bool // route chrome to stderr, print one finding per line to stdout
Diff bool // surface only findings added/removed vs the last snapshot
Store string // snapshot dir for diff mode ("" = default state dir)
Modules string // Comma-separated list of module IDs to run
ModuleTags string // Run modules matching these tags
AllModules bool // Run all loaded modules
ListModules bool // List available modules and exit
Proxy string
Header goflags.StringSlice // custom request headers ("Key: Value")
Cookie string
RateLimit int
MaxRetries int // -max-retries: retries on 429/503 (0 = off)
Notify bool // -notify: ship findings to configured providers
NotifySeverity string // -notify-severity: minimum severity to send (info..critical)
NotifyConfig string // -notify-config: path to a notify-compatible yaml file
ConfigFile string // -config: path to a yaml config file ("" = default ~/.config/sif/config.yaml)
Profile string // -profile: named profile overlay from the config file
Dirlist string
DirMatchCodes string // -mc dirlist: status codes to keep
DirFilterCodes string // -fc dirlist: status codes to drop
DirFilterSizes string // -fs dirlist: body sizes to drop
DirFilterWords string // -fw dirlist: word counts to drop
DirFilterRegex string // -fr dirlist: regex; body match drops response
Calibrate bool // -ac auto-calibrate the soft-404 baseline (dirlist, sql)
DirWordlist string // -w dirlist: custom wordlist (file path or url)
DirExtensions string // -e dirlist: extensions appended to each word
Dnslist string
Resolvers string // -resolvers dnslist: comma list overriding the bundled pool
Debug bool
LogDir string
NoScan bool
Ports string
Dorking bool
Git bool
Whois bool
Threads int
Concurrency int
Nuclei bool
JavaScript bool
Timeout time.Duration
URLs goflags.StringSlice
File string
ApiMode bool
Template string
CMS bool
Headers bool
SecurityHeaders bool
CloudStorage bool
SubdomainTakeover bool
Shodan bool
SecurityTrails bool
SQL bool
LFI bool
JWT bool
OpenAPI bool
Favicon bool
CORS bool
Redirect bool
XSS bool
Framework bool
Crawl bool
CrawlDepth int
TLSCert bool
TLSCertPort int
Passive bool
Probe bool
SARIF string // path to write a sarif 2.1.0 report to ("" = off)
Markdown string // path to write a markdown report to ("" = off)
JSONReport string // path to write a json findings report to ("" = off)
Silent bool // route chrome to stderr, print one finding per line to stdout
Diff bool // surface only findings added/removed vs the last snapshot
Store string // snapshot dir for diff mode ("" = default state dir)
Modules string // Comma-separated list of module IDs to run
ModuleTags string // Run modules matching these tags
AllModules bool // Run all loaded modules
ListModules bool // List available modules and exit
Proxy string
Header goflags.StringSlice // custom request headers ("Key: Value")
Cookie string
RateLimit int
MaxRetries int // -max-retries: retries on 429/503 (0 = off)
FuzzMaxRequests int // -fuzz-max-requests: cap per fuzzing module per target (0 = unlimited)
FuzzGlobalMaxRequests int // -fuzz-global-max-requests: scan-wide cap shared by every fuzzing module (0 = unlimited)
Notify bool // -notify: ship findings to configured providers
NotifySeverity string // -notify-severity: minimum severity to send (info..critical)
NotifyConfig string // -notify-config: path to a notify-compatible yaml file
ConfigFile string // -config: path to a yaml config file ("" = default ~/.config/sif/config.yaml)
Profile string // -profile: named profile overlay from the config file
}

// minThreads is the floor for the worker count. Threads feeds wg.Add across the
Expand Down Expand Up @@ -194,6 +196,8 @@ func registerFlags(settings *Settings) *goflags.FlagSet {
flagSet.StringVar(&settings.Cookie, "cookie", "", "Cookie header to send with every request"),
flagSet.IntVar(&settings.RateLimit, "rate-limit", 0, "Max requests per second (0 = unlimited)"),
flagSet.IntVar(&settings.MaxRetries, "max-retries", 2, "Retries on 429/503 with Retry-After backoff (0 = off)"),
flagSet.IntVar(&settings.FuzzMaxRequests, "fuzz-max-requests", 25000, "Max requests a single fuzzing module may send per target (0 = unlimited)"),
flagSet.IntVar(&settings.FuzzGlobalMaxRequests, "fuzz-global-max-requests", 100000, "Max total fuzz requests across every module and target in the scan (0 = unlimited)"),
)

flagSet.CreateGroup("output", "Output",
Expand Down
8 changes: 4 additions & 4 deletions internal/modules/attack_modes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestGenerateHTTPRequestsAttack(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &HTTPConfig{Paths: tt.paths, Payloads: tt.payloads, Attack: tt.attack}
cfg := &HTTPConfig{Paths: tt.paths, Payloads: legacyPayloads(tt.payloads), Attack: tt.attack}
reqs, err := generateHTTPRequests(target, cfg)
if err != nil {
t.Fatalf("generateHTTPRequests: %v", err)
Expand All @@ -75,12 +75,12 @@ func TestGenerateHTTPRequestsAttack(t *testing.T) {
}

func TestValidateAttack(t *testing.T) {
for _, ok := range []string{"", "clusterbomb", "pitchfork", "Pitchfork", "CLUSTERBOMB"} {
for _, ok := range []string{"", "clusterbomb", "pitchfork", "Pitchfork", "CLUSTERBOMB", "batteringram", "BatteringRam"} {
if err := validateAttack(ok); err != nil {
t.Errorf("validateAttack(%q) = %v, want nil", ok, err)
}
}
for _, bad := range []string{"sniper", "batteringram", "bogus"} {
for _, bad := range []string{"sniper", "bogus"} {
if err := validateAttack(bad); err == nil {
t.Errorf("validateAttack(%q) = nil, want error", bad)
}
Expand Down Expand Up @@ -127,7 +127,7 @@ func TestExecuteHTTPModulePitchfork(t *testing.T) {
HTTP: &HTTPConfig{
Attack: "pitchfork",
Paths: []string{"{{BaseURL}}/a?x={{payload}}", "{{BaseURL}}/b?x={{payload}}"},
Payloads: []string{"1", "2"},
Payloads: legacyPayloads([]string{"1", "2"}),
Matchers: []Matcher{{Type: "word", Part: "body", Words: []string{"ok"}}},
},
}
Expand Down
Loading
Loading