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 }