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 }