Files
spray/pkg/neutron.go
T
M09IcandClaude Opus 4.6 af0c2493e1 fix: prevent fuzzy-failed results from leaking to stdout and output file
- 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]>
2026-06-28 21:36:55 -07:00

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
}