yellow.git


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 }