func NormalizeHost(host string) (string, error) { return normalizeOwnedHost(host) } func runOwnedTargetScan(req ScanRequest, scanID string, emit StreamEmit) (*ScanReport, error) { host, err := normalizeOwnedHost(req.Host) if err != nil { return nil, err } uxProfile := NormalizeProfile(req.Profile) opts := ProfileOptions(uxProfile) portsToScan, profilesUsed := ResolveScanPortsFromRequest(req) if uxProfile == ProfileQuick && len(req.Profiles) == 0 && len(portsToScan) > 10 { portsToScan = portsToScan[:10] } report := &ScanReport{ScanID: scanID, Host: host, Profile: uxProfile, ProfilesUsed: profilesUsed, Status: "complete", ScannedAt: time.Now().UTC()} if !opts.SkipPorts { ports := scanPortsList(host, portsToScan) report.Ports = ports report.Banners = GrabBanners(host, ports) } if shouldCrawlProfile(req, uxProfile, report.Ports, opts) { crawl, err := crawlWithOptions(host, req.Port, req.Scheme, req.Paths, opts, scanID, emit) if err == nil && crawl != nil { report.Crawl = crawl report.Stack = MergeStack(crawl.Stack) } } report.DeployKitLane = SuggestDeployKitLane(report.Stack) cloudMeta := containsPortProfile(profilesUsed, PortProfileCloudMetadata) report.Recommendations = BuildRecommendations(report.Ports, report.Crawl, report.Stack, host, cloudMeta) return report, nil } const (ProfileQuick = "quick"; ProfileDeep = "deep"; ProfileSSRFOnly = "ssrf_only") type StreamEmit func(eventType string, payload map[string]interface{}) type ScanOptions struct { Ports []int; MaxPages, CrawlDepth int; SkipPorts, SkipCrawl bool } func NormalizeProfile(profile string) string { switch profile { case ProfileQuick, ProfileDeep, ProfileSSRFOnly: return profile; default: return "" } } func ProfileOptions(profile string) ScanOptions { switch NormalizeProfile(profile) { case ProfileQuick: p := FleetPorts if len(p) > 10 { p = p[:10] } return ScanOptions{Ports: p, MaxPages: 1, CrawlDepth: 0} case ProfileDeep: return ScanOptions{Ports: FleetPorts, MaxPages: 50, CrawlDepth: 2} case ProfileSSRFOnly: return ScanOptions{SkipPorts: true, MaxPages: 50, CrawlDepth: 2} default: return ScanOptions{Ports: FleetPorts, MaxPages: DefaultCrawlMaxPages, CrawlDepth: DefaultCrawlDepth} } } func ScanStream(req ScanRequest, scanID string, emit StreamEmit) (*ScanReport, error) { return runOwnedTargetScan(req, scanID, emit) } func scanPortsList(host string, ports []int) []PortResult { if len(ports) == 0 { return ScanPorts(host, nil) } out := make([]PortResult, 0, len(ports)) for _, port := range ports { out = append(out, PortResult{Port: port, Open: dialPort(host, port, DefaultPortDialTimeout)}) } return out } func shouldCrawlProfile(req ScanRequest, profile string, ports []PortResult, opts ScanOptions) bool { if opts.SkipCrawl { return false } if profile == ProfileSSRFOnly { return true } return shouldCrawl(req, ports) } func crawlWithOptions(host string, port int, scheme string, seedPaths []string, opts ScanOptions, scanID string, emit StreamEmit) (*CrawlReport, error) { scheme = normalizeScheme(scheme, port) if port <= 0 { port = defaultPortForScheme(scheme) } base := fmt.Sprintf("%s://%s", scheme, joinHostPort(host, port)) seeds := seedPaths if len(seeds) == 0 { seeds = []string{"/"} } maxPages, maxDepth := opts.MaxPages, opts.CrawlDepth if maxPages <= 0 { maxPages = DefaultCrawlMaxPages } if maxDepth < 0 { maxDepth = DefaultCrawlDepth } report := &CrawlReport{} var headerSnaps []HTTPHeaderSnap var htmlBodies []string visited := map[string]bool{} queue := []queuedURL{} for _, p := range seeds { if abs, err := resolveSameOrigin(base, p); err == nil { queue = append(queue, queuedURL{url: abs, depth: 0}) } } for len(queue) > 0 && report.PagesFetched < maxPages { item := queue[0]; queue = queue[1:] key := normalizeURLKey(item.url) if visited[key] { continue } visited[key] = true status, body, headers, err := fetchPage(item.url) if err != nil { continue } report.PagesFetched++ title, _ := htmlParseTitle(body) report.Pages = append(report.Pages, PageFinding{URL: item.url, StatusCode: status, Title: title}) if len(headers) > 0 { headerSnaps = append(headerSnaps, HTTPHeaderSnap{URL: item.url, Headers: headers}) } htmlBodies = append(htmlBodies, body) files, multi, fields, pageScore, cms := ParseHTML(item.url, body) report.FileInputs = append(report.FileInputs, files...) report.MultipartForms = append(report.MultipartForms, multi...) report.URLFields = append(report.URLFields, fields...) report.SSRFScore += pageScore report.CMSFingerprints = mergeCMS(report.CMSFingerprints, cms) if item.depth >= maxDepth { continue } for _, link := range extractLinks(body) { abs, err := resolveSameOrigin(base, link) if err != nil || !sameOrigin(base, abs) { continue } if !visited[normalizeURLKey(abs)] { queue = append(queue, queuedURL{url: abs, depth: item.depth + 1}) } } } if report.SSRFScore > 100 { report.SSRFScore = 100 } report.CMSFingerprints = mergeCMS(nil, report.CMSFingerprints) report.Stack = BuildStack(headerSnaps, htmlBodies) return report, nil } func OpenPorts(ports []PortResult) []int { var o []int; for _, p := range ports { if p.Open { o = append(o, p.Port) } }; return o } func DiffReports(prev, cur *ScanReport) *ReconScanDiff { if cur == nil { return nil } d := &ReconScanDiff{} if prev == nil { d.NewPorts = OpenPorts(cur.Ports); return d } po := map[int]bool{} for _, p := range prev.Ports { if p.Open { po[p.Port] = true } } for _, p := range cur.Ports { if p.Open && !po[p.Port] { d.NewPorts = append(d.NewPorts, p.Port) } } return d } func BuildHistory(rows []*ScanReport) []ReconHistoryEntry { out := make([]ReconHistoryEntry, 0, len(rows)) var prev *ScanReport for _, r := range rows { out = append(out, ReconHistoryEntry{ScanID: r.ScanID, Host: r.Host, Profile: r.Profile, Status: r.Status, ScannedAt: r.ScannedAt, Report: r, Diff: DiffReports(prev, r)}); prev = r } return out } func ReportToPDF(report *ScanReport) []byte { return []byte("AetherForge Recon\n") } var metaGeneratorRe = regexp.MustCompile(`(?is)]+name=["']generator["'][^>]+content=["']([^"']+)["']`) var bannerTitleRe = regexp.MustCompile(`(?is)]*>(.*?)`) var probeSSHBannerFn func(host string, port int) string var probeHTTPTitleFn func(host string, port int) (string, string) var probeWinRMHintFn func(host string, port int) string var probeEC2MetaFn func() bool func SetBannerHooks(ssh func(host string, port int) string, httpTitle func(host string, port int) (string, string), winrm func(host string, port int) string, ec2Meta func() bool) { probeSSHBannerFn, probeHTTPTitleFn, probeWinRMHintFn, probeEC2MetaFn = ssh, httpTitle, winrm, ec2Meta } func BuildStack(snaps []HTTPHeaderSnap, htmlBodies []string) []StackEntry { seen := map[string]bool{} var out []StackEntry add := func(name, source, detail string) { key := strings.ToLower(name) + "|" + source; if !seen[key] { seen[key] = true; out = append(out, StackEntry{Name: name, Source: source, Detail: detail}) } } for _, snap := range snaps { for k, v := range snap.Headers { switch strings.ToLower(k) { case "server": add(v, "header:Server", snap.URL); case "x-powered-by": add(v, "header:X-Powered-By", snap.URL) } } } for _, body := range htmlBodies { if m := metaGeneratorRe.FindStringSubmatch(body); len(m) > 1 { add(strings.TrimSpace(m[1]), "meta:generator", "") } if strings.Contains(strings.ToLower(body), "react") { add("React", "html:js", "") } } return out } func MergeStack(parts ...[]StackEntry) []StackEntry { seen := map[string]bool{}; var out []StackEntry; for _, part := range parts { for _, e := range part { key := strings.ToLower(e.Name) + "|" + e.Source; if !seen[key] { seen[key] = true; out = append(out, e) } } }; return out } func SuggestDeployKitLane(stack []StackEntry) string { for _, e := range stack { n := strings.ToLower(e.Name); if strings.Contains(n, "php") { return "php" }; if strings.Contains(n, "node") || strings.Contains(n, "react") { return "node" }; if strings.Contains(n, "nginx") { return "nginx" } } if len(stack) > 0 { return "static" } return "" } func GrabBanners(host string, ports []PortResult) []PortBanner { open := map[int]bool{}; for _, p := range ports { if p.Open { open[p.Port] = true } } var out []PortBanner if open[22] { if b := probeSSHBanner(host, 22); b != "" { out = append(out, PortBanner{Port: 22, Service: "ssh", Banner: b}) } } for _, port := range []int{80, 443, 6262, 8080} { if !open[port] { continue } title, server := probeHTTPBanner(host, port) if title != "" || server != "" { b := PortBanner{Port: port, Service: "http", Title: title, Banner: server, Hint: "origin_host"}; if looksLikeCDN(server) { b.Hint = "cdn_fronted" }; out = append(out, b) } } if open[5985] { if h := probeWinRMHint(host, 5985); h != "" { out = append(out, PortBanner{Port: 5985, Service: "winrm", Hint: h}) } } return out } func looksLikeCDN(s string) bool { s = strings.ToLower(s); return strings.Contains(s, "cloudflare") || strings.Contains(s, "cloudfront") } func probeSSHBanner(host string, port int) string { if probeSSHBannerFn != nil { return probeSSHBannerFn(host, port) } c, e := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), DefaultPortDialTimeout) if e != nil { return "" }; defer c.Close(); _ = c.SetReadDeadline(time.Now().Add(DefaultPortDialTimeout)) line, e := bufio.NewReader(c).ReadString('\n'); if e != nil { return "" }; return strings.TrimSpace(line) } func probeHTTPBanner(host string, port int) (string, string) { if probeHTTPTitleFn != nil { return probeHTTPTitleFn(host, port) } scheme := "http"; if port == 443 { scheme = "https" } resp, err := http.Get(fmt.Sprintf("%s://%s/", scheme, joinHostPort(host, port))) if err != nil { return "", "" }; defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192)) title := ""; if m := bannerTitleRe.FindStringSubmatch(string(body)); len(m) > 1 { title = strings.TrimSpace(m[1]) } return title, strings.TrimSpace(resp.Header.Get("Server")) } func probeWinRMHint(host string, port int) string { if probeWinRMHintFn != nil { return probeWinRMHintFn(host, port) } c, e := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), DefaultPortDialTimeout) if e != nil { return "" }; _ = c.Close(); return "winrm_listening" } func ProbeEC2Metadata() bool { if probeEC2MetaFn != nil { return probeEC2MetaFn() } c := &http.Client{Timeout: 800 * time.Millisecond} r, e := c.Get("http://169.254.169.254/latest/meta-data/") if e != nil { return false }; defer r.Body.Close() return r.StatusCode == http.StatusOK || r.StatusCode == http.StatusUnauthorized }