diff --git a/go.mod b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/projectdiscovery/httpx v1.7.1
github.com/projectdiscovery/subfinder/v2 v2.9.0
github.com/projectdiscovery/wappalyzergo v0.2.48
+ github.com/shadowscatcher/shodan v1.0.8
github.com/spf13/cobra v1.10.1
github.com/spiral-sec/assetfinder v0.2.0
)
diff --git a/go.sum b/go.sum
@@ -405,6 +405,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
+github.com/shadowscatcher/shodan v1.0.8 h1:oQ4r2zDBYnFkatcgE9tr/uuZb6VgL3V/DI1+lXT0qxo=
+github.com/shadowscatcher/shodan v1.0.8/go.mod h1:8tnu38ME7RJgOfkJt/20JXK7bMvPTYCkLagO3IhI2SI=
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
diff --git a/helpers/strings.go b/helpers/strings.go
@@ -1,6 +1,7 @@
package helper
import (
+ "encoding/json"
"strings"
"unicode"
)
@@ -38,3 +39,49 @@ func StringHasUnwantedCharactersForDomainName(url string) bool {
return false
}
+
+func ExtractString(m map[string]any, k string) string {
+ if v, ok := m[k]; ok {
+ if s, ok := v.(string); ok {
+ return s
+ }
+ }
+ return ""
+}
+
+func ExtractInt(m map[string]any, k string) int {
+ if v, ok := m[k]; ok {
+ switch t := v.(type) {
+ case json.Number:
+ if i, err := t.Int64(); err == nil {
+ return int(i)
+ }
+ if f, err := t.Float64(); err == nil {
+ return int(f)
+ }
+ case float64:
+ return int(t)
+ case int:
+ return t
+ case int64:
+ return int(t)
+ }
+ }
+ return 0
+}
+
+func ExtractNumberAsFloat(m map[string]any, k string) (float64, bool) {
+ if v, ok := m[k]; ok {
+ switch t := v.(type) {
+ case json.Number:
+ f, err := t.Float64()
+ if err != nil {
+ return 0, false
+ }
+ return f, true
+ case float64:
+ return t, true
+ }
+ }
+ return 0, false
+}
diff --git a/inventory/quick-run.sh b/inventory/quick-run.sh
@@ -9,4 +9,6 @@ set -euo
./yellow osint -d ${1}
-./yellow scan -d ${1}/scans/infra --file ${1}/scans/domains.txt
+./yellow prune -f ${1}/scans/domains.txt -o ${1}/scans/active-web-panels.txt
+
+./yellow scan -d ${1}/scans/infra --file ${1}/scans/active-web-panels.txt
diff --git a/osint/run.go b/osint/run.go
@@ -57,6 +57,24 @@ func (opts OsintOpts) RunCleanup() {
}
}
+func (opts OsintOpts) runShodan(domains []string) {
+
+ shodanExec := Shodan{}
+ shodanCfg := make(map[string]any)
+
+ shodanCfg["outfile"] = fmt.Sprintf("%s/shodan.txt", opts.scanPath)
+ shodanExec.Configure(shodanCfg)
+
+ shodanExec.Info(opts.domain)
+ if opts.dryRun || !shodanExec.ShouldRun() {
+ return
+ }
+
+ for _, d := range domains {
+ shodanExec.Run(d)
+ }
+}
+
func (opts OsintOpts) runGoogleDorks() {
dorks := Dorks{}
@@ -166,6 +184,8 @@ func (opts OsintOpts) Run() {
}
}
+ /* opts.runShodan(domains) */
+
// now add all assets together, line by line
uniqueOutfile := fmt.Sprintf("%s/domains.txt", opts.scanPath)
err := os.WriteFile(uniqueOutfile, domainBuffer.Bytes(), 0644)
diff --git a/osint/shodan.go b/osint/shodan.go
@@ -0,0 +1,213 @@
+package osint
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "evil.djnn.sh/djnn/yellow/helpers"
+ "github.com/shadowscatcher/shodan"
+ "github.com/shadowscatcher/shodan/search"
+)
+
+type Shodan struct {
+ outfile string
+
+ HTTPClient *http.Client
+ apiKey string
+}
+
+func (s Shodan) ShouldRun() bool {
+ return s.apiKey != ""
+}
+
+func (s *Shodan) Info(target string) {
+ if s.apiKey == "" {
+ fmt.Println("[!] env value SHODAN_API_KEY not found. skipping shodan scan")
+ } else {
+ fmt.Println("[+] Running shodan on ", target)
+ }
+}
+
+func (s *Shodan) Configure(c any) {
+ s.apiKey = os.Getenv("SHODAN_API_KEY")
+
+ s.outfile = c.(map[string]any)["outfile"].(string)
+ if s.HTTPClient == nil {
+ s.HTTPClient = &http.Client{
+ Timeout: 15 * time.Second,
+ }
+ }
+}
+
+func (s *Shodan) Run(ip string) {
+ ip = strings.TrimSpace(ip)
+ if ip == "" {
+ fmt.Println("err: empty ip provided")
+ return
+ }
+
+ if s.apiKey == "" {
+ fmt.Println("err: shodan api key not configured (set SHODAN_API_KEY or provide apikey in Configure)")
+ return
+ }
+
+ if s.outfile == "" {
+ fmt.Println("err: outfile not configured in Shodan (provide 'outfile' in Configure)")
+ return
+ }
+
+ httpClient := s.HTTPClient
+ if httpClient == nil {
+ httpClient = &http.Client{Timeout: 15 * time.Second}
+ }
+
+ client, err := shodan.GetClient(s.apiKey, httpClient, false)
+ if err != nil {
+ fmt.Printf("err: creating shodan client: %v\n", err)
+ return
+ }
+
+ ctx := context.Background()
+ hostParams := search.HostParams{
+ IP: ip,
+ }
+ hostInfo, err := client.Host(ctx, hostParams)
+ if err != nil {
+ fmt.Printf("err: shodan host lookup failed for %s: %v\n", ip, err)
+ return
+ }
+
+ rawJSON, err := json.Marshal(hostInfo)
+ if err != nil {
+ fmt.Printf("err: marshaling shodan response: %v\n", err)
+ return
+ }
+
+ var pretty bytes.Buffer
+ if err := json.Indent(&pretty, rawJSON, "", "\t"); err != nil {
+ pretty.Write(rawJSON)
+ }
+
+ safeIP := strings.ReplaceAll(ip, ":", "-")
+ outdir := filepath.Dir(s.outfile)
+ if outdir == "." || outdir == "" {
+ outdir = "."
+ }
+ jsonPath := filepath.Join(outdir, fmt.Sprintf("%s.shodan.json", safeIP))
+
+ if err := os.WriteFile(jsonPath, pretty.Bytes(), 0o644); err != nil {
+ fmt.Printf("warning: failed to write json file %s: %v\n", jsonPath, err)
+ } else {
+ fmt.Printf("wrote json: %s\n", jsonPath)
+ }
+
+ summary, perr := buildShodanSummaryFromJSON(bytes.NewReader(pretty.Bytes()), ip)
+ if perr != nil {
+ summary = fmt.Sprintf("IP: %s\n\n(Unable to build structured summary: %v)\n\n%s\n", ip, perr, string(pretty.Bytes()))
+ }
+
+ f, err := os.OpenFile(s.outfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ fmt.Printf("err: unable to open outfile %s for append: %v\n", s.outfile, err)
+ return
+ }
+ defer f.Close()
+
+ sep := strings.Repeat("=", 80) + "\n"
+ toWrite := fmt.Sprintf("%s\n%s\n\n", summary, sep)
+ if _, err := f.WriteString(toWrite); err != nil {
+ fmt.Printf("err: writing summary to %s: %v\n", s.outfile, err)
+ return
+ }
+
+ fmt.Printf("[OSINT %s] Shodan summary appended to %s (json: %s)\n", ip, s.outfile, jsonPath)
+
+ // small sleep to be polite in case caller loops over many IPs
+ time.Sleep(200 * time.Millisecond)
+}
+
+func buildShodanSummaryFromJSON(r io.Reader, ip string) (string, error) {
+ var obj map[string]any
+ dec := json.NewDecoder(r)
+ dec.UseNumber()
+ if err := dec.Decode(&obj); err != nil {
+ return "", fmt.Errorf("json decode: %w", err)
+ }
+
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf("Shodan results for %s\n", ip))
+ b.WriteString("=================================\n\n")
+
+ if hosts, ok := obj["hostnames"].([]any); ok && len(hosts) > 0 {
+ names := []string{}
+ for _, h := range hosts {
+ if s, ok := h.(string); ok && s != "" {
+ names = append(names, s)
+ }
+ }
+ if len(names) > 0 {
+ b.WriteString("Hostnames: " + strings.Join(names, ", ") + "\n")
+ }
+ }
+
+ if v, ok := obj["org"].(string); ok && v != "" {
+ b.WriteString("Organization: " + v + "\n")
+ }
+ if v, ok := obj["asn"].(string); ok && v != "" {
+ b.WriteString("ASN: " + v + "\n")
+ }
+ if v, ok := obj["os"].(string); ok && v != "" {
+ b.WriteString("OS: " + v + "\n")
+ }
+ if v, ok := obj["country_name"].(string); ok && v != "" {
+ b.WriteString("Country: " + v + "\n")
+ }
+ if v, ok := obj["city"].(string); ok && v != "" {
+ b.WriteString("City: " + v + "\n")
+ }
+
+ if loc, ok := obj["location"].(map[string]any); ok {
+ lat, lok := helper.ExtractNumberAsFloat(loc, "latitude")
+ lon, rok := helper.ExtractNumberAsFloat(loc, "longitude")
+ if lok && rok {
+ b.WriteString(fmt.Sprintf("Location: %f,%f\n", lat, lon))
+ }
+ }
+
+ if dataArr, ok := obj["data"].([]any); ok && len(dataArr) > 0 {
+ b.WriteString("\nOpen services:\n")
+ for _, e := range dataArr {
+ if m, ok := e.(map[string]any); ok {
+ port := helper.ExtractInt(m, "port")
+ prod := helper.ExtractString(m, "product")
+ banner := helper.ExtractString(m, "data")
+ if port != 0 {
+ b.WriteString(fmt.Sprintf("- Port %d", port))
+ } else {
+ b.WriteString("- (unknown port)")
+ }
+ if prod != "" {
+ b.WriteString(fmt.Sprintf(" — %s", prod))
+ }
+ b.WriteString("\n")
+ if banner != "" {
+ sn := banner
+ if len(sn) > 512 {
+ sn = sn[:512] + "…"
+ }
+ b.WriteString(fmt.Sprintf(" Banner: %s\n", sn))
+ }
+ }
+ }
+ }
+
+ return b.String(), nil
+}
| | | | | |