/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ // Package fingerprint holds small response-fingerprinting primitives shared by // the scan checks and the module engine, so both compute identical values. package fingerprint import ( "encoding/base64" "strings" "github.com/twmb/murmur3" ) // b64LineLen is python's base64.encodebytes line width. mmh3/shodan hash the // chunked base64 (newline every 76 chars, trailing newline), so we must wrap at // exactly this width to land on the same hash. const b64LineLen = 76 // FaviconHash computes shodan's favicon hash: murmur3 32-bit over the python // base64.encodebytes encoding of the raw icon (newline every 76 chars plus a // trailing newline), reinterpreted as a signed int32 (both load-bearing, golden-pinned). func FaviconHash(data []byte) int32 { encoded := encodeFaviconBase64(data) return int32(murmur3.Sum32(encoded)) //nolint:gosec // shodan stores the signed reinterpretation on purpose } // faviconTech maps a known shodan favicon hash to the tech that ships it. // these are stable default icons for panels/frameworks/c2; a hit is a strong // fingerprint. kept small on purpose - high-signal defaults, not an exhaustive db. var faviconTech = map[int32]string{ 116323821: "Apache Tomcat", 81586312: "Spring Boot (default whitelabel)", -235701012: "Jenkins", -1255347784: "GitLab", 1278322581: "Grafana", 743365239: "Kibana", -1462443472: "phpMyAdmin", 999357577: "Cobalt Strike (default beacon)", -1521704893: "Metasploit", -1893514588: "Gitea", } // LookupFaviconTech returns the tech that ships the given shodan favicon hash and // whether the hash is known. func LookupFaviconTech(hash int32) (string, bool) { tech, ok := faviconTech[hash] return tech, ok } // encodeFaviconBase64 mirrors python's base64.encodebytes: standard base64 with // a newline inserted every 76 output characters and a trailing newline. this is // the exact byte stream shodan feeds to mmh3, so it must match byte-for-byte. func encodeFaviconBase64(data []byte) []byte { raw := base64.StdEncoding.EncodeToString(data) var b strings.Builder // final size: the base64 body plus one '\n' per (full or partial) 76-char // line. preallocate so the builder never regrows mid-loop. b.Grow(len(raw) + len(raw)/b64LineLen + 1) for i := 0; i < len(raw); i += b64LineLen { end := i + b64LineLen if end > len(raw) { end = len(raw) } b.WriteString(raw[i:end]) b.WriteByte('\n') } return []byte(b.String()) }