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

asn.go (6115B)

      1 package osint
      2 
      3 import (
      4 	"encoding/json"
      5 	"fmt"
      6 	"net"
      7 	"os"
      8 	"strings"
      9 
     10 	"evil.djnn.sh/djnn/yellow/core"
     11 	helper "evil.djnn.sh/djnn/yellow/helpers"
     12 )
     13 
     14 type ASNRecord struct {
     15 	Input           string `json:"input"`
     16 	IP              string `json:"ip"`
     17 	ASN             string `json:"asn"`
     18 	Org             string `json:"org,omitempty"`
     19 	Country         string `json:"country,omitempty"`
     20 	Registry        string `json:"registry,omitempty"`
     21 	Prefix          string `json:"prefix"`
     22 	HostingProvider bool   `json:"hosting_provider"`
     23 	Source          string `json:"source"`
     24 }
     25 
     26 type asnOrigin struct {
     27 	ASN      string
     28 	Prefix   string
     29 	Country  string
     30 	Registry string
     31 	Org      string
     32 }
     33 
     34 type asnProvider struct {
     35 	name  string
     36 	query func(reversed string) string
     37 	parse func(txt string) (asnOrigin, bool)
     38 }
     39 
     40 var asnProviders = []asnProvider{
     41 	{
     42 		name:  "cymru",
     43 		query: func(r string) string { return r + ".origin.asn.cymru.com" },
     44 		parse: parseCymruOrigin,
     45 	},
     46 	{
     47 		name:  "shadowserver",
     48 		query: func(r string) string { return r + ".origin.asn.shadowserver.org" },
     49 		parse: parseShadowserverOrigin,
     50 	},
     51 }
     52 
     53 type ASN struct {
     54 	lookupTXT func(string) ([]string, error)
     55 	providers []asnProvider
     56 }
     57 
     58 func (*ASN) Name() string { return "asn" }
     59 
     60 func (a *ASN) Run(ctx *core.Context) error {
     61 	fmt.Println("[+] Running asn lookup on", ctx.Domain)
     62 	if ctx.DryRun {
     63 		return nil
     64 	}
     65 
     66 	if a.lookupTXT == nil {
     67 		a.lookupTXT = net.LookupTXT
     68 	}
     69 	if a.providers == nil {
     70 		a.providers = asnProviders
     71 	}
     72 
     73 	records := []ASNRecord{}
     74 	seenIP := map[string]bool{}
     75 	asnOrgs := map[string]string{}
     76 
     77 	for _, candidate := range ctx.Domains {
     78 		input := strings.TrimSpace(candidate)
     79 		if input == "" {
     80 			continue
     81 		}
     82 
     83 		ip := input
     84 		if net.ParseIP(ip) == nil {
     85 			addrs, err := net.LookupHost(ip)
     86 			if err != nil || len(addrs) == 0 {
     87 				continue
     88 			}
     89 			ip = addrs[0]
     90 		}
     91 
     92 		parsed := net.ParseIP(ip)
     93 		if parsed == nil || parsed.To4() == nil {
     94 			fmt.Printf("[!] asn: %s is not IPv4, skipping\n", ip)
     95 			continue
     96 		}
     97 		if seenIP[ip] {
     98 			continue
     99 		}
    100 		seenIP[ip] = true
    101 
    102 		origin, source, ok := a.origin(ip)
    103 		if !ok {
    104 			continue
    105 		}
    106 
    107 		org := origin.Org
    108 		if org == "" {
    109 			org = a.asnOrg(origin.ASN, asnOrgs)
    110 		}
    111 
    112 		rec := ASNRecord{
    113 			Input:           input,
    114 			IP:              ip,
    115 			ASN:             "AS" + origin.ASN,
    116 			Org:             org,
    117 			Country:         origin.Country,
    118 			Registry:        origin.Registry,
    119 			Prefix:          origin.Prefix,
    120 			HostingProvider: isHostingProvider(org, parsed),
    121 			Source:          source,
    122 		}
    123 		records = append(records, rec)
    124 
    125 		flag := ""
    126 		if rec.HostingProvider {
    127 			flag = " [provider-owned, NOT client scope]"
    128 		}
    129 		fmt.Printf("[OSINT %s] %s %s %s%s\n", ip, rec.ASN, rec.Prefix, org, flag)
    130 	}
    131 
    132 	outfile := fmt.Sprintf("%s/asn.json", ctx.ScanPath)
    133 	data, _ := json.MarshalIndent(records, "", "\t")
    134 	if err := os.WriteFile(outfile, data, 0644); err != nil {
    135 		fmt.Printf("[!] asn: could not write %s: %v\n", outfile, err)
    136 	}
    137 
    138 	uniqueASN := map[string]bool{}
    139 	hosted := 0
    140 	for _, r := range records {
    141 		uniqueASN[r.ASN] = true
    142 		if r.HostingProvider {
    143 			hosted++
    144 		}
    145 	}
    146 	fmt.Printf("[OSINT %s] asn: %d prefix(es) across %d ASN(s), %d provider-owned, in %s\n\n",
    147 		ctx.Domain, len(records), len(uniqueASN), hosted, outfile)
    148 	return nil
    149 }
    150 
    151 func (a *ASN) origin(ip string) (asnOrigin, string, bool) {
    152 	reversed := reverseIPv4(ip)
    153 	if reversed == "" {
    154 		return asnOrigin{}, "", false
    155 	}
    156 
    157 	for _, p := range a.providers {
    158 		txts, err := a.lookupTXT(p.query(reversed))
    159 		if err != nil || len(txts) == 0 {
    160 			continue
    161 		}
    162 		for _, txt := range txts {
    163 			if origin, ok := p.parse(txt); ok {
    164 				return origin, p.name, true
    165 			}
    166 		}
    167 	}
    168 	return asnOrigin{}, "", false
    169 }
    170 
    171 func (a *ASN) asnOrg(asn string, cache map[string]string) string {
    172 	if org, ok := cache[asn]; ok {
    173 		return org
    174 	}
    175 	txts, err := a.lookupTXT("AS" + asn + ".asn.cymru.com")
    176 	org := ""
    177 	if err == nil {
    178 		for _, txt := range txts {
    179 			if name := parseCymruASName(txt); name != "" {
    180 				org = name
    181 				break
    182 			}
    183 		}
    184 	}
    185 	cache[asn] = org
    186 	return org
    187 }
    188 
    189 // "15169 | 8.8.8.0/24 | US | arin | 1992-12-01"
    190 func parseCymruOrigin(txt string) (asnOrigin, bool) {
    191 	f := splitCymru(txt)
    192 	if len(f) < 2 || f[0] == "" || f[1] == "" {
    193 		return asnOrigin{}, false
    194 	}
    195 	o := asnOrigin{ASN: firstField(f[0]), Prefix: f[1]}
    196 	if len(f) > 2 {
    197 		o.Country = f[2]
    198 	}
    199 	if len(f) > 3 {
    200 		o.Registry = f[3]
    201 	}
    202 	return o, o.ASN != ""
    203 }
    204 
    205 // "15169 | 8.8.8.0/24 | AS15169 | US | Google LLC"
    206 func parseShadowserverOrigin(txt string) (asnOrigin, bool) {
    207 	f := splitCymru(txt)
    208 	if len(f) < 2 || f[0] == "" || f[1] == "" {
    209 		return asnOrigin{}, false
    210 	}
    211 	o := asnOrigin{ASN: firstField(f[0]), Prefix: f[1]}
    212 	if len(f) > 3 {
    213 		o.Country = f[3]
    214 	}
    215 	if len(f) > 4 {
    216 		o.Org = f[4]
    217 	}
    218 	return o, o.ASN != ""
    219 }
    220 
    221 // "20473 | US | arin | 2001-05-11 | AS-VULTR - The Constant Company, LLC, US"
    222 func parseCymruASName(txt string) string {
    223 	f := splitCymru(txt)
    224 	if len(f) < 5 {
    225 		return ""
    226 	}
    227 	return f[4]
    228 }
    229 
    230 func splitCymru(txt string) []string {
    231 	parts := strings.Split(txt, "|")
    232 	for i := range parts {
    233 		parts[i] = strings.TrimSpace(parts[i])
    234 	}
    235 	return parts
    236 }
    237 
    238 // an IP can be announced by several ASNs: "1234 5678 | prefix | ..."
    239 func firstField(s string) string {
    240 	if i := strings.IndexByte(s, ' '); i >= 0 {
    241 		return s[:i]
    242 	}
    243 	return s
    244 }
    245 
    246 func reverseIPv4(ip string) string {
    247 	v4 := net.ParseIP(ip).To4()
    248 	if v4 == nil {
    249 		return ""
    250 	}
    251 	return fmt.Sprintf("%d.%d.%d.%d", v4[3], v4[2], v4[1], v4[0])
    252 }
    253 
    254 // cdncheck misses several hosting providers (Vultr among them), so the AS name
    255 // is checked too.
    256 var hostingKeywords = []string{
    257 	"amazon", "aws", "google", "microsoft", "azure", "oracle", "alibaba",
    258 	"digitalocean", "linode", "vultr", "constant company", "hetzner", "ovh",
    259 	"scaleway", "leaseweb", "rackspace", "cloudflare", "akamai", "fastly",
    260 	"contabo", "upcloud", "ionos", "godaddy", "hostinger",
    261 }
    262 
    263 func isHostingProvider(org string, ip net.IP) bool {
    264 	if helper.LookupCDN(ip) != nil {
    265 		return true
    266 	}
    267 	lower := strings.ToLower(org)
    268 	for _, kw := range hostingKeywords {
    269 		if strings.Contains(lower, kw) {
    270 			return true
    271 		}
    272 	}
    273 	return false
    274 }