yellow~ opinionated web scanning pipeline for pentesters in a hurry |
| git clone https://github.com/djnnvx/yellow.git |
| log | files | refs | archive | README | LICENSE |
portscan.go (5769B)
1 package scan 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net" 8 "net/netip" 9 "os" 10 "sort" 11 "strconv" 12 "strings" 13 "sync" 14 "time" 15 16 helper "evil.djnn.sh/djnn/yellow/helpers" 17 "github.com/praetorian-inc/nerva/pkg/plugins" 18 nervascan "github.com/praetorian-inc/nerva/pkg/scan" 19 ) 20 21 type PortScanResult struct { 22 Target string `json:"target"` 23 CDN *helper.CDNInfo `json:"cdn,omitempty"` 24 ScannedAt time.Time `json:"scanned_at"` 25 OpenPorts []PortResult `json:"open_ports"` 26 } 27 28 type SecurityFinding struct { 29 ID string `json:"id"` 30 Severity string `json:"severity"` 31 Description string `json:"description"` 32 Evidence string `json:"evidence,omitempty"` 33 } 34 35 type PortResult struct { 36 Port int `json:"port"` 37 Proto string `json:"proto"` 38 Service string `json:"service,omitempty"` 39 Version string `json:"version,omitempty"` 40 Findings []SecurityFinding `json:"findings,omitempty"` 41 } 42 43 type PortScanner struct { 44 scanPath string 45 ports string 46 timeout time.Duration 47 workers int 48 } 49 50 func (opts ScanOpts) runPortScan() { 51 ps := PortScanner{ 52 scanPath: opts.ScanPath + "/" + opts.Domain, 53 ports: opts.Ports, 54 timeout: 2 * time.Second, 55 workers: 100, 56 } 57 fmt.Printf("[+] Running port scan on %s\n", opts.Domain) 58 if !opts.DryRun { 59 if err := os.MkdirAll(ps.scanPath, 0755); err != nil { 60 fmt.Printf("[!] port scan: could not create %s: %v\n", ps.scanPath, err) 61 return 62 } 63 ps.Run(opts.Domain) 64 } 65 } 66 67 func (p *PortScanner) Run(host string) { 68 if net.ParseIP(host) == nil { 69 addrs, err := net.LookupHost(host) 70 if err != nil || len(addrs) == 0 { 71 fmt.Printf("err: could not resolve %s to an IP: %v\n", host, err) 72 return 73 } 74 host = addrs[0] 75 } 76 77 cdn := helper.LookupCDN(net.ParseIP(host)) 78 if helper.SkipPortScan(cdn) { 79 fmt.Printf("[SCAN %s] behind %s/%s, skipping port scan\n\n", host, cdn.Type, cdn.Provider) 80 p.write(PortScanResult{Target: host, CDN: cdn, ScannedAt: time.Now().UTC()}) 81 return 82 } 83 if cdn != nil { 84 fmt.Printf("[SCAN %s] %s/%s range, scanning anyway\n", host, cdn.Type, cdn.Provider) 85 } 86 87 ports, err := parsePorts(p.ports) 88 if err != nil { 89 fmt.Printf("[!] invalid ports spec: %v\n", err) 90 return 91 } 92 93 open := p.tcpScan(host, ports) 94 95 p.write(PortScanResult{ 96 Target: host, 97 CDN: cdn, 98 ScannedAt: time.Now().UTC(), 99 OpenPorts: p.fingerprint(host, open), 100 }) 101 102 fmt.Printf("[SCAN %s] port scan done — %d open ports\n\n", host, len(open)) 103 } 104 105 func (p *PortScanner) write(out PortScanResult) { 106 data, _ := json.MarshalIndent(out, "", " ") 107 if err := os.WriteFile(p.scanPath+"/portscan.json", data, 0644); err != nil { 108 fmt.Printf("[!] port scan: could not write %s/portscan.json: %v\n", p.scanPath, err) 109 } 110 } 111 112 func (p *PortScanner) tcpScan(host string, ports []int) []int { 113 var mu sync.Mutex 114 var open []int 115 sem := make(chan struct{}, p.workers) 116 var wg sync.WaitGroup 117 118 for _, port := range ports { 119 wg.Add(1) 120 sem <- struct{}{} 121 go func(port int) { 122 defer wg.Done() 123 defer func() { <-sem }() 124 addr := net.JoinHostPort(host, strconv.Itoa(port)) 125 conn, err := net.DialTimeout("tcp", addr, p.timeout) 126 if err == nil { 127 conn.Close() 128 mu.Lock() 129 open = append(open, port) 130 mu.Unlock() 131 } 132 }(port) 133 } 134 wg.Wait() 135 sort.Ints(open) 136 return open 137 } 138 139 func (p *PortScanner) fingerprint(host string, openPorts []int) []PortResult { 140 if len(openPorts) == 0 { 141 return nil 142 } 143 144 ip := net.ParseIP(host) 145 if ip == nil { 146 return fallbackResults(openPorts) 147 } 148 if v4 := ip.To4(); v4 != nil { 149 ip = v4 150 } 151 152 addr, ok := netip.AddrFromSlice(ip) 153 if !ok { 154 return fallbackResults(openPorts) 155 } 156 157 targets := make([]plugins.Target, len(openPorts)) 158 for i, port := range openPorts { 159 targets[i] = plugins.Target{ 160 Address: netip.AddrPortFrom(addr, uint16(port)), 161 Host: host, 162 } 163 } 164 165 cfg := nervascan.Config{ 166 DefaultTimeout: p.timeout, 167 Misconfigs: true, 168 } 169 nervaResults, err := nervascan.ScanTargets(context.Background(), targets, cfg) 170 if err != nil { 171 return fallbackResults(openPorts) 172 } 173 174 enriched := make(map[int]PortResult, len(nervaResults)) 175 for _, r := range nervaResults { 176 var findings []SecurityFinding 177 for _, f := range r.SecurityFindings { 178 findings = append(findings, SecurityFinding{ 179 ID: f.ID, 180 Severity: string(f.Severity), 181 Description: f.Description, 182 Evidence: f.Evidence, 183 }) 184 } 185 enriched[r.Port] = PortResult{ 186 Port: r.Port, 187 Proto: "tcp", 188 Service: r.Protocol, 189 Version: r.Version, 190 Findings: findings, 191 } 192 } 193 194 return mergePortResults(openPorts, enriched) 195 } 196 197 func mergePortResults(openPorts []int, enriched map[int]PortResult) []PortResult { 198 results := make([]PortResult, len(openPorts)) 199 for i, port := range openPorts { 200 if r, ok := enriched[port]; ok { 201 results[i] = r 202 continue 203 } 204 results[i] = PortResult{Port: port, Proto: "tcp"} 205 } 206 return results 207 } 208 209 func fallbackResults(openPorts []int) []PortResult { 210 results := make([]PortResult, len(openPorts)) 211 for i, port := range openPorts { 212 results[i] = PortResult{Port: port, Proto: "tcp"} 213 } 214 return results 215 } 216 217 func parsePorts(spec string) ([]int, error) { 218 var ports []int 219 for _, part := range strings.Split(spec, ",") { 220 part = strings.TrimSpace(part) 221 if strings.Contains(part, "-") { 222 bounds := strings.SplitN(part, "-", 2) 223 lo, err1 := strconv.Atoi(bounds[0]) 224 hi, err2 := strconv.Atoi(bounds[1]) 225 if err1 != nil || err2 != nil || lo > hi { 226 return nil, fmt.Errorf("invalid range: %s", part) 227 } 228 for p := lo; p <= hi; p++ { 229 ports = append(ports, p) 230 } 231 } else { 232 p, err := strconv.Atoi(part) 233 if err != nil { 234 return nil, fmt.Errorf("invalid port: %s", part) 235 } 236 ports = append(ports, p) 237 } 238 } 239 return ports, nil 240 }