github.com/chainreactors/fingers@v1.2.1/fingerprinthub/fingerprinthub.go (about)

     1  package fingerprinthub
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/chainreactors/fingers/common"
    13  	"github.com/chainreactors/fingers/resources"
    14  	"github.com/chainreactors/logs"
    15  	"github.com/chainreactors/neutron/protocols"
    16  	http2 "github.com/chainreactors/neutron/protocols/http"
    17  	"github.com/chainreactors/neutron/templates"
    18  	"github.com/chainreactors/utils/encode"
    19  	"github.com/chainreactors/utils/httputils"
    20  	"gopkg.in/yaml.v3"
    21  )
    22  
    23  // CachedResponse 存储缓存的 HTTP 响应
    24  type CachedResponse struct {
    25  	Response *http.Response // 响应对象(Body 为 nil)
    26  	Body     []byte         // 响应体内容
    27  }
    28  
    29  // CachedTransport 实现带缓存的 http.RoundTripper
    30  // 通过 path 去重,避免重复请求
    31  type CachedTransport struct {
    32  	transport http.RoundTripper
    33  	cache     map[string]*CachedResponse
    34  	mu        sync.Mutex
    35  }
    36  
    37  // RoundTrip 实现 http.RoundTripper 接口,带缓存功能
    38  func (c *CachedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    39  	// 使用 path 作为缓存键
    40  	cacheKey := req.URL.Path
    41  	if cacheKey == "" {
    42  		cacheKey = "/"
    43  	}
    44  
    45  	// 检查缓存
    46  	c.mu.Lock()
    47  	if cached, ok := c.cache[cacheKey]; ok {
    48  		c.mu.Unlock()
    49  		// 复制响应对象,使用缓存的 Body
    50  		resp := *cached.Response
    51  		resp.Body = ioutil.NopCloser(bytes.NewReader(cached.Body))
    52  		resp.Request = req
    53  		return &resp, nil
    54  	}
    55  	c.mu.Unlock()
    56  
    57  	// 缓存未命中,发送实际请求
    58  	resp, err := c.transport.RoundTrip(req)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	// 读取响应体
    64  	bodyBytes, err := ioutil.ReadAll(resp.Body)
    65  	resp.Body.Close()
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	// 保存到缓存(复制响应对象,Body 设为 nil)
    71  	cachedResp := *resp
    72  	cachedResp.Body = nil
    73  	c.mu.Lock()
    74  	c.cache[cacheKey] = &CachedResponse{
    75  		Response: &cachedResp,
    76  		Body:     bodyBytes,
    77  	}
    78  	c.mu.Unlock()
    79  
    80  	// 返回响应(使用缓存的 Body)
    81  	resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
    82  	return resp, nil
    83  }
    84  
    85  // FingerPrintHubEngine 基于 neutron 的 FingerprintHub 引擎
    86  type FingerPrintHubEngine struct {
    87  	webTemplates     []*templates.Template // Web 指纹模板
    88  	serviceTemplates []*templates.Template // Service 指纹模板
    89  	executerOptions  *protocols.ExecuterOptions
    90  	webTemplateIndex *TemplateKeywordIndex // AC keyword index for web template prefiltering
    91  
    92  	// CaseInsensitive 控制匹配时是否忽略大小写(默认 true)。
    93  	// 开启时 event 中的 body/header 统一 ToLower,word matcher keywords 也在编译时 ToLower。
    94  	// DSL/Regex 模板中的字面量需使用小写以配合此模式。
    95  	CaseInsensitive bool
    96  }
    97  
    98  // NewFingerPrintHubEngine 创建新的引擎实例
    99  func NewFingerPrintHubEngine(webData, serviceData []byte) (*FingerPrintHubEngine, error) {
   100  	engine := &FingerPrintHubEngine{
   101  		CaseInsensitive:  true,
   102  		webTemplates:     make([]*templates.Template, 0),
   103  		serviceTemplates: make([]*templates.Template, 0),
   104  		executerOptions: &protocols.ExecuterOptions{
   105  			Options: &protocols.Options{
   106  				Timeout: 10, // 默认 10 秒超时
   107  			},
   108  		},
   109  	}
   110  
   111  	// 加载 web 指纹
   112  	var webTemplates []map[string]interface{}
   113  	if err := resources.UnmarshalData(webData, &webTemplates); err != nil {
   114  		return nil, fmt.Errorf("failed to unmarshal web fingerprints: %w", err)
   115  	}
   116  
   117  	webCount, webErrors := engine.loadTemplates(webTemplates, true)
   118  
   119  	// 加载 service 指纹
   120  	var serviceTemplates []map[string]interface{}
   121  	if err := resources.UnmarshalData(serviceData, &serviceTemplates); err != nil {
   122  		return nil, fmt.Errorf("failed to unmarshal service fingerprints: %w", err)
   123  	}
   124  
   125  	serviceCount, serviceErrors := engine.loadTemplates(serviceTemplates, false)
   126  
   127  	// 显示前几个错误
   128  	allErrors := append(webErrors, serviceErrors...)
   129  	if len(allErrors) > 0 && len(allErrors) < 10 {
   130  		for _, e := range allErrors {
   131  			logs.Log.Warn(e)
   132  		}
   133  	}
   134  
   135  	logs.Log.Infof("resources type=fingerprints source=fingerprinthub templates=%d web=%d service=%d", webCount+serviceCount, webCount, serviceCount)
   136  
   137  	engine.webTemplateIndex = NewTemplateKeywordIndex(engine.webTemplates)
   138  
   139  	return engine, nil
   140  }
   141  
   142  // loadTemplates 加载并编译模板
   143  func (engine *FingerPrintHubEngine) loadTemplates(templateData []map[string]interface{}, isWeb bool) (int, []error) {
   144  	loadedCount := 0
   145  	var errors []error
   146  
   147  	for _, rawTemplate := range templateData {
   148  		sanitizeTemplateForTinyGo(rawTemplate)
   149  
   150  		// 将 map 转为 YAML bytes (neutron 使用 YAML unmarshaler)
   151  		yamlBytes, err := yaml.Marshal(rawTemplate)
   152  		if err != nil {
   153  			errors = append(errors, fmt.Errorf("failed to marshal template: %w", err))
   154  			continue
   155  		}
   156  
   157  		// 解析模板
   158  		tmpl := &templates.Template{}
   159  		if err := yaml.Unmarshal(yamlBytes, tmpl); err != nil {
   160  			errors = append(errors, fmt.Errorf("failed to unmarshal template: %w", err))
   161  			continue
   162  		}
   163  
   164  		if err := engine.compileTemplate(tmpl); err != nil {
   165  			errors = append(errors, fmt.Errorf("failed to compile template %s: %w", tmpl.Id, err))
   166  			continue
   167  		}
   168  
   169  		// 修复 FingerprintHub 指纹中缺少 ReadSize 和 Input.Read 字段的问题
   170  		for _, netReq := range tmpl.RequestsNetwork {
   171  			for _, input := range netReq.Inputs {
   172  				if input.Read == 0 {
   173  					input.Read = 1024
   174  				}
   175  			}
   176  			if netReq.ReadSize == 0 {
   177  				netReq.ReadSize = 1024
   178  			}
   179  		}
   180  
   181  		// 添加到对应的列表
   182  		if isWeb {
   183  			engine.webTemplates = append(engine.webTemplates, tmpl)
   184  		} else {
   185  			engine.serviceTemplates = append(engine.serviceTemplates, tmpl)
   186  		}
   187  		loadedCount++
   188  	}
   189  
   190  	return loadedCount, errors
   191  }
   192  
   193  // compileTemplate 编译模板。当 CaseInsensitive 开启时,设置 word matcher
   194  // 的 CaseInsensitive 标志,使 neutron 在编译时将 keywords ToLower,
   195  // 匹配时将 corpus ToLower。所有模板加载路径都必须经过此方法。
   196  func (engine *FingerPrintHubEngine) compileTemplate(tmpl *templates.Template) error {
   197  	if engine.CaseInsensitive {
   198  		for _, req := range tmpl.GetRequests() {
   199  			for _, matcher := range req.Matchers {
   200  				if matcher.Type == "word" {
   201  					matcher.CaseInsensitive = true
   202  				}
   203  			}
   204  		}
   205  	}
   206  	return tmpl.Compile(engine.executerOptions)
   207  }
   208  
   209  // LoadFromJSON 从 JSON 数据加载指纹
   210  func (engine *FingerPrintHubEngine) LoadFromJSON(data []byte) error {
   211  	// 解析 JSON
   212  	var templateData []map[string]interface{}
   213  	if err := json.Unmarshal(data, &templateData); err != nil {
   214  		return fmt.Errorf("failed to unmarshal JSON: %w", err)
   215  	}
   216  
   217  	// 转换为 YAML 并加载每个模板
   218  	loadedCount := 0
   219  	var errors []error
   220  
   221  	for _, rawTemplate := range templateData {
   222  		sanitizeTemplateForTinyGo(rawTemplate)
   223  
   224  		// 将 map 转为 YAML bytes (neutron 使用 YAML unmarshaler)
   225  		yamlBytes, err := yaml.Marshal(rawTemplate)
   226  		if err != nil {
   227  			errors = append(errors, fmt.Errorf("failed to marshal template: %w", err))
   228  			continue
   229  		}
   230  
   231  		// 解析模板
   232  		tmpl := &templates.Template{}
   233  		err = yaml.Unmarshal(yamlBytes, tmpl)
   234  		if err != nil {
   235  			errors = append(errors, fmt.Errorf("failed to unmarshal template: %w", err))
   236  			continue
   237  		}
   238  
   239  		if err = engine.compileTemplate(tmpl); err != nil {
   240  			errors = append(errors, fmt.Errorf("failed to compile template %s: %w", tmpl.Id, err))
   241  			continue
   242  		}
   243  
   244  		// 修复 FingerprintHub 指纹中缺少 ReadSize 和 Input.Read 字段的问题
   245  		for _, netReq := range tmpl.RequestsNetwork {
   246  			for _, input := range netReq.Inputs {
   247  				if input.Read == 0 {
   248  					input.Read = 1024
   249  				}
   250  			}
   251  			if netReq.ReadSize == 0 {
   252  				netReq.ReadSize = 1024
   253  			}
   254  		}
   255  
   256  		// 根据模板类型添加到对应的列表
   257  		// web 指纹包含 HTTP 请求,service 指纹包含 network 请求
   258  		if len(tmpl.RequestsHTTP) > 0 {
   259  			engine.webTemplates = append(engine.webTemplates, tmpl)
   260  		} else if len(tmpl.RequestsNetwork) > 0 {
   261  			engine.serviceTemplates = append(engine.serviceTemplates, tmpl)
   262  		}
   263  		loadedCount++
   264  	}
   265  
   266  	if len(errors) > 0 && len(errors) < 10 {
   267  		for _, e := range errors {
   268  			logs.Log.Warn(e)
   269  		}
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  // LoadFromFS 从文件系统加载模板(用于开发测试)
   276  //func (engine *FingerPrintHubEngine) LoadFromFS(fsys fs.FS, pattern string) error {
   277  //	var loadedCount int
   278  //	var errors []error
   279  //
   280  //	err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
   281  //		if err != nil {
   282  //			return err
   283  //		}
   284  //
   285  //		if d.IsDir() {
   286  //			return nil
   287  //		}
   288  //
   289  //		// 只处理 .yaml 和 .yml 文件
   290  //		ext := filepath.Ext(path)
   291  //		if ext != ".yaml" && ext != ".yml" {
   292  //			return nil
   293  //		}
   294  //
   295  //		// 检查是否匹配 pattern
   296  //		if pattern != "" {
   297  //			matched, _ := filepath.Match(pattern, filepath.Base(path))
   298  //			if !matched {
   299  //				return nil
   300  //			}
   301  //		}
   302  //
   303  //		// 读取文件内容
   304  //		content, err := fs.ReadFile(fsys, path)
   305  //		if err != nil {
   306  //			errors = append(errors, fmt.Errorf("failed to read %s: %w", path, err))
   307  //			return nil // 继续处理其他文件
   308  //		}
   309  //
   310  //		// 解析模板
   311  //		tmpl := &templates.Template{}
   312  //		err = yaml.Unmarshal(content, tmpl)
   313  //		if err != nil {
   314  //			errors = append(errors, fmt.Errorf("failed to unmarshal %s: %w", path, err))
   315  //			return nil
   316  //		}
   317  //
   318  //		// 编译模板
   319  //		// neutron 会自动处理 tcp/udp 字段作为 network 的别名
   320  //		err = tmpl.Compile(engine.executerOptions)
   321  //		if err != nil {
   322  //			errors = append(errors, fmt.Errorf("failed to compile %s: %w", path, err))
   323  //			return nil
   324  //		}
   325  //
   326  //		// 修复 FingerprintHub 指纹中缺少 ReadSize 和 Input.Read 字段的问题
   327  //		// 这个修复在编译后执行,适用于所有 network 请求(包括从 tcp/udp 转换来的)
   328  //		for _, netReq := range tmpl.RequestsNetwork {
   329  //			// 修复 input.Read 字段
   330  //			for _, input := range netReq.Inputs {
   331  //				if input.Read == 0 {
   332  //					input.Read = 1024
   333  //				}
   334  //			}
   335  //			// 修复 ReadSize 字段
   336  //			if netReq.ReadSize == 0 {
   337  //				netReq.ReadSize = 1024
   338  //			}
   339  //		}
   340  //
   341  //		// 根据模板类型添加到对应的列表
   342  //		// web 指纹包含 HTTP 请求,service 指纹包含 network 请求
   343  //		if len(tmpl.RequestsHTTP) > 0 {
   344  //			engine.webTemplates = append(engine.webTemplates, tmpl)
   345  //		} else if len(tmpl.RequestsNetwork) > 0 {
   346  //			engine.serviceTemplates = append(engine.serviceTemplates, tmpl)
   347  //		}
   348  //		loadedCount++
   349  //
   350  //		return nil
   351  //	})
   352  //
   353  //	if err != nil {
   354  //		return fmt.Errorf("failed to walk filesystem: %w", err)
   355  //	}
   356  //
   357  //	if len(errors) > 0 {
   358  //		// 记录所有错误
   359  //		for _, e := range errors {
   360  //			fmt.Printf("Warning: %v\n", e)
   361  //		}
   362  //	}
   363  //
   364  //	fmt.Printf("Loaded %d fingerprint templates from filesystem\n", loadedCount)
   365  //	return nil
   366  //}
   367  
   368  // Name 返回引擎名称
   369  func (engine *FingerPrintHubEngine) Name() string {
   370  	return "fingerprinthub"
   371  }
   372  
   373  // Len 返回指纹数量
   374  func (engine *FingerPrintHubEngine) Len() int {
   375  	return len(engine.webTemplates) + len(engine.serviceTemplates)
   376  }
   377  
   378  // Compile 编译所有模板(已在加载时完成)
   379  func (engine *FingerPrintHubEngine) Compile() error {
   380  	return nil
   381  }
   382  
   383  // Capability 返回引擎能力
   384  func (engine *FingerPrintHubEngine) Capability() common.EngineCapability {
   385  	return common.EngineCapability{
   386  		SupportWeb:     true, // 支持 HTTP 指纹
   387  		SupportService: true, // 支持 Service 指纹 (通过 neutron network)
   388  	}
   389  }
   390  
   391  // WebMatch 实现 Web 指纹匹配
   392  func (engine *FingerPrintHubEngine) WebMatch(content []byte) common.Frameworks {
   393  	resp := httputils.NewResponseWithRaw(content)
   394  	if resp == nil {
   395  		return make(common.Frameworks)
   396  	}
   397  
   398  	rawBody := httputils.ReadBody(resp)
   399  	bodyStr := string(rawBody)
   400  
   401  	// AC index 始终使用小写进行关键词预过滤
   402  	lowerBodyStr := strings.ToLower(bodyStr)
   403  
   404  	// CaseInsensitive 开启时,event 中的 body/header 使用小写,
   405  	// 使 word/DSL/regex 等所有 matcher 统一在小写上匹配
   406  	if engine.CaseInsensitive {
   407  		bodyStr = lowerBodyStr
   408  	}
   409  
   410  	event := engine.buildInternalEvent(resp, bodyStr, len(content))
   411  
   412  	frames := make(common.Frameworks)
   413  
   414  	headerStr, _ := event["all_headers"].(string)
   415  	lowerHeaderStr := headerStr
   416  	if !engine.CaseInsensitive {
   417  		lowerHeaderStr = strings.ToLower(headerStr)
   418  	}
   419  	mr := engine.webTemplateIndex.Match(lowerHeaderStr, lowerBodyStr)
   420  
   421  	// Fast path: AC keyword hit directly resolves these templates.
   422  	// All matchers are Word type with OR condition — AC match = matched.
   423  	// 仅在 CaseInsensitive 模式下可用,因为 AC index 始终用小写匹配。
   424  	if engine.CaseInsensitive {
   425  		for ti := range mr.Matched {
   426  			frames.Add(engine.newFramework(engine.webTemplates[ti]))
   427  		}
   428  	}
   429  
   430  	// Slow path: templates needing full matchRequest (regex, AND, fallback).
   431  	// CaseSensitive 模式下 AC fast path 的结果也走 slow path 以确保精确匹配。
   432  	if !engine.CaseInsensitive {
   433  		for ti := range mr.Matched {
   434  			mr.NeedsCheck[ti] = true
   435  		}
   436  	}
   437  	for ti := range mr.NeedsCheck {
   438  		tmpl := engine.webTemplates[ti]
   439  		requests := tmpl.GetRequests()
   440  		if len(requests) == 0 {
   441  			continue
   442  		}
   443  
   444  		for _, req := range requests {
   445  			if req.Matchers == nil || len(req.Matchers) == 0 {
   446  				continue
   447  			}
   448  
   449  			if engine.matchRequest(req, event) {
   450  				frames.Add(engine.newFramework(tmpl))
   451  				break
   452  			}
   453  		}
   454  	}
   455  
   456  	return frames
   457  }
   458  
   459  func (engine *FingerPrintHubEngine) newFramework(tmpl *templates.Template) *common.Framework {
   460  	name := tmpl.Info.Name
   461  	if name == "" {
   462  		name = tmpl.Id
   463  	}
   464  	frame := common.NewFramework(name, common.FrameFromFingerprintHub)
   465  	if tmpl.Info.Metadata != nil {
   466  		if vendor, ok := tmpl.Info.Metadata["vendor"].(string); ok {
   467  			frame.Attributes.Vendor = vendor
   468  		}
   469  		if product, ok := tmpl.Info.Metadata["product"].(string); ok {
   470  			frame.Attributes.Product = product
   471  		}
   472  	}
   473  	return frame
   474  }
   475  
   476  // buildInternalEvent 构建 neutron 的 InternalEvent
   477  // 复用 neutron 的数据结构,包括 all_headers 等字段
   478  func (engine *FingerPrintHubEngine) buildInternalEvent(resp *http.Response, bodyStr string, contentLength int) protocols.InternalEvent {
   479  	event := make(protocols.InternalEvent)
   480  
   481  	// 基础字段
   482  	event["body"] = bodyStr
   483  	event["status_code"] = resp.StatusCode
   484  	event["content_length"] = contentLength
   485  
   486  	// header 字段:原始 http.Header
   487  	event["header"] = resp.Header
   488  
   489  	// all_headers 字段:neutron 使用的拼接格式
   490  	// 复用这个逻辑避免在 matchSingle 中重复拼接
   491  	event["all_headers"] = engine.buildHeaderString(resp.Header)
   492  
   493  	// favicon 字段:提取 favicon hash 用于 favicon matcher
   494  	faviconData := extractFaviconFromResponse(resp, []byte(bodyStr))
   495  	if len(faviconData) > 0 {
   496  		event["favicon"] = faviconData
   497  	}
   498  
   499  	return event
   500  }
   501  
   502  // buildHeaderString 构建 header 字符串。
   503  // key 始终小写(HTTP 规范);value 根据 CaseInsensitive 开关决定。
   504  func (engine *FingerPrintHubEngine) buildHeaderString(header http.Header) string {
   505  	var builder strings.Builder
   506  	for key, values := range header {
   507  		for _, value := range values {
   508  			builder.WriteString(strings.ToLower(key))
   509  			builder.WriteString(": ")
   510  			if engine.CaseInsensitive {
   511  				builder.WriteString(strings.ToLower(value))
   512  			} else {
   513  				builder.WriteString(value)
   514  			}
   515  			builder.WriteString("\n")
   516  		}
   517  	}
   518  	return builder.String()
   519  }
   520  
   521  // matchRequest 检查请求的所有 matchers 是否匹配
   522  // 复用 neutron 的 Request.Match 方法
   523  func (engine *FingerPrintHubEngine) matchRequest(req *http2.Request, event protocols.InternalEvent) bool {
   524  	if req.Matchers == nil || len(req.Matchers) == 0 {
   525  		return false
   526  	}
   527  
   528  	// 根据 MatchersCondition 决定逻辑
   529  	matchersCondition := req.MatchersCondition
   530  	if matchersCondition == "" {
   531  		matchersCondition = "or" // 默认为 OR
   532  	}
   533  
   534  	matchedCount := 0
   535  	for _, matcher := range req.Matchers {
   536  		// 直接使用 neutron 的 Request.Match 方法
   537  		// 这样可以复用所有的匹配逻辑,包括 getMatchPart 等
   538  		matched, _ := req.Match(event, matcher)
   539  		if matched {
   540  			matchedCount++
   541  			if matchersCondition == "or" {
   542  				return true // OR 条件下,任意匹配即可
   543  			}
   544  		} else {
   545  			if matchersCondition == "and" {
   546  				return false // AND 条件下,任意不匹配即失败
   547  			}
   548  		}
   549  	}
   550  
   551  	// AND 条件下,需要所有 matcher 都匹配
   552  	if matchersCondition == "and" {
   553  		return matchedCount == len(req.Matchers)
   554  	}
   555  
   556  	return false
   557  }
   558  
   559  // HTTPActiveMatch 实现 HTTP 主动指纹匹配
   560  // 使用 http.RoundTripper 进行主动探测,统一发包逻辑
   561  // transport: 自定义的 HTTP 传输层,用于发送请求
   562  func (engine *FingerPrintHubEngine) HTTPActiveMatch(baseURL string, level int, transport http.RoundTripper, callback func(*common.Framework, *common.Vuln)) (common.Frameworks, common.Vulns) {
   563  	if baseURL == "" || transport == nil {
   564  		return nil, nil
   565  	}
   566  
   567  	// 初始化结果集 map
   568  	allFrameworks := make(common.Frameworks)
   569  	allVulns := make(common.Vulns)
   570  
   571  	// 创建带缓存的 transport,通过 path 去重
   572  	cachedTransport := &CachedTransport{
   573  		transport: transport,
   574  		cache:     make(map[string]*CachedResponse),
   575  	}
   576  
   577  	// 使用带缓存的 transport 创建 HTTP client
   578  	httpClient := &http.Client{
   579  		Transport: cachedTransport,
   580  	}
   581  
   582  	// 创建扫描上下文
   583  	// Thread the per-call client through the ScanContext so concurrent calls never
   584  	// mutate the shared compiled templates (httpReq.SetHTTPClient was a data race).
   585  	scanCtx := &protocols.ScanContext{
   586  		Input:  baseURL,
   587  		Client: httpClient,
   588  	}
   589  
   590  	// 遍历所有 web 模板(保留手动遍历 + CachedTransport 加速路径)
   591  	for _, tmpl := range engine.webTemplates {
   592  		if len(tmpl.RequestsHTTP) == 0 {
   593  			continue
   594  		}
   595  
   596  		for _, httpReq := range tmpl.RequestsHTTP {
   597  			err := httpReq.ExecuteWithResults(scanCtx, make(map[string]interface{}), make(map[string]interface{}), func(event *protocols.InternalWrappedEvent) {
   598  				if event.OperatorsResult != nil && event.OperatorsResult.Matched {
   599  					frame := engine.newFramework(tmpl)
   600  					allFrameworks.Add(frame)
   601  					if callback != nil {
   602  						callback(frame, nil)
   603  					}
   604  				}
   605  			})
   606  
   607  			if err != nil {
   608  				continue
   609  			}
   610  		}
   611  	}
   612  
   613  	return allFrameworks, allVulns
   614  }
   615  
   616  // ServiceMatch 实现 Service 指纹匹配
   617  func (engine *FingerPrintHubEngine) ServiceMatch(host string, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult {
   618  	// 构建目标地址
   619  	target := fmt.Sprintf("%s:%s", host, portStr)
   620  
   621  	// 创建扫描上下文
   622  	scanCtx := &protocols.ScanContext{
   623  		Input: target,
   624  	}
   625  
   626  	// 遍历所有 service 模板,找到包含 network 请求的模板
   627  	for _, tmpl := range engine.serviceTemplates {
   628  		// 检查是否有 network 请求
   629  		if len(tmpl.RequestsNetwork) == 0 {
   630  			continue
   631  		}
   632  
   633  		// 遍历所有 network 请求
   634  		for _, networkReq := range tmpl.RequestsNetwork {
   635  			// 执行 network 请求
   636  			var matched bool
   637  			err := networkReq.ExecuteWithResults(scanCtx, make(map[string]interface{}), make(map[string]interface{}), func(event *protocols.InternalWrappedEvent) {
   638  				// 检查是否有匹配结果
   639  				// FingerprintHub service-fingerprint 使用 extractors 而不是 matchers
   640  				// 如果有 extractor 提取到值,说明匹配成功
   641  				if event.OperatorsResult != nil {
   642  					// 有 matchers 的情况
   643  					if event.OperatorsResult.Matched {
   644  						matched = true
   645  					}
   646  					// 有 extractors 的情况 - 提取到值说明匹配成功
   647  					if len(event.OperatorsResult.OutputExtracts) > 0 {
   648  						matched = true
   649  					}
   650  				}
   651  
   652  				if matched {
   653  					// 构建 Framework
   654  					name := tmpl.Info.Name
   655  					if name == "" {
   656  						name = tmpl.Id
   657  					}
   658  					frame := common.NewFramework(name, common.FrameFromFingerprintHub)
   659  
   660  					// 添加元数据
   661  					if tmpl.Info.Metadata != nil {
   662  						if vendor, ok := tmpl.Info.Metadata["vendor"].(string); ok {
   663  							frame.Attributes.Vendor = vendor
   664  						}
   665  						if product, ok := tmpl.Info.Metadata["product"].(string); ok {
   666  							frame.Attributes.Product = product
   667  						}
   668  					}
   669  
   670  					// 创建 ServiceResult 并通过回调返回
   671  					if callback != nil {
   672  						callback(&common.ServiceResult{
   673  							Framework: frame,
   674  						})
   675  					}
   676  				}
   677  			})
   678  
   679  			if err != nil {
   680  				// 忽略错误继续尝试其他指纹
   681  				continue
   682  			}
   683  
   684  			// 如果匹配成功,可以选择提前返回
   685  			if matched {
   686  				// 这里选择继续匹配其他指纹,以便返回所有可能的匹配
   687  				// 如果只需要第一个匹配,可以在这里 return
   688  			}
   689  		}
   690  	}
   691  
   692  	return nil
   693  }
   694  
   695  // calculateFaviconHash 计算 favicon 的 MD5 和 MMH3 hash
   696  // 返回 [md5, mmh3] 格式的 hash 数组
   697  func calculateFaviconHash(content []byte) []string {
   698  	if len(content) == 0 {
   699  		return nil
   700  	}
   701  
   702  	md5Hash := encode.Md5Hash(content)
   703  	mmh3Hash := encode.Mmh3Hash32(content)
   704  
   705  	return []string{md5Hash, mmh3Hash}
   706  }
   707  
   708  // extractFaviconFromResponse 从 HTTP 响应中提取 favicon 数据
   709  // 返回 map[url][]hash 格式的数据,用于 favicon matcher
   710  func extractFaviconFromResponse(resp *http.Response, body []byte) map[string]interface{} {
   711  	faviconData := make(map[string]interface{})
   712  
   713  	// 检查响应和请求是否有效
   714  	if resp == nil || resp.Request == nil || resp.Request.URL == nil {
   715  		return faviconData
   716  	}
   717  
   718  	// 如果响应本身是 favicon.ico
   719  	if strings.HasSuffix(resp.Request.URL.Path, "/favicon.ico") {
   720  		if isImageContent(resp, body) {
   721  			hashes := calculateFaviconHash(body)
   722  			if hashes != nil {
   723  				faviconData[resp.Request.URL.String()] = hashes
   724  			}
   725  		}
   726  	}
   727  
   728  	// TODO: 未来可以扩展支持从 HTML 中提取 <link rel="icon"> 标签
   729  	// 目前仅支持直接请求 favicon.ico 的场景
   730  
   731  	return faviconData
   732  }
   733  
   734  // isImageContent 判断响应内容是否为图片
   735  func isImageContent(resp *http.Response, body []byte) bool {
   736  	// 检查 Content-Type
   737  	contentType := resp.Header.Get("Content-Type")
   738  	if strings.Contains(contentType, "image/") {
   739  		return true
   740  	}
   741  
   742  	// 简单检查:如果内容可以解析为 UTF-8 文本且包含 HTML 标签,则不是图片
   743  	if len(body) > 0 {
   744  		bodyStr := string(body)
   745  		htmlTags := []string{"<html", "<head", "<script", "<div", "<title", "<?xml"}
   746  		for _, tag := range htmlTags {
   747  			if strings.Contains(strings.ToLower(bodyStr), tag) {
   748  				return false
   749  			}
   750  		}
   751  	}
   752  
   753  	return true
   754  }