Files
fscan/webscan/fingerprint/enhanced.go
T
ZacharyZcR 6a1636112f fix+perf: 修复10个bug & 10项性能优化
Bug修复:
- clustersend CEL结果判断从字符串比较改为类型断言
- Nuclei DSL matcher安全降级为false避免误报
- clusterpoc发现漏洞后返回true修正语义
- reverseCheck加10s超时防止ceye API阻塞
- doSearch/bmatches正则编译结果缓存到sync.Map
- evalset CEL求值失败时存空字符串而非原始表达式
- CEL wait()函数加nil Reverse指针检查防panic
- MongoDB readMongoMsg应用timeout参数设置读超时
- TXTWriter.Close确保Sync失败后仍调用file.Close

性能优化:
- 指纹regex缓存从RWMutex+map改为sync.Map消除锁竞争
- CaseInsensitive指纹词加载时预小写化避免匹配时分配
- 版本提取FindAllStringSubmatch限制返回数量
- i18n.Tr用strconv.Itoa替代Sprintf减少分配
- POC加载用atomic.Bool+DCLP消除热路径锁
- 结果缓冲map预分配容量减少rehash
- HTTP连接池参数随并发数动态调整
- getRuleHash去除反射+Headers排序保证确定性dedup
- POC并发加载用channel替代Mutex收集结果
2026-06-13 18:46:14 +08:00

