├── go.mod ├── shosubgo.png ├── README.md ├── apishodan └── api.go └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/incogbyte/shosubgo 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /shosubgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incogbyte/shosubgo/HEAD/shosubgo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shosubgo 2 | Small tool to Grab subdomains using Shodan api. 3 | ## Get your shodan api FREE with limit usage: 4 | 5 | 6 | ## Install 7 | 8 | ```bash 9 | $ go install github.com/incogbyte/shosubgo@latest 10 | # verify inside your $GOPATH the folder "bin" 11 | ``` 12 | 13 | ## Usage 14 | ```bash 15 | go run main.go -d target.com -s YourAPIKEY / go run main.go -f file -s YourAPIKEY 16 | ``` 17 | 18 | If you want the tool to fail instantly on any error, use `-fail` flag: 19 | 20 | ```bash 21 | $ go run main.go -d target.com -s YourAPIKEY 22 | Error fetching subdomains for domain target.com: invalid API key 23 | 24 | $ go run main.go -d target.com -s YourAPIKEY -fail 25 | Error fetching subdomains for domain target.com: invalid API key 26 | exit status 1 27 | ``` 28 | 29 | ## Usage download from releases: 30 | 31 | https://github.com/incogbyte/shosubgo/releases/tag/1.1 32 | 33 | ```bash 34 | # From Download Releases 35 | 36 | ./shosubgo_linux -d target.com -s YourAPIKEY 37 | ./shosubgo_linux -f file -s YourAPIKEY 38 | ``` 39 | 40 | ![shosubgo](https://raw.githubusercontent.com/incogbyte/shosubgo/master/shosubgo.png) 41 | 42 | ![gopher](https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcTFcFPxQzLnq18PnHBkUxF6KfavmHX9q6Ukz-JWSNOg7iJu7Dsy) 43 | 44 | #### Mentions 45 | 46 | 1. Quoted in the vídeo https://www.youtube.com/watch?v=qLTe6Z10vj8 h@cktivitycon 2020: The Bug Hunter's Methodology v4: Recon Edition by @Jhaddix 47 | 2. Mentioned at @SANSOffensive con :) 2024 by @Jhaddix 48 | 49 | -------------------------------------------------------------------------------- /apishodan/api.go: -------------------------------------------------------------------------------- 1 | package apishodan 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | ErrInsufficientCredits = errors.New("insufficient query credits") 13 | ErrInvalidAPIKey = errors.New("invalid API key") 14 | ) 15 | 16 | const URL = "https://api.shodan.io" 17 | const URLDOMAIN = "https://api.shodan.io/dns/domain/" 18 | 19 | type API struct { 20 | apiKey string 21 | } 22 | 23 | type JsonData struct { 24 | QueryCredits int `json:"query_credits"` 25 | ScanCredits int `json:"scan_credits"` 26 | Telnet bool `json:"telnet"` 27 | Plan string `json:"plan"` 28 | HTTPS bool `json:"https"` 29 | Unlocked bool `json:"unlocked"` 30 | } 31 | 32 | type JsonSubDomain struct { 33 | Domain string `json:"domain,omitempty"` 34 | Tags []string `json:"tags,omitempty"` 35 | Data []SubDomain `json:"data,omitempty"` 36 | SubDomains []string `json:"subdomains,omitempty"` 37 | } 38 | 39 | type SubDomain struct { 40 | SubD string `json:"subdomain,omitempty"` 41 | Type string `json:"type,omitempty"` 42 | Value string `json:"value,omitempty"` 43 | LastSeen string `json:"last_seen,omitempty"` 44 | } 45 | 46 | type ErrorResponse struct { 47 | Error string `json:"error"` 48 | } 49 | 50 | func New(key string) *API { 51 | return &API{apiKey: key} 52 | } 53 | 54 | func (s *API) InfoAccount() (*JsonData, error) { 55 | res, err := http.Get(fmt.Sprintf("%s/api-info?key=%s", URL, s.apiKey)) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to make request ( Info Account Shodan ): %v", err) 58 | } 59 | defer res.Body.Close() 60 | 61 | if res.StatusCode == http.StatusUnauthorized { 62 | return nil, ErrInvalidAPIKey 63 | } 64 | 65 | if res.StatusCode != http.StatusOK { 66 | return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode) 67 | } 68 | 69 | body, err := io.ReadAll(res.Body) 70 | if err != nil { 71 | return nil, fmt.Errorf("failed to read response body: %v", err) 72 | } 73 | 74 | var ret JsonData 75 | if err := json.Unmarshal(body, &ret); err != nil { 76 | return nil, fmt.Errorf("failed to decode JSON response: %v", err) 77 | } 78 | 79 | return &ret, nil 80 | } 81 | 82 | func (s *API) GetSubdomain(domain string) (*JsonSubDomain, error) { 83 | 84 | url := fmt.Sprintf("%s%s?key=%s", URLDOMAIN, domain, s.apiKey) 85 | res, err := http.Get(url) 86 | 87 | if err != nil { 88 | return nil, fmt.Errorf("failed to execute http request: %w", err) 89 | } 90 | defer res.Body.Close() 91 | 92 | if res.StatusCode == http.StatusOK { 93 | var sub JsonSubDomain 94 | if err := json.NewDecoder(res.Body).Decode(&sub); err != nil { 95 | return nil, fmt.Errorf("failed to decode successful response: %w", err) 96 | } 97 | return &sub, nil 98 | } 99 | 100 | if res.StatusCode == http.StatusUnauthorized { 101 | body, err := io.ReadAll(res.Body) 102 | if err != nil { 103 | return nil, fmt.Errorf("failed to read error response body: %w", err) 104 | } 105 | 106 | var errResp ErrorResponse 107 | if json.Unmarshal(body, &errResp) == nil { 108 | if errResp.Error == "Insufficient query credits, please upgrade your API plan or wait for the monthly limit to reset" { 109 | return nil, ErrInsufficientCredits 110 | } 111 | } 112 | return nil, ErrInvalidAPIKey 113 | } 114 | 115 | return nil, fmt.Errorf("shodan API returned non-200 status: %d", res.StatusCode) 116 | } 117 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/incogbyte/shosubgo/apishodan" 12 | ) 13 | 14 | const Author = "inc0gbyt3" 15 | 16 | func main() { 17 | domain := flag.String("d", "", "> Domain to find subdomains") 18 | shodanKey := flag.String("s", "", "> Shodan api key") 19 | verbose := flag.Bool("v", false, "> Show all output") 20 | fileName := flag.String("o", "", "> Save domains into a file") 21 | inputFile := flag.String("f", "", "> File containing domains to find subdomains") 22 | jsonFlag := flag.Bool("json", false, "> Save output in JSON format") 23 | failOnError := flag.Bool("fail", false, "> Exit immediately on any API error") 24 | 25 | flag.Parse() 26 | 27 | log.SetFlags(0) 28 | 29 | if *domain == "" && *inputFile == "" { 30 | fmt.Printf("[*] Usage: %s -d target.com -s shodanKey [-f input_file]\n", os.Args[0]) 31 | fmt.Printf("[*] Author: %s\n", Author) 32 | os.Exit(1) 33 | } 34 | 35 | apiKey := apishodan.New(*shodanKey) 36 | 37 | var domains []string 38 | 39 | if *domain != "" { 40 | domains = append(domains, *domain) 41 | } 42 | 43 | if *inputFile != "" { 44 | fileDomains, err := readDomainsFromFile(*inputFile) 45 | if err != nil { 46 | log.Fatalf("Failed to read domains from file: %v", err) 47 | } 48 | domains = append(domains, fileDomains...) 49 | } 50 | 51 | var outputFile *os.File 52 | var err error 53 | if *fileName != "" { 54 | outputFile, err = os.OpenFile(*fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 55 | if err != nil { 56 | log.Fatalf("Failed to open output file: %v", err) 57 | } 58 | defer outputFile.Close() 59 | } 60 | 61 | for _, domainSearch := range domains { 62 | subdomain, err := apiKey.GetSubdomain(domainSearch) 63 | if err != nil { 64 | if *failOnError { 65 | log.Fatalf("Error fetching subdomains for domain %s: %v", domainSearch, err) 66 | } 67 | fmt.Printf("Error fetching subdomains for domain %s: %v\n", domainSearch, err) 68 | continue 69 | } 70 | 71 | if len(subdomain.SubDomains) == 0 { 72 | fmt.Printf("No subdomains found for domain %s\n", domainSearch) 73 | continue 74 | } 75 | 76 | if *verbose { 77 | info, err := apiKey.InfoAccount() 78 | if err != nil { 79 | if *failOnError { 80 | log.Fatalf("Error fetching account info: %v", err) 81 | } 82 | fmt.Printf("Error fetching account info: %v\n", err) 83 | continue 84 | } 85 | fmt.Printf("[*] Credits: %d\nScan Credits: %d\n\n", info.QueryCredits, info.ScanCredits) 86 | 87 | for _, v := range subdomain.Data { 88 | d := v.SubD + subdomain.Domain 89 | fmt.Printf("[*] Domain: %s\nIP/DNS: %s\nLast Scan made by Shodan: %s\n", d, v.Value, v.LastSeen) 90 | } 91 | } else { 92 | if *jsonFlag { 93 | // Convert full subdomains to a slice 94 | var fullSubdomains []string 95 | for _, sub := range subdomain.SubDomains { 96 | fullSubdomains = append(fullSubdomains, fmt.Sprintf("%s.%s", sub, domainSearch)) 97 | } 98 | 99 | jsonData, err := json.MarshalIndent(fullSubdomains, "", " ") 100 | if err != nil { 101 | log.Fatal("Error marshaling JSON:", err) 102 | } 103 | 104 | if outputFile != nil { 105 | _, err = outputFile.Write(jsonData) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | _, err = outputFile.WriteString("\n") 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | fmt.Println("[*] DONE writing JSON to file:", *fileName) 114 | } else { 115 | fmt.Println(string(jsonData)) 116 | } 117 | } else { 118 | for _, v := range subdomain.SubDomains { 119 | fullDomain := fmt.Sprintf("%s.%s", v, domainSearch) 120 | if outputFile != nil { 121 | _, err := outputFile.WriteString(fullDomain + "\n") 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | } 126 | fmt.Println(fullDomain) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | func readDomainsFromFile(filename string) ([]string, error) { 134 | file, err := os.Open(filename) 135 | if err != nil { 136 | return nil, err 137 | } 138 | defer file.Close() 139 | 140 | var domains []string 141 | scanner := bufio.NewScanner(file) 142 | for scanner.Scan() { 143 | domains = append(domains, scanner.Text()) 144 | } 145 | if err := scanner.Err(); err != nil { 146 | return nil, err 147 | } 148 | 149 | return domains, nil 150 | } 151 | --------------------------------------------------------------------------------