yellow~ opinionated web scanning pipeline for pentesters in a hurry |
| git clone https://github.com/djnnvx/yellow.git |
| log | files | refs | archive | README | LICENSE |
shodan.go (6043B)
1 package osint 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "evil.djnn.sh/djnn/yellow/core" 17 "evil.djnn.sh/djnn/yellow/helpers" 18 "github.com/shadowscatcher/shodan" 19 "github.com/shadowscatcher/shodan/search" 20 ) 21 22 type Shodan struct { 23 outfile string 24 25 HTTPClient *http.Client 26 apiKey string 27 queried map[string]bool 28 } 29 30 func (s Shodan) ShouldRun() bool { 31 return s.apiKey != "" 32 } 33 34 func (s *Shodan) Info(target string) { 35 if s.apiKey == "" { 36 fmt.Println("[!] env value SHODAN_API_KEY not found. skipping shodan scan") 37 } else { 38 fmt.Println("[+] Running shodan on ", target) 39 } 40 } 41 42 func (*Shodan) Name() string { return "shodan" } 43 44 func (s *Shodan) Run(ctx *core.Context) error { 45 s.apiKey = os.Getenv("SHODAN_API_KEY") 46 s.outfile = fmt.Sprintf("%s/shodan.txt", ctx.ScanPath) 47 s.queried = map[string]bool{} 48 if s.HTTPClient == nil { 49 s.HTTPClient = &http.Client{Timeout: 15 * time.Second} 50 } 51 52 s.Info(ctx.Domain) 53 if ctx.DryRun || !s.ShouldRun() { 54 return nil 55 } 56 57 for _, d := range ctx.Domains { 58 s.scanIP(d) 59 } 60 return nil 61 } 62 63 func (s *Shodan) scanIP(ip string) { 64 ip = strings.TrimSpace(ip) 65 if ip == "" { 66 fmt.Println("err: empty ip provided") 67 return 68 } 69 70 if net.ParseIP(ip) == nil { 71 addrs, err := net.LookupHost(ip) 72 if err != nil || len(addrs) == 0 { 73 fmt.Printf("err: could not resolve %s to an IP: %v\n", ip, err) 74 return 75 } 76 ip = addrs[0] 77 } 78 79 if s.queried[ip] { 80 return 81 } 82 s.queried[ip] = true 83 84 if cdn := helper.LookupCDN(net.ParseIP(ip)); helper.SkipPortScan(cdn) { 85 fmt.Printf("[OSINT %s] behind %s/%s, skipping shodan lookup\n", ip, cdn.Type, cdn.Provider) 86 return 87 } 88 89 if s.apiKey == "" { 90 fmt.Println("err: shodan api key not configured (set SHODAN_API_KEY or provide apikey in Configure)") 91 return 92 } 93 94 if s.outfile == "" { 95 fmt.Println("err: outfile not configured in Shodan (provide 'outfile' in Configure)") 96 return 97 } 98 99 httpClient := s.HTTPClient 100 if httpClient == nil { 101 httpClient = &http.Client{Timeout: 15 * time.Second} 102 } 103 104 client, err := shodan.GetClient(s.apiKey, httpClient, false) 105 if err != nil { 106 fmt.Printf("err: creating shodan client: %v\n", err) 107 return 108 } 109 110 ctx := context.Background() 111 hostParams := search.HostParams{ 112 IP: ip, 113 } 114 hostInfo, err := client.Host(ctx, hostParams) 115 if err != nil { 116 fmt.Printf("err: shodan host lookup failed for %s: %v\n", ip, err) 117 return 118 } 119 120 rawJSON, err := json.Marshal(hostInfo) 121 if err != nil { 122 fmt.Printf("err: marshaling shodan response: %v\n", err) 123 return 124 } 125 126 var pretty bytes.Buffer 127 if err := json.Indent(&pretty, rawJSON, "", "\t"); err != nil { 128 pretty.Write(rawJSON) 129 } 130 131 safeIP := strings.ReplaceAll(ip, ":", "-") 132 outdir := filepath.Dir(s.outfile) 133 if outdir == "." || outdir == "" { 134 outdir = "." 135 } 136 jsonPath := filepath.Join(outdir, fmt.Sprintf("%s.shodan.json", safeIP)) 137 138 if err := os.WriteFile(jsonPath, pretty.Bytes(), 0o644); err != nil { 139 fmt.Printf("warning: failed to write json file %s: %v\n", jsonPath, err) 140 } else { 141 fmt.Printf("wrote json: %s\n", jsonPath) 142 } 143 144 summary, perr := buildShodanSummaryFromJSON(bytes.NewReader(pretty.Bytes()), ip) 145 if perr != nil { 146 summary = fmt.Sprintf("IP: %s\n\n(Unable to build structured summary: %v)\n\n%s\n", ip, perr, pretty.String()) 147 } 148 149 f, err := os.OpenFile(s.outfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) 150 if err != nil { 151 fmt.Printf("err: unable to open outfile %s for append: %v\n", s.outfile, err) 152 return 153 } 154 defer f.Close() 155 156 sep := strings.Repeat("=", 80) + "\n" 157 toWrite := fmt.Sprintf("%s\n%s\n\n", summary, sep) 158 if _, err := f.WriteString(toWrite); err != nil { 159 fmt.Printf("err: writing summary to %s: %v\n", s.outfile, err) 160 return 161 } 162 163 fmt.Printf("[OSINT %s] Shodan summary appended to %s (json: %s)\n", ip, s.outfile, jsonPath) 164 165 // small sleep to be polite in case caller loops over many IPs 166 time.Sleep(1 * time.Second) 167 } 168 169 func buildShodanSummaryFromJSON(r io.Reader, ip string) (string, error) { 170 var obj map[string]any 171 dec := json.NewDecoder(r) 172 dec.UseNumber() 173 if err := dec.Decode(&obj); err != nil { 174 return "", fmt.Errorf("json decode: %w", err) 175 } 176 177 var b strings.Builder 178 b.WriteString(fmt.Sprintf("Shodan results for %s\n", ip)) 179 b.WriteString("=================================\n\n") 180 181 if hosts, ok := obj["hostnames"].([]any); ok && len(hosts) > 0 { 182 names := []string{} 183 for _, h := range hosts { 184 if s, ok := h.(string); ok && s != "" { 185 names = append(names, s) 186 } 187 } 188 if len(names) > 0 { 189 b.WriteString("Hostnames: " + strings.Join(names, ", ") + "\n") 190 } 191 } 192 193 if v, ok := obj["org"].(string); ok && v != "" { 194 b.WriteString("Organization: " + v + "\n") 195 } 196 if v, ok := obj["asn"].(string); ok && v != "" { 197 b.WriteString("ASN: " + v + "\n") 198 } 199 if v, ok := obj["os"].(string); ok && v != "" { 200 b.WriteString("OS: " + v + "\n") 201 } 202 if v, ok := obj["country_name"].(string); ok && v != "" { 203 b.WriteString("Country: " + v + "\n") 204 } 205 if v, ok := obj["city"].(string); ok && v != "" { 206 b.WriteString("City: " + v + "\n") 207 } 208 209 if loc, ok := obj["location"].(map[string]any); ok { 210 lat, lok := helper.ExtractNumberAsFloat(loc, "latitude") 211 lon, rok := helper.ExtractNumberAsFloat(loc, "longitude") 212 if lok && rok { 213 b.WriteString(fmt.Sprintf("Location: %f,%f\n", lat, lon)) 214 } 215 } 216 217 if dataArr, ok := obj["data"].([]any); ok && len(dataArr) > 0 { 218 b.WriteString("\nOpen services:\n") 219 for _, e := range dataArr { 220 if m, ok := e.(map[string]any); ok { 221 port := helper.ExtractInt(m, "port") 222 prod := helper.ExtractString(m, "product") 223 banner := helper.ExtractString(m, "data") 224 if port != 0 { 225 b.WriteString(fmt.Sprintf("- Port %d", port)) 226 } else { 227 b.WriteString("- (unknown port)") 228 } 229 if prod != "" { 230 b.WriteString(fmt.Sprintf(" — %s", prod)) 231 } 232 b.WriteString("\n") 233 if banner != "" { 234 sn := banner 235 if len(sn) > 512 { 236 sn = sn[:512] + "…" 237 } 238 b.WriteString(fmt.Sprintf(" Banner: %s\n", sn)) 239 } 240 } 241 } 242 } 243 244 return b.String(), nil 245 }