515 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fingerprint
import (
"crypto/md5" //nolint:gosec
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"github.com/shadow1ng/fscan/common/i18n"
)
//go:embed web_fingerprint_v4.json
var fingerprintHubData []byte
// EnhancedFingerprint FingerprintHub增强指纹结构
type EnhancedFingerprint struct {
ID string `json:"id"`
Info struct {
Name string `json:"name"`
Author string `json:"author"`
Tags string `json:"tags"`
Severity string `json:"severity"`
Metadata map[string]interface{} `json:"metadata"`
} `json:"info"`
HTTP []struct {
Method string `json:"method"`
Path []string `json:"path"`
Matchers []struct {
Type string `json:"type"`
Words []string `json:"words"`
Regex []string `json:"regex"`
Hash []string `json:"hash"` // favicon hash
Part string `json:"part"` // header, body
CaseInsensitive bool `json:"case-insensitive"`
Condition string `json:"condition"` // and, or
} `json:"matchers"`
} `json:"http"`
}
// EnhancedFingerprintDB 增强指纹数据库
type EnhancedFingerprintDB struct {
Fingerprints []*EnhancedFingerprint
regexCache sync.Map // pattern → *regexp.Regexp,无锁并发安全
}
var (
enhancedDB *EnhancedFingerprintDB
enhancedDBOnce sync.Once // 保证只初始化一次
)
// LoadEnhancedFingerprints 加载增强指纹库
func LoadEnhancedFingerprints() error {
var fps []*EnhancedFingerprint
if err := json.Unmarshal(fingerprintHubData, &fps); err != nil {
return fmt.Errorf("%s: %w", i18n.GetText("fingerprint_enhanced_parse_failed"), err)
}
// 预处理:CaseInsensitive 的 matcher 预先小写化 Words,避免匹配时重复分配
for _, fp := range fps {
for hi := range fp.HTTP {
for mi := range fp.HTTP[hi].Matchers {
m := &fp.HTTP[hi].Matchers[mi]
if m.CaseInsensitive && m.Type == "word" {
for wi, w := range m.Words {
m.Words[wi] = strings.ToLower(w)
}
}
}
}
}
enhancedDB = &EnhancedFingerprintDB{
Fingerprints: fps,
}
return nil
}
// fingerprintMatch 指纹匹配结果(带优先级)
type fingerprintMatch struct {
Name string
Priority int // 优先级分数,越高越优先
}
// MatchEnhancedFingerprints 匹配增强指纹(并发版本,结果按优先级排序)
func MatchEnhancedFingerprints(body []byte, headers string, favicon FaviconHashes) []string {
// 使用 sync.Once 保证只初始化一次,线程安全
enhancedDBOnce.Do(func() {
_ = LoadEnhancedFingerprints() // 忽略错误,enhancedDB 为 nil 时下面会返回 nil
})
if enhancedDB == nil || len(enhancedDB.Fingerprints) == 0 {
return nil
}
bodyStr := string(body)
fingerprints := enhancedDB.Fingerprints
total := len(fingerprints)
// 根据 CPU 核心数决定并发数,但不超过指纹数量
workers := runtime.NumCPU()
if workers > total {
workers = total
}
if workers < 1 {
workers = 1
}
// 每个 worker 处理的指纹数量
chunkSize := (total + workers - 1) / workers
// 结果收集 channel(带优先级信息)
resultCh := make(chan fingerprintMatch, total)
var wg sync.WaitGroup
// 启动 worker
for i := 0; i < workers; i++ {
start := i * chunkSize
end := start + chunkSize
if end > total {
end = total
}
if start >= total {
break
}
wg.Add(1)
go func(fps []*EnhancedFingerprint) {
defer wg.Done()
for _, fp := range fps {
if len(fp.HTTP) == 0 {
continue
}
httpRule := fp.HTTP[0]
for _, matcher := range httpRule.Matchers {
if matchMatcher(matcher, bodyStr, headers, favicon) {
resultCh <- fingerprintMatch{
Name: fp.Info.Name,
Priority: calcPriority(fp, matcher.Type),
}
break
}
}
}
}(fingerprints[start:end])
}
// 等待所有 worker 完成后关闭 channel
go func() {
wg.Wait()
close(resultCh)
}()
// 收集结果
var matches []fingerprintMatch
for m := range resultCh {
matches = append(matches, m)
}
// 按优先级排序(降序)
sort.Slice(matches, func(i, j int) bool {
if matches[i].Priority != matches[j].Priority {
return matches[i].Priority > matches[j].Priority
}
return matches[i].Name < matches[j].Name // 同优先级按名称排序
})
// 提取名称
result := make([]string, len(matches))
for i, m := range matches {
result[i] = m.Name
}
return result
}
// calcPriority 计算指纹优先级分数
func calcPriority(fp *EnhancedFingerprint, matcherType string) int {
priority := 0
// 匹配类型权重(favicon 最精确)
switch matcherType {
case "favicon":
priority += 100
case "regex":
priority += 50
case "word":
priority += 30
}
// verified 状态加分
if fp.Info.Metadata != nil {
if verified, ok := fp.Info.Metadata["verified"].(bool); ok && verified {
priority += 20
}
}
return priority
}
// matchMatcher 匹配单个matcher
func matchMatcher(matcher struct {
Type string `json:"type"`
Words []string `json:"words"`
Regex []string `json:"regex"`
Hash []string `json:"hash"`
Part string `json:"part"`
CaseInsensitive bool `json:"case-insensitive"`
Condition string `json:"condition"`
}, body, headers string, favicon FaviconHashes) bool {
switch matcher.Type {
case "word":
return matchWords(matcher, body, headers)
case "regex":
return matchRegex(matcher, body, headers)
case "favicon":
return matchFavicon(matcher, favicon)
default:
return false
}
}
// matchWords 匹配关键词
func matchWords(matcher struct {
Type string `json:"type"`
Words []string `json:"words"`
Regex []string `json:"regex"`
Hash []string `json:"hash"`
Part string `json:"part"`
CaseInsensitive bool `json:"case-insensitive"`
Condition string `json:"condition"`
}, body, headers string) bool {
// 确定匹配目标
target := body
if matcher.Part == "header" {
target = headers
}
// CaseInsensitive: target 转小写一次,Words 已在加载时预处理(直接调用时也兼容未预处理的词)
if matcher.CaseInsensitive {
target = strings.ToLower(target)
}
// 默认condition为or
isAnd := matcher.Condition == "and"
matchCount := 0
for _, searchWord := range matcher.Words {
if matcher.CaseInsensitive {
searchWord = strings.ToLower(searchWord)
}
if strings.Contains(target, searchWord) {
if !isAnd {
// OR条件:匹配任一即可
return true
}
matchCount++
} else if isAnd {
// AND条件:任一不匹配即失败
return false
}
}
// AND条件:全部匹配
return isAnd && matchCount == len(matcher.Words)
}
// matchRegex 匹配正则表达式
func matchRegex(matcher struct {
Type string `json:"type"`
Words []string `json:"words"`
Regex []string `json:"regex"`
Hash []string `json:"hash"`
Part string `json:"part"`
CaseInsensitive bool `json:"case-insensitive"`
Condition string `json:"condition"`
}, body, headers string) bool {
// 确定匹配目标
target := body
if matcher.Part == "header" {
target = headers
}
// 默认condition为or
isAnd := matcher.Condition == "and"
for _, pattern := range matcher.Regex {
// CaseInsensitive 正则需要前缀
cacheKey := pattern
if matcher.CaseInsensitive {
cacheKey = "(?i)" + pattern
}
// 从 sync.Map 缓存获取或编译正则
var re *regexp.Regexp
if cached, ok := enhancedDB.regexCache.Load(cacheKey); ok {
re = cached.(*regexp.Regexp)
} else {
compiled, err := regexp.Compile(cacheKey)
if err != nil {
continue
}
actual, _ := enhancedDB.regexCache.LoadOrStore(cacheKey, compiled)
re = actual.(*regexp.Regexp)
}
// 确保 re 不为 nil(防止并发场景下的 nil panic)
if re != nil && re.MatchString(target) {
if !isAnd {
return true
}
} else if isAnd {
return false
}
}
// AND条件需要全部匹配
return isAnd && len(matcher.Regex) > 0
}
// matchFavicon 匹配favicon hash(同时支持 mmh3 和 MD5 格式)
func matchFavicon(matcher struct {
Type string `json:"type"`
Words []string `json:"words"`
Regex []string `json:"regex"`
Hash []string `json:"hash"`
Part string `json:"part"`
CaseInsensitive bool `json:"case-insensitive"`
Condition string `json:"condition"`
}, favicon FaviconHashes) bool {
if favicon.MMH3 == "" && favicon.MD5 == "" {
return false
}
for _, hash := range matcher.Hash {
// 同时匹配 mmh3(数字格式)和 MD5(十六进制格式)
if hash == favicon.MMH3 || hash == favicon.MD5 {
return true
}
}
return false
}
// FaviconHashes 包含 mmh3 和 MD5 两种格式的 hash
type FaviconHashes struct {
MMH3 string // Shodan/FOFA 风格: 有符号32位整数
MD5 string // 传统 MD5 十六进制
}
// VersionInfo 版本提取结果
type VersionInfo struct {
Name string // 产品名称
Version string // 版本号
}
// 通用版本提取正则(预编译)
var versionExtractors = []struct {
pattern *regexp.Regexp
name string // 产品名称(空表示从匹配中提取)
}{
// Server header: nginx/1.18.0, Apache/2.4.41, IIS/10.0
{regexp.MustCompile(`(?i)(?:^|[\s,])(?P<name>nginx|apache|iis|lighttpd|openresty|tengine)[/\s]?(?P<ver>[\d.]+)`), ""},
// X-Powered-By: PHP/7.4.3, ASP.NET/4.0
{regexp.MustCompile(`(?i)(?:x-powered-by[:\s]*)?(?P<name>php|asp\.net|express|servlet)[/\s]?(?P<ver>[\d.]+)`), ""},
// Generator meta: WordPress 6.0, Drupal 9.0
{regexp.MustCompile(`(?i)(?:generator|powered[\s-]*by)["\s:]*(?P<name>wordpress|drupal|joomla|typo3|hugo|jekyll|ghost)[\s/]*(?P<ver>[\d.]+)?`), ""},
// jQuery/Vue/React
{regexp.MustCompile(`(?i)(?P<name>jquery|vue|react|angular)[\s./-]*(?:v|version)?[\s]*(?P<ver>\d+(?:\.\d+)+)`), ""},
// 通用 version= 或 ver= 模式
{regexp.MustCompile(`(?i)(?P<name>[a-z][\w-]*?)[\s_-]*(?:version|ver)[=:\s"']*(?P<ver>[\d]+(?:\.[\d]+)+)`), ""},
// Tomcat, WebLogic, WebSphere
{regexp.MustCompile(`(?i)(?P<name>tomcat|weblogic|websphere|jetty|jboss|wildfly)[/\s-]*(?P<ver>[\d.]+)`), ""},
// 数据库版本
{regexp.MustCompile(`(?i)(?P<name>mysql|mariadb|postgresql|mongodb|redis|elasticsearch)[/\s-]*(?P<ver>[\d.]+)`), ""},
// OpenSSL, OpenSSH
{regexp.MustCompile(`(?i)(?P<name>openssl|openssh)[/\s-]*(?P<ver>[\d.]+[a-z]?)`), ""},
}
// ExtractVersions 从 HTTP 响应中提取软件版本信息
func ExtractVersions(body string, headers string) []VersionInfo {
content := headers + "\n" + body
var results []VersionInfo
seen := make(map[string]struct{})
for _, extractor := range versionExtractors {
matches := extractor.pattern.FindAllStringSubmatch(content, 5)
for _, match := range matches {
var name, version string
// 提取命名捕获组
for i, groupName := range extractor.pattern.SubexpNames() {
if i > 0 && i < len(match) {
switch groupName {
case "name":
name = strings.ToLower(match[i])
case "ver":
version = match[i]
}
}
}
// 跳过空结果或已存在的
if name == "" || version == "" {
continue
}
key := name + ":" + version
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
results = append(results, VersionInfo{
Name: name,
Version: version,
})
}
}
return results
}
// CalculateFaviconHashes 计算 favicon 的 hash(同时返回 mmh3 和 MD5
func CalculateFaviconHashes(data []byte) FaviconHashes {
if len(data) == 0 {
return FaviconHashes{}
}
// mmh3: base64编码后计算(Shodan/FOFA 标准)
b64 := base64.StdEncoding.EncodeToString(data)
mmh3Hash := mmh3Hash32([]byte(b64))
// MD5: 直接计算原始数据
//nolint:gosec
md5Hash := md5.Sum(data)
return FaviconHashes{
MMH3: fmt.Sprintf("%d", mmh3Hash),
MD5: fmt.Sprintf("%x", md5Hash),
}
}
// mmh3Hash32 计算 MurmurHash3 32位 hash(有符号整数)
func mmh3Hash32(data []byte) int32 {
const (
c1 uint32 = 0xcc9e2d51
c2 uint32 = 0x1b873593
seed uint32 = 0
)
length := len(data)
nblocks := length / 4
h1 := seed
// 处理4字节块
for i := 0; i < nblocks; i++ {
k1 := uint32(data[i*4]) | uint32(data[i*4+1])<<8 |
uint32(data[i*4+2])<<16 | uint32(data[i*4+3])<<24
k1 *= c1
k1 = (k1 << 15) | (k1 >> 17)
k1 *= c2
h1 ^= k1
h1 = (h1 << 13) | (h1 >> 19)
h1 = h1*5 + 0xe6546b64
}
// 处理剩余字节
tail := data[nblocks*4:]
var k1 uint32
switch len(tail) {
case 3:
k1 ^= uint32(tail[2]) << 16
fallthrough
case 2:
k1 ^= uint32(tail[1]) << 8
fallthrough
case 1:
k1 ^= uint32(tail[0])
k1 *= c1
k1 = (k1 << 15) | (k1 >> 17)
k1 *= c2
h1 ^= k1
}
// 最终混合
h1 ^= uint32(length)
h1 ^= h1 >> 16
h1 *= 0x85ebca6b
h1 ^= h1 >> 13
h1 *= 0xc2b2ae35
h1 ^= h1 >> 16
return int32(h1)
}