github.com/chainreactors/fingers@v1.2.1/xray/xray.go (about) 1 // Package xray implements a fingerprint engine based on converted xray POCs. 2 // 3 // Unlike fingerprinthub which matches all templates against one response, 4 // the xray engine matches each template's request independently: 5 // - WebMatch (passive): only matches requests targeting path "/" 6 // - HTTPActiveMatch (active): sends each request to its specified path, 7 // caching responses by path to avoid duplicate requests 8 package xray 9 10 import ( 11 "bytes" 12 "encoding/json" 13 "fmt" 14 "io/ioutil" 15 "net/http" 16 "strings" 17 "sync" 18 19 "github.com/chainreactors/fingers/common" 20 "github.com/chainreactors/fingers/resources" 21 "github.com/chainreactors/logs" 22 "github.com/chainreactors/neutron/operators" 23 "github.com/chainreactors/neutron/protocols" 24 nhttp "github.com/chainreactors/neutron/protocols/http" 25 "github.com/chainreactors/neutron/templates" 26 "github.com/chainreactors/utils/httputils" 27 "gopkg.in/yaml.v3" 28 ) 29 30 const FrameFromXray common.From = common.From(20) 31 32 func init() { 33 common.FrameFromMap[FrameFromXray] = "xray" 34 } 35 36 // XrayEngine implements fingerprint matching using converted xray POC templates. 37 type XrayEngine struct { 38 templates []*templates.Template 39 executerOptions *protocols.ExecuterOptions 40 41 // CaseInsensitive 控制匹配时是否忽略大小写(默认 true)。 42 CaseInsensitive bool 43 } 44 45 // NewXrayEngine creates a new xray fingerprint engine from gzipped JSON data. 46 func NewXrayEngine(webData []byte) (*XrayEngine, error) { 47 engine := &XrayEngine{ 48 CaseInsensitive: true, 49 executerOptions: &protocols.ExecuterOptions{ 50 Options: &protocols.Options{Timeout: 10}, 51 }, 52 } 53 54 // xray data is not embedded; an empty engine is valid (templates are 55 // supplied later via the Provider layer). 56 if len(webData) == 0 { 57 return engine, nil 58 } 59 60 var rawTemplates []map[string]interface{} 61 if err := resources.UnmarshalData(webData, &rawTemplates); err != nil { 62 return nil, fmt.Errorf("unmarshal xray fingerprints: %w", err) 63 } 64 65 loaded, errs := engine.loadTemplates(rawTemplates) 66 if len(errs) > 0 && len(errs) < 10 { 67 for _, e := range errs { 68 logs.Log.Warn(e) 69 } 70 } 71 72 logs.Log.Infof("resources type=fingerprints source=xray templates=%d", loaded) 73 return engine, nil 74 } 75 76 func (e *XrayEngine) loadTemplates(data []map[string]interface{}) (int, []error) { 77 var loaded int 78 var errs []error 79 80 for _, raw := range data { 81 yb, err := yaml.Marshal(raw) 82 if err != nil { 83 errs = append(errs, fmt.Errorf("marshal: %w", err)) 84 continue 85 } 86 // templates.Load parses the (normally pre-converted) neutron template. 87 // Raw xray POCs are only auto-converted if the application opts in by 88 // importing github.com/chainreactors/neutron/convert, which registers 89 // the xray converter; without it, this package pulls in no conversion 90 // dependency and simply parses neutron-format templates. 91 tmpl, err := templates.Load(yb) 92 if err != nil { 93 errs = append(errs, fmt.Errorf("load: %w", err)) 94 continue 95 } 96 if err := e.compileTemplate(tmpl); err != nil { 97 for _, req := range tmpl.GetRequests() { 98 if compileErr := (&req.Operators).Compile(); compileErr != nil { 99 continue 100 } 101 req.CompiledOperators = &req.Operators 102 } 103 } 104 if tmpl.GetRequests() != nil { 105 e.templates = append(e.templates, tmpl) 106 loaded++ 107 } 108 } 109 return loaded, errs 110 } 111 112 func (e *XrayEngine) compileTemplate(tmpl *templates.Template) error { 113 if e.CaseInsensitive { 114 for _, req := range tmpl.GetRequests() { 115 for _, matcher := range req.Matchers { 116 if matcher.Type == "word" { 117 matcher.CaseInsensitive = true 118 } 119 } 120 } 121 } 122 return tmpl.Compile(e.executerOptions) 123 } 124 125 // --------------------------------------------------------------------------- 126 // EngineImpl interface 127 // --------------------------------------------------------------------------- 128 129 func (e *XrayEngine) Name() string { return "xray" } 130 func (e *XrayEngine) Len() int { return len(e.templates) } 131 func (e *XrayEngine) Compile() error { return nil } 132 func (e *XrayEngine) Capability() common.EngineCapability { 133 return common.EngineCapability{SupportWeb: true, SupportService: false} 134 } 135 136 // WebMatch performs passive fingerprint matching against an HTTP response. 137 // Only requests targeting path "/" are matched (other paths require active probing). 138 func (e *XrayEngine) WebMatch(content []byte) common.Frameworks { 139 resp := httputils.NewResponseWithRaw(content) 140 if resp == nil { 141 return make(common.Frameworks) 142 } 143 144 bodyStr := string(httputils.ReadBody(resp)) 145 if e.CaseInsensitive { 146 bodyStr = strings.ToLower(bodyStr) 147 } 148 event := e.buildEvent(resp, bodyStr, len(content)) 149 frames := make(common.Frameworks) 150 151 for _, tmpl := range e.templates { 152 if e.matchTemplatePassive(tmpl, event) { 153 frames.Add(e.newFramework(tmpl)) 154 } 155 } 156 return frames 157 } 158 159 // matchTemplatePassive checks if ANY root-path request in the template matches. 160 // Only requests with path "/" or "{{BaseURL}}/" are evaluated in passive mode. 161 func (e *XrayEngine) matchTemplatePassive(tmpl *templates.Template, event protocols.InternalEvent) bool { 162 for _, req := range tmpl.GetRequests() { 163 if !isRootPath(req) { 164 continue 165 } 166 if req.CompiledOperators == nil || len(req.CompiledOperators.Matchers) == 0 { 167 continue 168 } 169 if matchRequest(req, event) { 170 return true 171 } 172 } 173 return false 174 } 175 176 func isRootPath(req *nhttp.Request) bool { 177 if len(req.Path) == 0 { 178 return true 179 } 180 for _, p := range req.Path { 181 cleaned := strings.TrimPrefix(p, "{{BaseURL}}") 182 cleaned = strings.TrimSuffix(cleaned, "/") 183 if cleaned == "" || cleaned == "/" { 184 return true 185 } 186 } 187 return false 188 } 189 190 func matchRequest(req *nhttp.Request, event protocols.InternalEvent) bool { 191 cond := strings.ToLower(strings.TrimSpace(req.CompiledOperators.MatchersCondition)) 192 if cond == "" { 193 cond = "or" 194 } 195 196 anyMatched, allMatched := false, true 197 for _, matcher := range req.CompiledOperators.Matchers { 198 ok, _ := req.Match(event, matcher) 199 if ok { 200 anyMatched = true 201 } else { 202 allMatched = false 203 } 204 } 205 if cond == "and" { 206 return allMatched && len(req.CompiledOperators.Matchers) > 0 207 } 208 return anyMatched 209 } 210 211 func (e *XrayEngine) buildEvent(resp *http.Response, body string, contentLength int) protocols.InternalEvent { 212 event := make(protocols.InternalEvent) 213 event["body"] = body 214 event["status_code"] = resp.StatusCode 215 event["content_length"] = contentLength 216 217 var hdrBuilder strings.Builder 218 for k, vals := range resp.Header { 219 joined := strings.Join(vals, " ") 220 norm := strings.ToLower(strings.Replace(strings.TrimSpace(k), "-", "_", -1)) 221 if e.CaseInsensitive { 222 joined = strings.ToLower(joined) 223 } 224 event[norm] = joined 225 hdrBuilder.WriteString(norm) 226 hdrBuilder.WriteString(": ") 227 hdrBuilder.WriteString(joined) 228 hdrBuilder.WriteString("\n") 229 } 230 event["all_headers"] = hdrBuilder.String() 231 event["header"] = hdrBuilder.String() 232 return event 233 } 234 235 func (e *XrayEngine) newFramework(tmpl *templates.Template) *common.Framework { 236 name := tmpl.Info.Name 237 if name == "" { 238 name = tmpl.Id 239 } 240 frame := common.NewFramework(name, FrameFromXray) 241 if tmpl.Info.Metadata != nil { 242 if vendor, ok := tmpl.Info.Metadata["vendor"].(string); ok { 243 frame.Attributes.Vendor = vendor 244 } 245 if product, ok := tmpl.Info.Metadata["product"].(string); ok { 246 frame.Attributes.Product = product 247 } 248 } 249 return frame 250 } 251 252 // ServiceMatch is not supported by the xray engine. 253 func (e *XrayEngine) ServiceMatch(host, portStr string, level int, sender common.ServiceSender, callback common.ServiceCallback) *common.ServiceResult { 254 return nil 255 } 256 257 // --------------------------------------------------------------------------- 258 // Active matching with per-request dispatch and path-level caching 259 // --------------------------------------------------------------------------- 260 261 // cachedTransport caches HTTP responses by request path to avoid duplicate requests. 262 type cachedTransport struct { 263 transport http.RoundTripper 264 cache map[string]*cachedResp 265 mu sync.Mutex 266 } 267 268 type cachedResp struct { 269 resp *http.Response 270 body []byte 271 } 272 273 func (c *cachedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 274 key := req.URL.Path 275 if key == "" { 276 key = "/" 277 } 278 279 c.mu.Lock() 280 if cached, ok := c.cache[key]; ok { 281 c.mu.Unlock() 282 resp := *cached.resp 283 resp.Body = ioutil.NopCloser(bytes.NewReader(cached.body)) 284 resp.Request = req 285 return &resp, nil 286 } 287 c.mu.Unlock() 288 289 resp, err := c.transport.RoundTrip(req) 290 if err != nil { 291 return nil, err 292 } 293 294 bodyBytes, err := ioutil.ReadAll(resp.Body) 295 resp.Body.Close() 296 if err != nil { 297 return nil, err 298 } 299 300 cr := *resp 301 cr.Body = nil 302 c.mu.Lock() 303 c.cache[key] = &cachedResp{resp: &cr, body: bodyBytes} 304 c.mu.Unlock() 305 306 resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) 307 return resp, nil 308 } 309 310 // HTTPActiveMatch sends per-template per-request probes with path-level caching. 311 func (e *XrayEngine) HTTPActiveMatch(baseURL string, level int, transport http.RoundTripper, callback func(*common.Framework, *common.Vuln)) (common.Frameworks, common.Vulns) { 312 if baseURL == "" || transport == nil { 313 return nil, nil 314 } 315 316 allFrameworks := make(common.Frameworks) 317 ct := &cachedTransport{transport: transport, cache: make(map[string]*cachedResp)} 318 client := &http.Client{Transport: ct} 319 for _, tmpl := range e.templates { 320 if len(tmpl.RequestsHTTP) == 0 { 321 continue 322 } 323 324 // Pass the per-call client through execution instead of stashing it on the 325 // shared template's request objects — concurrent calls must not mutate the 326 // shared compiled templates. 327 result, err := tmpl.ExecuteWithClient(baseURL, nil, client) 328 if err == nil && result != nil && result.Matched { 329 frame := e.newFramework(tmpl) 330 allFrameworks.Add(frame) 331 if callback != nil { 332 callback(frame, nil) 333 } 334 } 335 } 336 return allFrameworks, nil 337 } 338 339 // LoadFromJSON loads templates from a raw JSON byte slice (for testing). 340 func (e *XrayEngine) LoadFromJSON(data []byte) error { 341 var raw []map[string]interface{} 342 if err := json.Unmarshal(data, &raw); err != nil { 343 return err 344 } 345 loaded, _ := e.loadTemplates(raw) 346 _ = loaded 347 return nil 348 } 349 350 // GetTemplateMatchersForRequest returns the set of matchers for a template, 351 // for a specific request (by index). 352 func GetTemplateMatchersForRequest(tmpl *templates.Template, reqIndex int) []*operators.Matcher { 353 reqs := tmpl.GetRequests() 354 if reqIndex < 0 || reqIndex >= len(reqs) { 355 return nil 356 } 357 req := reqs[reqIndex] 358 if req.CompiledOperators == nil { 359 return nil 360 } 361 return req.CompiledOperators.Matchers 362 }