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

     1  package fingers
     2  
     3  import (
     4  	"github.com/chainreactors/fingers/common"
     5  	"github.com/chainreactors/logs"
     6  	"github.com/chainreactors/utils"
     7  	"github.com/chainreactors/utils/encode"
     8  	"github.com/chainreactors/utils/httputils"
     9  )
    10  
    11  var (
    12  	OPSEC     = false
    13  	FingerLog = logs.Log
    14  )
    15  
    16  type Finger struct {
    17  	Name              string            `yaml:"name" json:"name" jsonschema:"required,title=Fingerprint Name,description=Unique identifier for the fingerprint,example=nginx"`
    18  	Attributes        common.Attributes `yaml:",inline" json:",inline"`
    19  	Author            string            `yaml:"author,omitempty" json:"author,omitempty" jsonschema:"title=Author,description= Finger template author,nullable"`
    20  	Description       string            `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description= Finger template description,nullable"`
    21  	Protocol          string            `yaml:"protocol,omitempty" json:"protocol,omitempty" jsonschema:"title=Protocol,description=Network protocol type,nullable,enum=http,enum=tcp,enum=udp,default=http,example=http"`
    22  	Link              string            `yaml:"link,omitempty" json:"link,omitempty" jsonschema:"title=Link,description=Reference URL for the software,nullable,format=uri,example=https://nginx.org"`
    23  	DefaultPort       []string          `yaml:"default_port,omitempty" json:"default_port,omitempty" jsonschema:"title=Default Ports,description=Default ports used by this service,nullable,example=80,example=443"`
    24  	Focus             bool              `yaml:"focus,omitempty" json:"focus,omitempty" jsonschema:"title=Focus,description=Whether this is a high-priority fingerprint,default=false"`
    25  	SendDataStr       string            `yaml:"send_data,omitempty" json:"send_data,omitempty" jsonschema:"title=Send Data,description=Data to send for active probing at level 1,nullable,example=/nacos/"`
    26  	SendData          senddata          `yaml:"-" json:"-"`
    27  	Rules             Rules             `yaml:"rule,omitempty" json:"rule,omitempty" jsonschema:"required,title=Rules,description=Matching rules for fingerprint detection"`
    28  	Tags              []string          `yaml:"tag,omitempty" json:"tag,omitempty" jsonschema:"title=Tags,description=Category tags for classification,nullable,example=web,example=server"`
    29  	Level             int               `yaml:"level,omitempty" json:"level,omitempty" jsonschema:"title=Level,description=Fingerprint detection level,default=0"`
    30  	Opsec             bool              `yaml:"opsec,omitempty" json:"opsec,omitempty" jsonschema:"title=OPSEC,description=Whether this fingerprint uses operational security measures,default=false"`
    31  	EnableMatchDetail bool              `yaml:"-" json:"-"`
    32  	IsActive          bool              `yaml:"-" json:"-"`
    33  }
    34  
    35  func (finger *Finger) Compile(caseSensitive bool) error {
    36  	if finger.Protocol == "" {
    37  		finger.Protocol = HTTPProtocol
    38  	}
    39  
    40  	if len(finger.DefaultPort) == 0 {
    41  		if finger.Protocol == HTTPProtocol {
    42  			finger.DefaultPort = []string{"80"}
    43  		}
    44  	} else if utils.PrePort != nil {
    45  		finger.DefaultPort = utils.ParsePortsSlice(finger.DefaultPort)
    46  	}
    47  
    48  	if finger.SendDataStr != "" {
    49  		finger.SendData, _ = encode.DSLParser(finger.SendDataStr)
    50  		if finger.Level == 0 {
    51  			finger.Level = 1
    52  		}
    53  	}
    54  
    55  	err := finger.Rules.Compile(finger.Name, caseSensitive)
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	finger.RefreshActive()
    61  	return nil
    62  }
    63  
    64  // RefreshActive recomputes whether this finger has any active rules.
    65  func (finger *Finger) RefreshActive() {
    66  	finger.IsActive = finger.SendDataStr != ""
    67  	for _, r := range finger.Rules {
    68  		r.RefreshActive()
    69  		if r.IsActive {
    70  			finger.IsActive = true
    71  		}
    72  	}
    73  }
    74  
    75  func (finger *Finger) ToResult(hasFrame, hasVuln bool, ver string, index int) (frame *common.Framework, vuln *common.Vuln) {
    76  	if index >= len(finger.Rules) {
    77  		return nil, nil
    78  	}
    79  
    80  	if hasFrame {
    81  		if ver != "" {
    82  			frame = common.NewFrameworkWithVersion(finger.Name, common.FrameFromFingers, ver)
    83  		} else if finger.Rules[index].Version != "_" {
    84  			frame = common.NewFrameworkWithVersion(finger.Name, common.FrameFromFingers, finger.Rules[index].Version)
    85  		} else {
    86  			frame = common.NewFramework(finger.Name, common.FrameFromFingers)
    87  		}
    88  	}
    89  
    90  	if hasVuln {
    91  		if finger.Rules[index].Vuln != "" {
    92  			vuln = &common.Vuln{Name: finger.Rules[index].Vuln, SeverityLevel: HIGH, Framework: frame}
    93  		} else if finger.Rules[index].Info != "" {
    94  			vuln = &common.Vuln{Name: finger.Rules[index].Info, SeverityLevel: INFO, Framework: frame}
    95  		} else {
    96  			vuln = &common.Vuln{Name: finger.Name, SeverityLevel: INFO}
    97  		}
    98  		if finger.IsActive {
    99  			if sendDataStr := finger.Rules[index].ActiveSendDataStr(finger.SendDataStr); sendDataStr != "" {
   100  				vuln.Detail = map[string][]string{"path": []string{sendDataStr}}
   101  			}
   102  		}
   103  	}
   104  
   105  	frame.Vendor = finger.Attributes.Vendor
   106  	frame.Product = finger.Attributes.Product
   107  	return frame, vuln
   108  }
   109  
   110  // annotateRuleHit records which rule, matcher, and send_data produced the hit on the framework.
   111  func (finger *Finger) annotateRuleHit(frame *common.Framework, index int, detail *common.MatchDetail, sendData string) {
   112  	if frame == nil || !finger.EnableMatchDetail {
   113  		return
   114  	}
   115  	matchDetail := &common.MatchDetail{
   116  		RuleIndex: index,
   117  		SendData:  sendData,
   118  	}
   119  	if detail != nil {
   120  		matchDetail.MatcherType = detail.MatcherType
   121  		matchDetail.MatcherIndex = detail.MatcherIndex
   122  		matchDetail.MatcherValue = detail.MatcherValue
   123  	}
   124  	frame.MatchDetail = matchDetail
   125  }
   126  
   127  // buildActiveResult constructs a result from an active hit and normalizes metadata.
   128  func (finger *Finger) buildActiveResult(index int, hasVuln bool, ver string, detail *common.MatchDetail, sendData string) (*common.Framework, *common.Vuln) {
   129  	frame, vuln := finger.ToResult(true, hasVuln, ver, index)
   130  	if frame == nil {
   131  		return nil, vuln
   132  	}
   133  	if finger.Focus {
   134  		frame.IsFocus = true
   135  	}
   136  	frame.From = common.FrameFromFingers
   137  	frame.Froms = map[common.From]bool{common.FrameFromACTIVE: true}
   138  	for _, tag := range finger.Tags {
   139  		frame.AddTag(tag)
   140  	}
   141  	finger.annotateRuleHit(frame, index, detail, sendData)
   142  	return frame, vuln
   143  }
   144  
   145  // activeProbeAll performs active probing across all rules and payloads.
   146  // It records the first match but does not short-circuit sending.
   147  func (finger *Finger) activeProbeAll(level int, sender Sender) (*common.Framework, *common.Vuln, bool) {
   148  	if sender == nil || level <= 0 {
   149  		return nil, nil, false
   150  	}
   151  	if OPSEC == true && finger.Opsec == true {
   152  		FingerLog.Debugf("(opsec!!!) skip active finger %s scan", finger.Name)
   153  		return nil, nil, false
   154  	}
   155  
   156  	ishttp := finger.Protocol == HTTPProtocol
   157  	var firstFrame *common.Framework
   158  	var firstVuln *common.Vuln
   159  	type cachedResp struct {
   160  		resp []byte
   161  		ok   bool
   162  	}
   163  	respCache := make(map[string]cachedResp)
   164  	for i, rule := range finger.Rules {
   165  		for _, payload := range rule.ActiveSendDataList(level, finger.SendData) {
   166  			payloadKey := string(payload)
   167  			activeContent := &Content{}
   168  			entry, found := respCache[payloadKey]
   169  			if !found {
   170  				FingerLog.Debugf("active probe send_data=%q for finger=%s", payloadKey, finger.Name)
   171  				resp, ok := sender(payload)
   172  				entry = cachedResp{resp: resp, ok: ok}
   173  				respCache[payloadKey] = entry
   174  			}
   175  			if !entry.ok {
   176  				continue
   177  			}
   178  			if ishttp {
   179  				activeContent.UpdateContent(entry.resp)
   180  			} else {
   181  				activeContent.Content = entry.resp
   182  			}
   183  
   184  			if ishttp && rule.Favicon != nil {
   185  				if matched, detail := matchFaviconRule(rule, entry.resp); matched {
   186  					if firstFrame == nil {
   187  						firstFrame, firstVuln = finger.buildActiveResult(i, false, "", detail, payloadKey)
   188  					}
   189  					continue
   190  				}
   191  			}
   192  
   193  			hasFrame, hasVuln, ver, detail := RuleMatcher(rule, activeContent, ishttp)
   194  			if hasFrame && firstFrame == nil {
   195  				firstFrame, firstVuln = finger.buildActiveResult(i, hasVuln, ver, detail, payloadKey)
   196  			}
   197  		}
   198  	}
   199  	if firstFrame != nil {
   200  		return firstFrame, firstVuln, true
   201  	}
   202  	return nil, nil, false
   203  }
   204  
   205  func (finger *Finger) Match(content *Content, level int, sender Sender) (*common.Framework, *common.Vuln, bool) {
   206  	// sender用来处理需要主动发包的场景, 因为不通工具中的传入指不相同, 因此采用闭包的方式自定义result进行处理, 并允许添加更多的功能.
   207  	// 例如在spray中, sender可以用来配置header等, 也可以进行特定的path拼接
   208  	// 如果sender留空只进行被动的指纹判断, 将无视rules中的senddata字段
   209  
   210  	ishttp := finger.Protocol == HTTPProtocol
   211  
   212  	// 主动阶段:遍历所有 rule,发送完整 send_data(记录首个命中,但不提前返回)
   213  	if frame, vuln, ok := finger.activeProbeAll(level, sender); ok {
   214  		return frame, vuln, true
   215  	}
   216  
   217  	// 被动阶段:不依赖主动发包结果
   218  	for i, rule := range finger.Rules {
   219  		hasFrame, hasVuln, ver, detail := RuleMatcher(rule, content, ishttp)
   220  		if hasFrame {
   221  			frame, vuln := finger.ToResult(hasFrame, hasVuln, ver, i)
   222  			if finger.Focus {
   223  				frame.IsFocus = true
   224  			}
   225  			for _, tag := range finger.Tags {
   226  				frame.AddTag(tag)
   227  			}
   228  			finger.annotateRuleHit(frame, i, detail, "")
   229  			return frame, vuln, true
   230  		}
   231  	}
   232  	return nil, nil, false
   233  }
   234  
   235  func (finger *Finger) PassiveMatch(content *Content) (*common.Framework, *common.Vuln, bool) {
   236  	for i, rule := range finger.Rules {
   237  		var ishttp bool
   238  		if finger.Protocol == HTTPProtocol {
   239  			ishttp = true
   240  		}
   241  
   242  		hasFrame, hasVuln, ver, detail := RuleMatcher(rule, content, ishttp)
   243  		if hasFrame {
   244  			frame, vuln := finger.ToResult(hasFrame, hasVuln, ver, i)
   245  			if finger.Focus {
   246  				frame.IsFocus = true
   247  			}
   248  			//if vuln == nil && isactive {
   249  			//	vuln = &common.Vuln{Name: finger.Name + " detect", SeverityLevel: INFO, Detail: map[string]interface{}{"path": rule.SendDataStr}}
   250  			//}
   251  
   252  			for _, tag := range finger.Tags {
   253  				frame.AddTag(tag)
   254  			}
   255  			finger.annotateRuleHit(frame, i, detail, "")
   256  			return frame, vuln, true
   257  		}
   258  	}
   259  	return nil, nil, false
   260  }
   261  
   262  func (finger *Finger) ActiveMatch(level int, sender Sender) (*common.Framework, *common.Vuln, bool) {
   263  	return finger.activeProbeAll(level, sender)
   264  }
   265  
   266  func matchFaviconRule(rule *Rule, raw []byte) (bool, *common.MatchDetail) {
   267  	if rule == nil || rule.Favicon == nil {
   268  		return false, nil
   269  	}
   270  
   271  	body := extractBody(raw)
   272  	if len(body) == 0 {
   273  		return false, nil
   274  	}
   275  
   276  	if len(rule.Favicon.Mmh3) > 0 {
   277  		hash := encode.Mmh3Hash32(body)
   278  		for _, expected := range rule.Favicon.Mmh3 {
   279  			if hash == expected {
   280  				return true, &common.MatchDetail{
   281  					MatcherType:  "favicon_mmh3",
   282  					MatcherValue: hash,
   283  				}
   284  			}
   285  		}
   286  	}
   287  
   288  	if len(rule.Favicon.Md5) > 0 {
   289  		hash := encode.Md5Hash(body)
   290  		for _, expected := range rule.Favicon.Md5 {
   291  			if hash == expected {
   292  				return true, &common.MatchDetail{
   293  					MatcherType:  "favicon_md5",
   294  					MatcherValue: hash,
   295  				}
   296  			}
   297  		}
   298  	}
   299  
   300  	return false, nil
   301  }
   302  
   303  func extractBody(raw []byte) []byte {
   304  	if len(raw) == 0 {
   305  		return nil
   306  	}
   307  	body, _, ok := httputils.SplitHttpRaw(raw)
   308  	if ok && len(body) > 0 {
   309  		return body
   310  	}
   311  	return raw
   312  }