diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd1ae90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.txt +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/GoShod.gif b/GoShod.gif new file mode 100644 index 0000000..a9558aa Binary files /dev/null and b/GoShod.gif differ diff --git a/GoShod.go b/GoShod.go new file mode 100644 index 0000000..f15e9df --- /dev/null +++ b/GoShod.go @@ -0,0 +1,161 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "mime" + "net" + "net/http" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/jayateertha043/goshod/pkg/shodan_utils" + "github.com/pkg/browser" +) + +var ApiKey string = "" + +func main() { + + printbanner() + apiKey := flag.String("api", "", "Your Shodan API Key, One time Configure.") + flag.Parse() + if strings.TrimSpace(*apiKey) != "" { + ApiKey = strings.TrimSpace(*apiKey) + b_api := []byte(ApiKey) + err := ioutil.WriteFile("./shodan_config.txt", b_api, 0666) + if err != nil { + log.Fatalln("[*] Error writing api to configuration file at ./shodan_config.txt") + } + log.Println("[*] Api Key Configured at ./shodan_config.txt") + } + + var server_wg = new(sync.WaitGroup) + log.Println("[*] Looking for shodan_config.txt at current directory") + b, err := ioutil.ReadFile("./shodan_config.txt") + if err != nil { + log.Fatalln("[*] Shodan_config.txt not found, Kindly configure Shodan api key.") + } + + ApiKey = strings.TrimSpace(string(b)) + uri := startServer(server_wg) + log.Println("[*] Server started at: " + uri) + browser.OpenURL(uri) + server_wg.Wait() +} +func startServer(wg *sync.WaitGroup) string { + wg.Add(1) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatal(err) + } + go func() { + defer ln.Close() + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + w.Header().Add("Access-Control-Allow-Origin", "*") + if path == "" || path == "/" { + path = "./html/index.html" + w.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(path))) + bs, err := ioutil.ReadFile(path) + if err != nil { + log.Println(err) + } + io.Copy(w, bytes.NewBuffer(bs)) + } else if path == "/shodanpagecount" && r.Method == "POST" { + err := r.ParseForm() + if err != nil { + log.Println(err) + } + apikey := ApiKey + //apikey := r.FormValue("apikey") + search := r.FormValue("search") + + if strings.TrimSpace(apikey) == "" || strings.TrimSpace(search) == "" { + w.WriteHeader(401) + w.Write([]byte("missing necessary params !")) + + } else { + params := make(map[string]string, 0) + params["key"] = apikey + params["query"] = search + status_code, page_count, response_body := shodan_utils.ShodanSearchHostCount(params) + if status_code == 200 && page_count > 0 && response_body != "" { + w.WriteHeader(status_code) + p_str := strconv.Itoa(page_count) + w.Write([]byte(p_str)) + } else if page_count == -101 { + w.WriteHeader(status_code) + w.Write([]byte(response_body)) + } else { + w.WriteHeader(status_code) + w.Write([]byte(response_body)) + } + } + } else if path == "/shodansearch" && r.Method == "POST" { + err := r.ParseForm() + if err != nil { + log.Println(err) + } + apikey := ApiKey + //apikey := r.FormValue("apikey") + search := r.FormValue("search") + page := r.FormValue("page") + _, err = strconv.Atoi(page) + if err != nil || page == "" { + page = "1" + } + + if strings.TrimSpace(apikey) == "" || strings.TrimSpace(search) == "" || strings.TrimSpace(page) == "" { + w.WriteHeader(401) + w.Write([]byte("missing necessary params !")) + + } else { + params := make(map[string]string, 0) + params["key"] = apikey + params["query"] = search + params["page"] = page + if params["page"] < "0" { + params["page"] = "1" + } + + status_code, response_body := shodan_utils.GetShodanResultForPage(params) + w.WriteHeader(status_code) + w.Write([]byte(response_body)) + + } + } else { + path = "./html/" + path[1:] + w.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(path))) + bs, err := ioutil.ReadFile(path) + if err != nil { + fmt.Println(err) + } + io.Copy(w, bytes.NewBuffer(bs)) + } + }) + log.Fatal(http.Serve(ln, nil)) + }() + return "http://" + ln.Addr().String() +} + +func printbanner() { + banner := + ` + +██████╗ ██████╗ ███████╗██╗ ██╗ ██████╗ ██████╗ +██╔════╝ ██╔═══██╗██╔════╝██║ ██║██╔═══██╗██╔══██╗ +██║ ███╗██║ ██║███████╗███████║██║ ██║██║ ██║ +██║ ██║██║ ██║╚════██║██╔══██║██║ ██║██║ ██║ +╚██████╔╝╚██████╔╝███████║██║ ██║╚██████╔╝██████╔╝ + ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ + +` + fmt.Println(banner) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a7c15f0 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/jayateertha043/goshod + +go 1.16 + +require ( + github.com/Jeffail/gabs v1.4.0 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2ea259c --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= +github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/html/css/style.css b/html/css/style.css new file mode 100644 index 0000000..346b065 --- /dev/null +++ b/html/css/style.css @@ -0,0 +1,3 @@ +#app { + background-color: var(--v-background-base); +} \ No newline at end of file diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..7ecd0e8 --- /dev/null +++ b/html/index.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/html/js/app.js b/html/js/app.js new file mode 100644 index 0000000..cf34f54 --- /dev/null +++ b/html/js/app.js @@ -0,0 +1,185 @@ +const vuetify = new Vuetify({ + theme: { + themes: { + dark: { + primary: '#620ee', + secondary: '#03dac5', + accent: '#82B1FF', + error: '#FF5252', + info: '#2196F3', + success: '#4CAF50', + warning: '#FFC107', + background: '#121212', + }, + + }, + options: { + customProperties: true + }, + }, +}); +const app = new Vue({ + el: "#app", + vuetify: vuetify, + data: () => ({ + search:"", + host_result:null, + maxPage:1, + minPage:1, + curPage:1, + showLoading:false, + + }), + created(){ + this.$vuetify.theme.dark = true; + }, + mounted () { + this.host_result={matches:[]} + }, + methods: { + shodanHostSearch(search,page,event){ + event.preventDefault(); + this.host_result={matches:[]} + this.showLoading=true; + var params = new URLSearchParams(); + params.append("search",search); + console.log(params.get("search")); + console.log("Search param submitted: "+params.get("search")) + + + if(page!=-1&&page<0){ + page=1; + } + + params.append("page",page); + console.log("Page:",page); + //If first time clicked on search, need page count + if(page=="-1"){ + this.showLoading=true; + axios({ + method: "post", + url: "/shodanpagecount", + data: params, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }).then( (response)=>{ + var tmp = parseInt(response.data); + console.log("Pages:",tmp); + if(tmp<=0){ + alert("No hosts found") + } + if(tmp!=NaN&&tmp>0){ + this.maxPage = tmp; + this.minPage = 1; + this.curPage = 1; + params.set("page",this.curPage); + this.showLoading=true; + axios({ + method: "post", + url: "/shodansearch", + data: params, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }).then( (response)=>{ + this.host_result = response.data + }).catch((e)=>{ + this.showLoading=false; + console.log(e); + if(e.response){ + console.log(e.response.data); + var err = e.response.data.error; + alert(err); + } + }).then(()=>{ + this.showLoading=false; + }); + } + }).catch((e)=>{ + this.showLoading=false; + console.log(e) + alert("Error, Try again later !!!"); + }).then(()=>{ + }); + + }else{ + axios({ + method: "post", + url: "/shodansearch", + data: params, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }).then( (response)=>{ + this.host_result = response.data + console.log(response.data) + }).catch((e)=>{ + this.showLoading=false; + console.log(e) + if(e.response){ + console.log(e.response.data); + var err = e.response.data.error; + alert(err); + } + }).then(()=>{ + this.showLoading=false; + }); + } + }, + nextPage(){ + if(this.curPage{ + this.host_result = response.data + }).catch((e)=>{ + this.curPage--; + this.showLoading=false; + console.log(e); + if(e.response){ + console.log(e.response.data); + var err = e.response.data.error; + alert(err); + } + }).then(()=>{ + this.showLoading=false; + }); + }else{ + alert("Max Pages Reached !!!"); + } + + }, + prevPage(){ + if(this.curPage>this.minPage){ + this.curPage--; + var params = new URLSearchParams(); + params.append("search",this.search); + params.append("page",this.curPage) + axios({ + method: "post", + url: "/shodansearch", + data: params, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }).then( (response)=>{ + this.host_result = response.data + }).catch((e)=>{ + this.curPage++; + this.showLoading=false; + console.log(e); + if(e.response){ + console.log(e.response.data); + var err = e.response.data.error; + alert(err); + } + }).then(()=>{ + this.showLoading=false; + }); + }else{ + alert("Min Page Reached !!!"); + } + + } + }, +}); diff --git a/html/js/shodan_utils.js b/html/js/shodan_utils.js new file mode 100644 index 0000000..82a4dac --- /dev/null +++ b/html/js/shodan_utils.js @@ -0,0 +1,25 @@ +function shodanSearch(){ + + searchbar = document.getElementById("search").value; + search = document.getElementById("search").value; + console.log("Executing shodanSearch ...") + console.log(searchbar) + + + var xhttp = new XMLHttpRequest(); + var url = '/shodansearch'; + + xhttp.open("POST", url, true); + xhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + var response_body=""; + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + // Typical action to be performed when the document is ready: + response_body = xhttp.responseText + console.log(response_body); + } + }; + xhttp.send("search="+search); + + +} \ No newline at end of file diff --git a/pkg/httpclient/httpclient.go b/pkg/httpclient/httpclient.go new file mode 100644 index 0000000..9510be8 --- /dev/null +++ b/pkg/httpclient/httpclient.go @@ -0,0 +1,248 @@ +package httpclient + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +var userAgents = []string{ + + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38", +} + +var mu = new(sync.Mutex) + +//Build Default Headers sent with every request +func BuildDefaultHeaders() map[string]string { + return map[string]string{ + "User-Agent": "goshod by @jayateerthag", //RandomUA(), + "Accept": "text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + // "Accept-Encoding": "gzip,deflate", + "DNT": "1", + //"Connection": "close", + } +} + +//Get request with params +func GetRequestP(url string, params map[string]string, headers map[string]string, timeoutP int) (*http.Response, error) { + timeout := timeoutP + if timeout <= 0 { + timeout = 5 + } + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + var client = new(http.Client) + + client = &http.Client{Transport: transport, Timeout: time.Duration(timeout) * time.Second} + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Close = true + + //params adding and encoding + q := req.URL.Query() + for name, value := range params { + q.Add(name, value) + } + req.URL.RawQuery = q.Encode() + + //Set Default Headers with Ramdom UA + tmpHeaders := BuildDefaultHeaders() + for key, value := range tmpHeaders { + req.Header.Set(key, value) + } + + mu.Lock() + if headers != nil { + for key, value := range headers { + req.Header.Set(key, value) + } + } + mu.Unlock() + resp, err := client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} +func GetRequest(uri string, headers map[string]string, timeoutP int, proxy string) (*http.Response, error) { + h := CloneHeaders(headers) + pu, err := url.Parse(proxy) + + if proxy != "" { + if err != nil { + fmt.Println("Invalid proxy...") + return nil, nil + } + } + timeout := timeoutP + if timeout <= 0 { + timeout = 5 + } + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + var client = new(http.Client) + if proxy != "" { + transport = &http.Transport{ + TLSClientConfig: tlsConfig, + Proxy: http.ProxyURL(pu), + } + } + client = &http.Client{Transport: transport, Timeout: time.Duration(timeout) * time.Second} + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return nil, err + } + req.Close = true + //Set Default Headers with Ramdom UA + tmpHeaders := BuildDefaultHeaders() + for key, value := range tmpHeaders { + req.Header.Set(key, value) + } + mu.Lock() + if h != nil { + for key, value := range h { + req.Header.Set(key, value) + } + } + mu.Unlock() + resp, err := client.Do(req) + + if err != nil { + return nil, err + } + return resp, nil +} + +//Post Request supports normal and json body too +func PostRequest(uri string, data string, headers map[string]string, timeoutP int, proxy string) (*http.Response, error) { + h := CloneHeaders(headers) + pu, err := url.Parse(proxy) + + if proxy != "" { + if err != nil { + fmt.Println("Invalid proxy...") + return nil, nil + } + } + var req *http.Request + var resp *http.Response + + timeout := timeoutP + if timeout <= 0 { + timeout = 5 + } + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + var client = new(http.Client) + if proxy != "" { + transport = &http.Transport{ + TLSClientConfig: tlsConfig, + Proxy: http.ProxyURL(pu), + } + } + client = &http.Client{Transport: transport, Timeout: time.Duration(timeout) * time.Second} + req, err = http.NewRequest("POST", uri, strings.NewReader(data)) + if err != nil { + return nil, err + } + + req.Close = true + + req.Header.Set("Content-Type", "application/x-www-form-uriencoded") + + //Set Default Headers with Ramdom UA + tmpHeaders := BuildDefaultHeaders() + for key, value := range tmpHeaders { + req.Header.Set(key, value) + } + + mu.Lock() + if h != nil { + for key, value := range h { + req.Header.Set(key, value) + } + } + mu.Unlock() + resp, err = client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} + +func RandomUA() string { + rand.Seed(time.Now().Unix()) + choice := rand.Intn(len(userAgents)) + return userAgents[choice] +} + +//default json escapes html characters such as <> so custom marshal needed +func JsonMarshal(params interface{}) ([]byte, error) { + buffer := new(bytes.Buffer) + enc := json.NewEncoder(buffer) + enc.SetEscapeHTML(false) + err := enc.Encode(¶ms) + if err != nil { + log.Println(err) + } + return buffer.Bytes(), err +} + +//default json escapes html characters such as <> so custom marshal needed +func JsonMarshalIndent(params interface{}, prefix string, indent string) ([]byte, error) { + buffer := new(bytes.Buffer) + enc := json.NewEncoder(buffer) + enc.SetEscapeHTML(false) + enc.SetIndent(prefix, indent) + err := enc.Encode(¶ms) + if err != nil { + log.Println(err) + } + return buffer.Bytes(), err +} + +func CloneHeaders(m map[string]string) map[string]string { + cp := make(map[string]string) + if m == nil { + return nil + } + for k, v := range m { + cp[k] = v + } + return cp +} diff --git a/pkg/shodan_utils/shodan_utils.go b/pkg/shodan_utils/shodan_utils.go new file mode 100644 index 0000000..848f6aa --- /dev/null +++ b/pkg/shodan_utils/shodan_utils.go @@ -0,0 +1,52 @@ +package shodan_utils + +import ( + "io/ioutil" + + "github.com/Jeffail/gabs" + "github.com/jayateertha043/goshod/pkg/httpclient" +) + +func ShodanSearchHostCount(params map[string]string) (statuscode int, hostcount int, responsebody string) { + sres, err := httpclient.GetRequestP("https://api.shodan.io/shodan/host/count", params, nil, 25) + + if err != nil { + return 500, -100, "Error: Unable to make request to shodan" + } + + defer sres.Body.Close() + status_code := sres.StatusCode + sresbytes, _ := ioutil.ReadAll(sres.Body) + response_body := string(sresbytes) + if status_code == 200 { + res, err := gabs.ParseJSON([]byte(sresbytes)) + if err != nil { + response_body = "Error: Unable to parse result from shodan" + return 500, -101, response_body + } + total_exist := res.ExistsP("total") + if total_exist { + total := res.Path("total").Data().(float64) + pages := int(((total - 1) / 100)) + 1 + return 200, pages, response_body + } + } else { + return 500, -100, "Error: Unknown" + } + + return 500, -100, "Error: Unknown" +} + +func GetShodanResultForPage(params map[string]string) (status_code int, response_body string) { + + sres, err := httpclient.GetRequestP("https://api.shodan.io/shodan/host/search", params, nil, 25) + if err != nil { + return 500, `{"error": "Internal connection error with shodan"}` + } + + defer sres.Body.Close() + statuscode := sres.StatusCode + sresbytes, _ := ioutil.ReadAll(sres.Body) + responsebody := string(sresbytes) + return statuscode, responsebody +}