mirror of
https://github.com/chainreactors/spray.git
synced 2026-10-24 17:04:26 +00:00
- Set bl.IsValid=false before putToFuzzy snapshot in BaseCompare, so the snapshot no longer passes the IsValid check in Output() - Gate file output on the same condition as console output: bl.IsValid || (r.Fuzzy && bl.IsFuzzy) - Bump deps: utils, utils/parsers, fingers (types migrated to parsers) - Remove local replace directives Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
331 lines
7.8 KiB
Go
331 lines
7.8 KiB
Go
package pkg
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/chainreactors/fingers/common"
|
|
"github.com/chainreactors/logs"
|
|
"github.com/chainreactors/neutron/operators"
|
|
"github.com/chainreactors/neutron/protocols"
|
|
"github.com/chainreactors/neutron/templates"
|
|
"github.com/chainreactors/utils/parsers"
|
|
yaml "sigs.k8s.io/yaml/goyaml.v3"
|
|
)
|
|
|
|
type PocMode int
|
|
|
|
const (
|
|
PocModeCheck PocMode = 1 << iota
|
|
PocModeBrute
|
|
PocModeAll = PocModeCheck | PocModeBrute
|
|
)
|
|
|
|
var (
|
|
neutronMu sync.RWMutex
|
|
neutronTemplates []*templates.Template
|
|
neutronFingerMap map[string][]*templates.Template // finger name → check templates
|
|
neutronBruteMap map[string][]*templates.Template // zombie/tag name → brute templates
|
|
neutronTimeout = 10
|
|
NeutronEnabled bool
|
|
NeutronPocMode PocMode
|
|
)
|
|
|
|
func LoadNeutronTemplates() error {
|
|
data := LoadConfig("neutron")
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var rawTemplates []interface{}
|
|
if err := yaml.Unmarshal(data, &rawTemplates); err != nil {
|
|
return err
|
|
}
|
|
|
|
var docs [][]byte
|
|
for _, tmpl := range rawTemplates {
|
|
doc, err := yaml.Marshal(tmpl)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
docs = append(docs, doc)
|
|
}
|
|
|
|
return loadNeutronDocs(docs)
|
|
}
|
|
|
|
func LoadNeutronTemplatesFromDir(dir string) error {
|
|
var docs [][]byte
|
|
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml") {
|
|
return nil
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
docs = append(docs, data)
|
|
return nil
|
|
})
|
|
return loadNeutronDocs(docs)
|
|
}
|
|
|
|
func loadNeutronDocs(docs [][]byte) error {
|
|
neutronMu.Lock()
|
|
defer neutronMu.Unlock()
|
|
|
|
neutronTemplates = nil
|
|
neutronFingerMap = make(map[string][]*templates.Template)
|
|
neutronBruteMap = make(map[string][]*templates.Template)
|
|
|
|
opts := &protocols.ExecuterOptions{
|
|
Options: &protocols.Options{
|
|
Timeout: neutronTimeout,
|
|
},
|
|
}
|
|
|
|
for _, doc := range docs {
|
|
var tmpl templates.Template
|
|
if err := yaml.Unmarshal(doc, &tmpl); err != nil {
|
|
continue
|
|
}
|
|
|
|
if tmpl.GetRequests() == nil && len(tmpl.RequestsNetwork) == 0 &&
|
|
len(tmpl.RequestsTCP) == 0 && len(tmpl.RequestsUDP) == 0 {
|
|
continue
|
|
}
|
|
|
|
if err := tmpl.Compile(opts); err != nil {
|
|
logs.Log.Debugf("neutron compile %s failed: %s", tmpl.Id, err.Error())
|
|
continue
|
|
}
|
|
|
|
t := &tmpl
|
|
neutronTemplates = append(neutronTemplates, t)
|
|
|
|
isBrute := tmpl.Info.Zombie != "" || hasPayloads(t)
|
|
|
|
if isBrute {
|
|
// brute template: index by zombie field and tags
|
|
if tmpl.Info.Zombie != "" {
|
|
key := strings.ToLower(tmpl.Info.Zombie)
|
|
neutronBruteMap[key] = append(neutronBruteMap[key], t)
|
|
}
|
|
for _, tag := range tmpl.GetTags() {
|
|
key := strings.ToLower(strings.TrimSpace(tag))
|
|
if key != "" {
|
|
neutronBruteMap[key] = append(neutronBruteMap[key], t)
|
|
}
|
|
}
|
|
// also index by finger field if present
|
|
for _, finger := range tmpl.Fingers {
|
|
key := strings.ToLower(strings.TrimSpace(finger))
|
|
if key != "" {
|
|
neutronBruteMap[key] = append(neutronBruteMap[key], t)
|
|
}
|
|
}
|
|
} else {
|
|
// check template: index by finger field and tags
|
|
for _, finger := range tmpl.Fingers {
|
|
key := strings.ToLower(strings.TrimSpace(finger))
|
|
if key != "" {
|
|
neutronFingerMap[key] = append(neutronFingerMap[key], t)
|
|
}
|
|
}
|
|
for _, tag := range tmpl.GetTags() {
|
|
key := strings.ToLower(strings.TrimSpace(tag))
|
|
if key != "" {
|
|
neutronFingerMap[key] = append(neutronFingerMap[key], t)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
logs.Log.Importantf("loaded %d neutron templates (check: %d fingers, brute: %d services)",
|
|
len(neutronTemplates), len(neutronFingerMap), len(neutronBruteMap))
|
|
return nil
|
|
}
|
|
|
|
func hasPayloads(t *templates.Template) bool {
|
|
for _, req := range t.GetRequests() {
|
|
if len(req.Payloads) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NeutronCheck runs vulnerability check templates matching the given fingerprints.
|
|
func NeutronCheck(baseURL string, frameworks common.Frameworks) []*PocResult {
|
|
if !NeutronEnabled || NeutronPocMode&PocModeCheck == 0 {
|
|
return nil
|
|
}
|
|
|
|
neutronMu.RLock()
|
|
defer neutronMu.RUnlock()
|
|
|
|
seen := make(map[string]bool)
|
|
var results []*PocResult
|
|
|
|
for name := range frameworks {
|
|
key := strings.ToLower(name)
|
|
tmplList, ok := neutronFingerMap[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, tmpl := range tmplList {
|
|
if seen[tmpl.Id] {
|
|
continue
|
|
}
|
|
seen[tmpl.Id] = true
|
|
|
|
result, err := tmpl.Execute(baseURL, nil)
|
|
if err != nil {
|
|
logs.Log.Debugf("neutron check %s on %s: %s", tmpl.Id, baseURL, err.Error())
|
|
continue
|
|
}
|
|
if result != nil && result.Matched {
|
|
results = append(results, &PocResult{
|
|
TemplateID: tmpl.Id,
|
|
Name: tmpl.Info.Name,
|
|
Severity: tmpl.Info.Severity,
|
|
Tags: tmpl.GetTags(),
|
|
Matched: true,
|
|
Extracts: result.OutputExtracts,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// NeutronBrute runs brute/login templates matching the given fingerprints.
|
|
func NeutronBrute(baseURL string, frameworks common.Frameworks) []*PocResult {
|
|
if !NeutronEnabled || NeutronPocMode&PocModeBrute == 0 {
|
|
return nil
|
|
}
|
|
|
|
neutronMu.RLock()
|
|
defer neutronMu.RUnlock()
|
|
|
|
seen := make(map[string]bool)
|
|
var results []*PocResult
|
|
|
|
for name := range frameworks {
|
|
key := strings.ToLower(name)
|
|
tmplList, ok := neutronBruteMap[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, tmpl := range tmplList {
|
|
if seen[tmpl.Id] {
|
|
continue
|
|
}
|
|
seen[tmpl.Id] = true
|
|
|
|
result, err := tmpl.Execute(baseURL, nil)
|
|
if err != nil {
|
|
logs.Log.Debugf("neutron brute %s on %s: %s", tmpl.Id, baseURL, err.Error())
|
|
continue
|
|
}
|
|
if result != nil && result.Matched {
|
|
pr := &PocResult{
|
|
TemplateID: tmpl.Id,
|
|
Name: tmpl.Info.Name,
|
|
Severity: tmpl.Info.Severity,
|
|
Tags: tmpl.GetTags(),
|
|
Matched: true,
|
|
Extracts: result.OutputExtracts,
|
|
}
|
|
if result.PayloadValues != nil {
|
|
pr.Payload = result.PayloadValues
|
|
}
|
|
results = append(results, pr)
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// NeutronScan runs both check and brute templates based on configured mode.
|
|
func NeutronScan(baseURL string, frameworks common.Frameworks) []*PocResult {
|
|
if !NeutronEnabled || len(frameworks) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var results []*PocResult
|
|
results = append(results, NeutronCheck(baseURL, frameworks)...)
|
|
results = append(results, NeutronBrute(baseURL, frameworks)...)
|
|
return results
|
|
}
|
|
|
|
// PocResultToExtracteds converts POC results to parsers.Extracteds for output.
|
|
func PocResultToExtracteds(results []*PocResult) parsers.Extracteds {
|
|
if len(results) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var extracteds parsers.Extracteds
|
|
for _, r := range results {
|
|
extracted := &parsers.Extracted{
|
|
Name: "poc:" + r.TemplateID,
|
|
}
|
|
extracted.ExtractResult = append(extracted.ExtractResult,
|
|
"["+r.Severity+"] "+r.Name)
|
|
if len(r.Extracts) > 0 {
|
|
extracted.ExtractResult = append(extracted.ExtractResult, r.Extracts...)
|
|
}
|
|
if r.Payload != nil {
|
|
for k, v := range r.Payload {
|
|
if s, ok := v.(string); ok {
|
|
extracted.ExtractResult = append(extracted.ExtractResult, k+"="+s)
|
|
}
|
|
}
|
|
}
|
|
extracteds = append(extracteds, extracted)
|
|
}
|
|
return extracteds
|
|
}
|
|
|
|
// PocResultToVulns converts POC results to common.Vulns for structured output.
|
|
func PocResultToVulns(results []*PocResult) common.Vulns {
|
|
if len(results) == 0 {
|
|
return nil
|
|
}
|
|
|
|
vulns := make(common.Vulns)
|
|
for _, r := range results {
|
|
vuln := &common.Vuln{
|
|
Name: r.TemplateID,
|
|
Tags: r.Tags,
|
|
SeverityLevel: common.GetSeverityLevel(r.Severity),
|
|
}
|
|
if r.Payload != nil {
|
|
vuln.Payload = r.Payload
|
|
}
|
|
if len(r.Extracts) > 0 {
|
|
vuln.Detail = map[string][]string{
|
|
"extracts": r.Extracts,
|
|
}
|
|
}
|
|
vulns.Add(vuln)
|
|
}
|
|
return vulns
|
|
}
|
|
|
|
type PocResult struct {
|
|
TemplateID string
|
|
Name string
|
|
Severity string
|
|
Tags []string
|
|
Matched bool
|
|
Extracts []string
|
|
Payload map[string]interface{}
|
|
Result *operators.Result
|
|
}
|