Files
sif/internal/modules/favicon.go
T
c195e88453 feat(modules): add a range matcher for response size or status (#362)
the existing size matcher only checks exact-match against a list of values.
add a range matcher type with inclusive min/max bounds over either the
response/banner byte length (source: size, the default) or the http status
code (source: status). an unbounded or inverted range, or an unknown source,
is rejected at load rather than silently never firing at match time.

Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
2026-07-22 22:40:58 +00:00

100 lines
3.8 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package modules
import (
"fmt"
"math"
"strings"
"github.com/vmfunc/sif/internal/fingerprint"
)
// checkFaviconHash reports whether the body's shodan mmh3 hash matches any
// configured value. only the body (the icon) is hashed; part is ignored.
func checkFaviconHash(body string, want []int64) bool {
if len(want) == 0 {
return false
}
got := fingerprint.FaviconHash([]byte(body))
for _, w := range want {
if n, ok := normalizeFaviconHash(w); ok && n == got {
return true
}
}
return false
}
// normalizeFaviconHash folds a hash to the signed int32 shodan stores, accepting
// either 32-bit form so a signed or unsigned value pastes in as-is. out-of-range
// values are rejected so a stray number can't wrap into a false match.
func normalizeFaviconHash(v int64) (int32, bool) {
if v < math.MinInt32 || v > math.MaxUint32 {
return 0, false
}
return int32(uint32(v)), true //nolint:gosec // intentional 32-bit fold to shodan's signed form
}
// faviconEvidence gives the hash as evidence for a favicon-only finding, and
// nothing when a word/regex matcher is present so its body evidence stands.
func faviconEvidence(matchers []Matcher, body string) (string, bool) {
favicon := false
for i := range matchers {
switch matchers[i].Type {
case "word", "regex":
return "", false
case "favicon":
favicon = true
}
}
if !favicon {
return "", false
}
hash := fingerprint.FaviconHash([]byte(body))
if tech, ok := fingerprint.LookupFaviconTech(hash); ok {
return fmt.Sprintf("favicon mmh3=%d tech=%s", hash, tech), true
}
return fmt.Sprintf("favicon mmh3=%d", hash), true
}
// validateMatchers fails favicon matchers that would silently never fire (no
// hash, or one out of 32-bit range) and malformed range matchers at load
// rather than at match time.
func validateMatchers(matchers []Matcher) error {
for i := range matchers {
if matchers[i].Type == "favicon" {
if len(matchers[i].Hash) == 0 {
return fmt.Errorf("favicon matcher requires at least one hash")
}
for _, h := range matchers[i].Hash {
if _, ok := normalizeFaviconHash(h); !ok {
return fmt.Errorf("favicon hash %d out of range (use a signed int32 or unsigned uint32 value)", h)
}
}
}
if matchers[i].Type == "range" {
if matchers[i].Min == nil && matchers[i].Max == nil {
return fmt.Errorf("range matcher requires min or max")
}
if s := strings.ToLower(matchers[i].Source); s != "" && s != "size" && s != "status" {
return fmt.Errorf("range matcher source %q not supported (use size or status)", matchers[i].Source)
}
if matchers[i].Min != nil && matchers[i].Max != nil && *matchers[i].Min > *matchers[i].Max {
return fmt.Errorf("range matcher min %d greater than max %d", *matchers[i].Min, *matchers[i].Max)
}
}
}
return nil
}