diff --git a/frontend.go b/frontend.go new file mode 100755 index 0000000..3d47e69 --- /dev/null +++ b/frontend.go @@ -0,0 +1,768 @@ +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "database/sql" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "strings" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/mattn/go-sqlite3" +) + +var port = "9000" +var ( + username string = "meer" + password string = "Satori" +) + +type BannerStore interface { + QueryHTTPBanners(listeningOnly, hasTitle, interesting bool, search string) ([]Host, error) + QuerySSHBanners(search string, listeningOnly bool) ([]SSHHost, error) + QueryTelnetBanners(search string, listeningOnly, hasBanner bool) ([]TelnetHost, error) + QueryHTTPBannerByIP(ip string) (*HostDetail, error) +} +type SQLBannerStore struct { + db *sql.DB +} +type HostDetail struct { + Ip, Title, Resp, Headers, Server, ContentType, ContentLength, + Redirect, MetaTags, BodySnippet string + IsListening bool +} +type TelnetHost struct { + Ip, Firstline string + IsListening bool +} +type SSHHost struct { + Ip, Banner string + IsListening bool +} + +var db *sql.DB +var Store BannerStore + +type DBConfig struct { + DbDriver string `json:"dbdriver"` + Username string `json:"username"` + Password string `json:"password"` + DBName string `json:"dbname"` +} +type Webconfig struct { + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` +} +type Config struct { + ZmapFile string `json:"zmapfile"` + Channelfeeders int `json:"channelfeeders"` + DB DBConfig `json:"db"` + Web Webconfig `json:"web"` +} + +func loadConfig(path string) (*Config, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + var cfg Config + if err := json.NewDecoder(f).Decode(&cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +type Host struct { + Ip, Title, Resp, Server, ContentType, ContentLength, Redirect string + IsListening bool +} + +func sqlInitMySQLFrontend(dsn string) (*SQLBannerStore, error) { + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + return &SQLBannerStore{db: db}, nil +} + +func sqlInitSQLiteFrontend(dbname string) (*SQLBannerStore, error) { + db, err := sql.Open("sqlite3", dbname) + if err != nil { + return nil, err + } + return &SQLBannerStore{db: db}, nil +} + +func (s *SQLBannerStore) QueryHTTPBanners(listeningOnly, hasTitle, interesting bool, search string) ([]Host, error) { + q := `SELECT Ip, IsListening, httptitle, Httpresp, ServerHeader, ContentType, ContentLength, Redirect + FROM httpbanners WHERE 1=1` + args := []interface{}{} + + if listeningOnly { + q += " AND IsListening=1" + } + if hasTitle { + q += " AND httptitle IS NOT NULL AND httptitle<>''" + } + if search != "" { + q += " AND (Ip LIKE ? OR httptitle LIKE ? OR ServerHeader LIKE ? OR ContentType LIKE ? OR Redirect LIKE ?)" + like := "%" + search + "%" + args = append(args, like, like, like, like, like) + } + if interesting { + q += buildExclusionSQL("excluded_titles.txt") + } + + q += " ORDER BY Ip" + + rows, err := s.db.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var hosts []Host + for rows.Next() { + var h Host + if err := rows.Scan(&h.Ip, &h.IsListening, &h.Title, &h.Resp, &h.Server, + &h.ContentType, &h.ContentLength, &h.Redirect); err != nil { + continue + } + hosts = append(hosts, h) + } + return hosts, nil +} +func (s *SQLBannerStore) QueryTelnetBanners(search string, listeningOnly, hasBanner bool) ([]TelnetHost, error) { + q := "SELECT Ip, IsListening, firstline FROM telnetbanners WHERE 1=1" + args := []interface{}{} + + if listeningOnly { + q += " AND IsListening=1" + } + if hasBanner { + q += " AND firstline IS NOT NULL AND firstline<>''" + } + if search != "" { + q += " AND (Ip LIKE ? OR firstline LIKE ?)" + like := "%" + search + "%" + args = append(args, like, like) + } + + q += " ORDER BY Ip" + + rows, err := s.db.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var hosts []TelnetHost + for rows.Next() { + var h TelnetHost + if err := rows.Scan(&h.Ip, &h.IsListening, &h.Firstline); err != nil { + continue + } + hosts = append(hosts, h) + } + return hosts, nil +} + +func (s *SQLBannerStore) QueryHTTPBannerByIP(ip string) (*HostDetail, error) { + row := s.db.QueryRow(`SELECT Ip, httptitle, Httpresp, Headers, + ServerHeader, ContentType, ContentLength, + Redirect, MetaTags, BodySnippet, IsListening + FROM httpbanners WHERE Ip=?`, ip) + + var h HostDetail + if err := row.Scan(&h.Ip, &h.Title, &h.Resp, &h.Headers, + &h.Server, &h.ContentType, &h.ContentLength, + &h.Redirect, &h.MetaTags, &h.BodySnippet, &h.IsListening); err != nil { + return nil, err + } + return &h, nil +} +func (s *SQLBannerStore) QuerySSHBanners(search string, listeningOnly bool) ([]SSHHost, error) { + q := "SELECT Ip, IsListening, firstline FROM sshbanners WHERE 1=1" + args := []interface{}{} + if listeningOnly { + q += " AND IsListening=1" + } + if search != "" { + q += " AND (Ip LIKE ? OR firstline LIKE ?)" + like := "%" + search + "%" + args = append(args, like, like) + } + + rows, err := s.db.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var hosts []SSHHost + for rows.Next() { + var h SSHHost + if err := rows.Scan(&h.Ip, &h.IsListening, &h.Banner); err != nil { + continue + } + hosts = append(hosts, h) + } + return hosts, nil +} + +func main() { + cfg, err := loadConfig("config.json") + if err != nil { + log.Fatal(err) + } + switch cfg.DB.DbDriver { + case "mysql": + dsn := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s", + cfg.DB.Username, cfg.DB.Password, cfg.DB.DBName) + Store, err = sqlInitMySQLFrontend(dsn) + case "sqlite": + Store, err = sqlInitSQLiteFrontend(cfg.DB.DBName) + default: + log.Fatalf("Unsupported driver: %s", cfg.DB.DbDriver) + } + if err != nil { + log.Fatal(err) + } + fmt.Println("DB opened") + http.HandleFunc("/", home) + http.HandleFunc("/http", listHTTP) + http.HandleFunc("/telnet", listTelnet) + http.HandleFunc("/ssh", listSSH) + http.HandleFunc("/ssh/", sshDetail) + http.HandleFunc("/http/", httpDetail) + http.HandleFunc("/telnet/", telnetDetail) + + http.HandleFunc("/admin", basicAuth(adminPanel, cfg.Web.Username, cfg.Web.Password)) + + fmt.Printf("Frontend running at http://localhost:%d\n", cfg.Web.Port) + http.ListenAndServe(fmt.Sprintf(":%d", cfg.Web.Port), nil) +} + +func home(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `Pixalia scan`+css+` + +

Welcome to Pixalia scan!

+

Select a tab above to view scan results from Pixalia scan.

+

for now, only telnet and http and SSH are available, more will be added soon

+ `) +} +func listHTTP(w http.ResponseWriter, r *http.Request) { + listening := r.URL.Query().Get("listening") == "1" + hasTitle := r.URL.Query().Get("title") == "1" + interesting := r.URL.Query().Get("interesting") == "1" + search := r.URL.Query().Get("q") + + // Ask the BannerStore for results + hosts, err := Store.QueryHTTPBanners(listening, hasTitle, interesting, search) + if err != nil { + http.Error(w, "DB error: "+err.Error(), 500) + return + } + + tmpl := template.Must(template.New("http").Parse(` +HTTP Results` + css + ` + +

HTTP Banners

+
+ + + + + +
+ +{{if .HasResults}} + + + + + + {{range .Hosts}} + + + + + + + + + + + {{end}} +
IPListeningTitleServerContent-TypeResponseLengthRedirect
{{.Ip}}{{.IsListening}}{{.Title}}{{.Server}}{{.ContentType}}{{.Resp}}{{.ContentLength}}{{.Redirect}}
+{{else}} +
+

No results found :(

+

Try adjusting your filters or search terms.

+
+{{end}} +`)) + + tmpl.Execute(w, map[string]interface{}{ + "Hosts": hosts, + "FilterListening": r.URL.Query().Get("listening"), + "FilterInteresting": r.URL.Query().Get("interesting"), + "FilterTitle": r.URL.Query().Get("title"), + "Search": search, + "HasResults": len(hosts) > 0, + }) +} + +func listSSH(w http.ResponseWriter, r *http.Request) { + listening := r.URL.Query().Get("listening") == "1" + search := r.URL.Query().Get("q") + + hosts, err := Store.QuerySSHBanners(search, listening) + if err != nil { + http.Error(w, "DB error: "+err.Error(), 500) + return + } + + tmpl := template.Must(template.New("http").Parse(` + SSH Results` + css + ` + +

SSH Banners

+
+ + + + +
+ + {{if .HasResults}} + + + {{range .Hosts}} + + + + + + {{end}} +
IPListeningTitle
{{.Ip}}{{.IsListening}}{{.Banner}}
+ {{else}} +
+

No results found :(

+

Try adjusting your filters or search terms.

+

dont worry, we will eventually update our database for your desired results.

+
+ {{end}} + + `)) + + tmpl.Execute(w, map[string]interface{}{ + "Hosts": hosts, + "FilterListening": r.URL.Query().Get("listening"), + "FilterTitle": r.URL.Query().Get("title"), + "Search": search, + "HasResults": len(hosts) > 0, + }) +} +func sshDetail(w http.ResponseWriter, r *http.Request) { + +} +func httpDetail(w http.ResponseWriter, r *http.Request) { + ip := strings.TrimPrefix(r.URL.Path, "/http/") + + h, err := Store.QueryHTTPBannerByIP(ip) + if err != nil { + http.Error(w, "DB error: "+err.Error(), 500) + } + tmpl := template.Must(template.New("httpDetail").Parse(` + {{.Title}}` + css + ` + +

HTTP Host {{.Ip}}

+

Title: {{.Title}}

+

Response: {{.Resp}}

+

Server: {{.Server}}

+

Content-Type: {{.ContentType}}

+

Content-Length: {{.ContentLength}}

+

Redirect: {{.Redirect}}

+ {{ if .MetaTags}} +

Meta Tags

+
{{.MetaTags}}
+ {{ end }} +

Raw Headers

+
{{.Headers}}
+

Body Snippet

+
{{.BodySnippet}}
+

Visit Site

+ `)) + + tmpl.Execute(w, map[string]interface{}{ + "Ip": h.Ip, + "Title": h.Title, + "Resp": h.Resp, + "Headers": h.Headers, + "Server": h.Server, + "ContentType": h.ContentType, + "ContentLength": h.ContentLength, + "Redirect": h.Redirect, + "MetaTags": h.MetaTags, + "BodySnippet": h.BodySnippet, + }) +} + +func listTelnet(w http.ResponseWriter, r *http.Request) { + listening := r.URL.Query().Get("listening") == "1" + banner := r.URL.Query().Get("banner") == "1" + search := r.URL.Query().Get("q") + hosts, err := Store.QueryTelnetBanners(search, listening, banner) + if err != nil { + http.Error(w, "DB error: "+err.Error(), 500) + return + } + tmpl := template.Must(template.New("telnet").Parse(` + Telnet Results` + css + ` + +

Telnet Banners

+
+ + + +
+ + + + + {{range .Hosts}} + + + + + + {{end}} +
IPListeningFirstline
{{.Ip}}{{.IsListening}}{{.Firstline}}
+ + `)) + tmpl.Execute(w, map[string]interface{}{ + "Hosts": hosts, + "FilterListening": r.URL.Query().Get("listening"), + "FilterBanner": r.URL.Query().Get("banner"), + }) +} +func telnetDetail(w http.ResponseWriter, r *http.Request) { + ip := strings.TrimPrefix(r.URL.Path, "/telnet/") + row := db.QueryRow("SELECT Ip, fullbanner FROM telnetbanners WHERE Ip=?", ip) + var ipAddr, banner string + row.Scan(&ipAddr, &banner) + + tmpl := template.Must(template.New("telnetDetail").Parse(` + Telnet Host {{.Ip}}` + css + ` + +

Telnet Host {{.Ip}}

+
{{.Banner}}
+ `)) + + tmpl.Execute(w, map[string]interface{}{ + "Ip": ipAddr, + "Banner": banner, + }) +} + +const css = `` + +// Admin panel handler +func adminPanel(w http.ResponseWriter, r *http.Request) { + fmt.Printf("admin login from %s, X-real-IP header: %s\n", r.RemoteAddr, r.Header.Get("X-Real-IP")) + if r.Method == http.MethodPost { + // Delete IP + if ip := r.FormValue("delete_ip"); ip != "" { + _, err := db.Exec("DELETE FROM httpbanners WHERE Ip = ?", ip) + fmt.Println("[admin panel] deleting IP: ", ip) + if err != nil { + http.Error(w, "Error deleting IP: "+err.Error(), http.StatusInternalServerError) + return + } + } + + // Add excluded title + if title := r.FormValue("add_title"); title != "" { + f, err := os.OpenFile("excluded_titles.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + http.Error(w, "Error writing title: "+err.Error(), http.StatusInternalServerError) + return + } + defer f.Close() + f.WriteString(title + "\n") + } + if remove := r.FormValue("remove_title"); remove != "" { + data, err := os.ReadFile("excluded_titles.txt") + if err == nil { + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + var newLines []string + for _, l := range lines { + if strings.TrimSpace(l) != remove { + newLines = append(newLines, l) + } + } + os.WriteFile("excluded_titles.txt", []byte(strings.Join(newLines, "\n")+"\n"), 0644) + } + } + } + var titles []string + if data, err := os.ReadFile("excluded_titles.txt"); err == nil { + titles = strings.Split(strings.TrimSpace(string(data)), "\n") + } + + tmpl := template.Must(template.New("admin").Parse(` + Admin Panel` + css + ` +

Admin Panel

+ +
+ + + +
+ +
+ + + +
+ +

Current Excluded Titles

+ + `)) + tmpl.Execute(w, titles) +} +func buildExclusionSQL(filename string) string { + data, _ := os.ReadFile(filename) + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + var parts []string + for _, l := range lines { + l = strings.TrimSpace(l) + if l == "" { + continue + } + parts = append(parts, fmt.Sprintf("httptitle NOT LIKE '%%%s%%'", l)) + } + if len(parts) == 0 { + return "" + } + return " AND (" + strings.Join(parts, " AND ") + ")" +} + +// taken from https://www.alexedwards.net/blog/basic-authentication-in-go +func basicAuth(next http.HandlerFunc, user, pass string) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if ok { + // Calculate SHA-256 hashes for the provided and expected + // usernames and passwords. + usernameHash := sha256.Sum256([]byte(username)) + passwordHash := sha256.Sum256([]byte(password)) + expectedUsernameHash := sha256.Sum256([]byte(user)) + expectedPasswordHash := sha256.Sum256([]byte(pass)) + usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) + passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) + + // If the username and password are correct, then call + // the next handler in the chain. Make sure to return + // afterwards, so that none of the code below is run. + if usernameMatch && passwordMatch { + next.ServeHTTP(w, r) + return + } + } + + // If the Authentication header is not present, is invalid, or the + // username or password is wrong, then set a WWW-Authenticate + // header to inform the client that we expect them to use basic + // authentication and send a 401 Unauthorized response. + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..6da0e55 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module interscan + +go 1.25.5 + +require ( + github.com/go-sql-driver/mysql v1.9.3 + github.com/mattn/go-sqlite3 v1.14.47 + golang.org/x/net v0.51.0 +) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..24282ae --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo= +github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= diff --git a/main.go b/main.go new file mode 100755 index 0000000..94ed6e8 --- /dev/null +++ b/main.go @@ -0,0 +1,686 @@ +package main + +import ( + "bufio" + "bytes" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "math/rand" + "net" + "net/http" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/net/html" +) + +type DBConfig struct { + DbDriver string `json:"dbdriver"` + Username string `json:"username"` + Password string `json:"password"` + DBName string `json:"dbname"` +} + +type Webconfig struct { + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` +} +type Config struct { + ZmapFile string `json:"zmapfile"` + Channelfeeders int `json:"channelfeeders"` + DB DBConfig `json:"db"` + Web Webconfig `json:"web"` +} + +func loadConfig(path string) (*Config, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + var cfg Config + if err := json.NewDecoder(f).Decode(&cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +type BannerStore interface { + InsertHTTPBanner(ip string, listening bool, title, resp, errStr, headers, serverHeader, contentType, contentLength, redirect, metaTags, bodySnippet string) error + InsertSSHBanner(ip string, listening bool, firstline string) error + InsertTelnetBanner(ip string, listening bool, firstline, fullbanner string) error +} +type SQLBannerStore struct { + db *sql.DB + insertHTTP *sql.Stmt + insertSSH *sql.Stmt + insertTelnet *sql.Stmt +} + +func (s *SQLBannerStore) InsertHTTPBanner( + ip string, + listening bool, + title, resp, errStr, headers, + serverHeader, contentType, contentLength, redirect, metaTags, bodySnippet string, +) error { + _, err := s.insertHTTP.Exec(ip, listening, title, resp, errStr, headers, + serverHeader, contentType, contentLength, redirect, metaTags, bodySnippet) + return err +} + +func (s *SQLBannerStore) InsertSSHBanner(ip string, listening bool, firstline string) error { + _, err := s.insertSSH.Exec(ip, listening, firstline) + return err +} + +func (s *SQLBannerStore) InsertTelnetBanner(ip string, listening bool, firstline, fullbanner string) error { + _, err := s.insertTelnet.Exec(ip, listening, firstline, fullbanner) + return err +} + +var ( + db *sql.DB + defaultFile = "zmaplist.txt" + store BannerStore + excluded []ipRange + allowed []ipRange +) + +const red = "\033[31m" +const reset = "\033[0m" +const green = "\033[32m" +const blue = "\033[34m" + +type ipRange struct { + start uint32 + end uint32 +} + +func ipToUint32(ip net.IP) uint32 { + ip = ip.To4() + return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3]) +} + +func uint32ToIP(n uint32) net.IP { + return net.IPv4( + byte(n>>24), + byte(n>>16), + byte(n>>8), + byte(n), + ) +} +func parseCIDRRange(cidr string) ipRange { + _, network, _ := net.ParseCIDR(cidr) + start := ipToUint32(network.IP) + mask := ipToUint32(net.IP(network.Mask)) + hostBits := ^mask + end := start | hostBits + return ipRange{start, end} +} +func main() { + blocks := []string{ + "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", + "127.0.0.0/8", "169.254.0.0/16", "100.64.0.0/10", + "192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24", + "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", + "224.0.0.0/4", "240.0.0.0/4", "255.255.255.255/32", + // US Gov / DoD legacy + "6.0.0.0/8", "7.0.0.0/8", "11.0.0.0/8", + "21.0.0.0/8", "22.0.0.0/8", "26.0.0.0/8", + "28.0.0.0/8", "29.0.0.0/8", "30.0.0.0/8", + "33.0.0.0/8", "55.0.0.0/8", + } + for _, b := range blocks { + excluded = append(excluded, parseCIDRRange(b)) + } + for first := 1; first <= 255; first++ { + cidr := fmt.Sprintf("%d.0.0.0/8", first) + r := parseCIDRRange(cidr) + skip := false + for _, ex := range excluded { + if r.start >= ex.start && r.start <= ex.end { + skip = true + break + } + } + if !skip { + allowed = append(allowed, r) + } + } + + rand.Seed(time.Now().UnixNano()) + cfg, err := loadConfig("config.json") + if err != nil { + log.Fatal(err) + } + + switch cfg.DB.DbDriver { + case "mysql": + store, err = sqlInitMySQL(fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s", cfg.DB.Username, cfg.DB.Password, cfg.DB.DBName)) + case "sqlite": + store, err = sqlInitSQLite(cfg.DB.DBName) + default: + log.Fatalf("Unsupported driver: %s", cfg.DB.DbDriver) + } + + if err != nil { + log.Fatal(err) + } + fmt.Println("Starting IP scan") + ConcurrentScan(defaultFile, cfg) + fmt.Println("Finished IP scan!") + return +} + +func sqlInitMySQL(connargs string) (*SQLBannerStore, error) { + // open connection + var err error + db, err = sql.Open("mysql", connargs) + if err != nil { + return nil, err + } + + // create tables + tables := []string{ + `CREATE TABLE IF NOT EXISTS httpbanners ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Ip VARCHAR(255) NOT NULL UNIQUE, + Scanned_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + IsListening TINYINT(1) NOT NULL, + httptitle TEXT, + Httpresp TEXT, + Headers TEXT, + Error TEXT, + ServerHeader TEXT, + ContentType TEXT, + ContentLength TEXT, + Redirect TEXT, + MetaTags TEXT, + BodySnippet TEXT + )`, + + `CREATE TABLE IF NOT EXISTS sshbanners ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Ip VARCHAR(255) NOT NULL UNIQUE, + scanned_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + IsListening TINYINT(1) NOT NULL, + firstline TEXT + )`, + + `CREATE TABLE IF NOT EXISTS telnetbanners ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Ip VARCHAR(255) NOT NULL UNIQUE, + Scanned_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + IsListening TINYINT(1) NOT NULL, + firstline TEXT, + fullbanner TEXT + )`, + } + + for _, stmt := range tables { + if _, err := db.Exec(stmt); err != nil { + return nil, err + } + } + + // prepare statements + httpStmt, err := db.Prepare(` + INSERT INTO httpbanners + (Ip, IsListening, httptitle, Httpresp, Error, Headers, + ServerHeader, ContentType, ContentLength, Redirect, MetaTags, BodySnippet) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + IsListening=VALUES(IsListening), + httptitle=VALUES(httptitle), + Httpresp=VALUES(Httpresp), + Error=VALUES(Error), + Headers=VALUES(Headers), + ServerHeader=VALUES(ServerHeader), + ContentType=VALUES(ContentType), + ContentLength=VALUES(ContentLength), + Redirect=VALUES(Redirect), + MetaTags=VALUES(MetaTags), + BodySnippet=VALUES(BodySnippet) +`) + + if err != nil { + return nil, err + } + + sshStmt, err := db.Prepare(` + INSERT INTO sshbanners (Ip, IsListening, firstline) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE + IsListening=VALUES(IsListening), + firstline=VALUES(firstline) + `) + if err != nil { + return nil, err + } + + telnetStmt, err := db.Prepare(` + INSERT INTO telnetbanners (Ip, IsListening, firstline, fullbanner) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + IsListening=VALUES(IsListening), + firstline=VALUES(firstline), + fullbanner=VALUES(fullbanner) + `) + if err != nil { + return nil, err + } + + return &SQLBannerStore{ + db: db, + insertHTTP: httpStmt, + insertSSH: sshStmt, + insertTelnet: telnetStmt, + }, nil +} + +func sqlInitSQLite(dbname string) (*SQLBannerStore, error) { + db, err := sql.Open("sqlite3", dbname) + if err != nil { + return nil, err + } + tables := []string{ + `CREATE TABLE IF NOT EXISTS httpbanners ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Ip TEXT NOT NULL UNIQUE, + Scanned_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + IsListening INTEGER NOT NULL, + httptitle TEXT, + Httpresp TEXT, + Headers TEXT, + Error TEXT, + ServerHeader TEXT, + ContentType TEXT, + ContentLength TEXT, + Redirect TEXT, + MetaTags TEXT, + BodySnippet TEXT + )`, + `CREATE TABLE IF NOT EXISTS sshbanners ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Ip TEXT NOT NULL UNIQUE, + Scanned_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + IsListening INTEGER NOT NULL, + firstline TEXT + )`, + `CREATE TABLE IF NOT EXISTS telnetbanners ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Ip TEXT NOT NULL UNIQUE, + Scanned_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + IsListening INTEGER NOT NULL, + firstline TEXT, + fullbanner TEXT + )`, + } + + for _, stmt := range tables { + if _, err := db.Exec(stmt); err != nil { + return nil, err + } + } + httpStmt, err := db.Prepare(` + INSERT INTO httpbanners + (Ip, IsListening, httptitle, Httpresp, Error, Headers, + ServerHeader, ContentType, ContentLength, Redirect, MetaTags, BodySnippet) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(Ip) DO UPDATE SET + IsListening=excluded.IsListening, + httptitle=excluded.httptitle, + Httpresp=excluded.Httpresp, + Error=excluded.Error, + Headers=excluded.Headers, + ServerHeader=excluded.ServerHeader, + ContentType=excluded.ContentType, + ContentLength=excluded.ContentLength, + Redirect=excluded.Redirect, + MetaTags=excluded.MetaTags, + BodySnippet=excluded.BodySnippet + `) + if err != nil { + return nil, err + } + + sshStmt, err := db.Prepare(` + INSERT INTO sshbanners (Ip, IsListening, firstline) + VALUES (?, ?, ?) + ON CONFLICT(Ip) DO UPDATE SET + IsListening=excluded.IsListening, + firstline=excluded.firstline + `) + if err != nil { + return nil, err + } + + telnetStmt, err := db.Prepare(` + INSERT INTO telnetbanners (Ip, IsListening, firstline, fullbanner) + VALUES (?, ?, ?, ?) + ON CONFLICT(Ip) DO UPDATE SET + IsListening=excluded.IsListening, + firstline=excluded.firstline, + fullbanner=excluded.fullbanner + `) + if err != nil { + return nil, err + } + + return &SQLBannerStore{ + db: db, + insertHTTP: httpStmt, + insertSSH: sshStmt, + insertTelnet: telnetStmt, + }, nil +} +func isPrivateIP(ip net.IP) bool { // blacklisted CIDR ranges from being scanned, you can also put your own IPs, a long as its in CIDR notation + var privateBlocks = []string{ + "10.0.0.0/8", // RFC1918 private + "172.16.0.0/12", // RFC1918 private + "192.168.0.0/16", // RFC1918 private + "127.0.0.0/8", // loopback + "169.254.0.0/16", // link-local + "100.64.0.0/10", // carrier-grade NAT + "192.0.0.0/24", // IANA special use + "192.0.2.0/24", // TEST-NET-1 + "192.88.99.0/24", // deprecated 6to4 relay + "198.18.0.0/15", // benchmarking + "198.51.100.0/24", // TEST-NET-2 + "203.0.113.0/24", // TEST-NET-3 + "224.0.0.0/4", // multicast + "240.0.0.0/4", // reserved + "255.255.255.255/32", // broadcast + } + + // US Government / DoD legacy ranges (policy exclusions) + var govBlocks = []string{ + "6.0.0.0/8", + "7.0.0.0/8", + "11.0.0.0/8", + "21.0.0.0/8", + "22.0.0.0/8", + "26.0.0.0/8", + "28.0.0.0/8", + "29.0.0.0/8", + "30.0.0.0/8", + "33.0.0.0/8", + "55.0.0.0/8", + } + for _, block := range append(privateBlocks, govBlocks...) { + _, cidr, _ := net.ParseCIDR(block) + if cidr.Contains(ip) { + return true + } + } + return false +} +func generateRandomPublicIP() string { + r := allowed[rand.Intn(len(allowed))] + ip := r.start + uint32(rand.Intn(int(r.end-r.start+1))) + return uint32ToIP(ip).String() +} +func grabbanner(ip string) { + ports := []int{80, 8080, 8088} + client := http.Client{Timeout: 1500 * time.Millisecond} + + for _, port := range ports { + url := fmt.Sprintf("http://%s:%d", ip, port) + resp, err := client.Get(url) + if err != nil { + //store.InsertHTTPBanner(fmt.Sprintf("%s:%d", ip, port),false, "", "", err.Error(), "", "", "", "", "", "", "") + continue + } + defer resp.Body.Close() + + // Limit body size to avoid huge pages + body, err := io.ReadAll(io.LimitReader(resp.Body, 8192)) + if err != nil { + store.InsertHTTPBanner(fmt.Sprintf("%s:%d", ip, port), true, "", "", err.Error(), "", "", "", "", "", "", "") + continue + } + + htmlStr := string(body) + title := extractTitle(htmlStr) + metas := extractMeta(htmlStr) + + // Collect headers + var sb strings.Builder + for k, v := range resp.Header { + sb.WriteString(fmt.Sprintf("%s: %s\n", k, strings.Join(v, ", "))) + } + headers := sb.String() + + // Extra metadata + server := resp.Header.Get("Server") + contentType := resp.Header.Get("Content-Type") + contentLength := resp.Header.Get("Content-Length") + redirect := resp.Header.Get("Location") + + // Truncate body snippet + snippet := htmlStr + if len(snippet) > 2000 { + snippet = snippet[:2000] + } + + fmt.Println(green+"[ HTTP BANNER ] target found", url, reset) + store.InsertHTTPBanner(fmt.Sprintf("%s:%d", ip, port), true, + title, + fmt.Sprintf("%d %s", resp.StatusCode, resp.Status), + "", headers, + server, contentType, contentLength, redirect, + fmt.Sprint(metas), snippet) + } +} + +func extractTitle(htmlStr string) string { // finally it works..except im trusting the go devs html package for this + doc, err := html.Parse(strings.NewReader(htmlStr)) + if err != nil { + return "" + } + var f func(*html.Node) string + f = func(n *html.Node) string { + if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil { + return n.FirstChild.Data + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + if t := f(c); t != "" { + return t + } + } + return "" + } + return f(doc) +} +func extractMeta(htmlStr string) map[string]string { + metas := make(map[string]string) + doc, err := html.Parse(strings.NewReader(htmlStr)) + if err != nil { + return metas + } + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "meta" { + var name, content string + for _, attr := range n.Attr { + if attr.Key == "name" { + name = attr.Val + } + if attr.Key == "content" { + content = attr.Val + } + } + if name != "" { + metas[name] = content + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + return metas +} +func startGenerators(n int, httpJobs, sshJobs, telnetJobs chan string) { + for i := 0; i < n; i++ { + go func(seed int64) { + rand.Seed(time.Now().UnixNano() + seed) + for { + ip := generateRandomPublicIP() + select { + case httpJobs <- ip: + default: + } + select { + case sshJobs <- ip: + default: + } + select { + case telnetJobs <- ip: + default: + // meh.. + } + } + }(int64(i)) + } +} + +func ConcurrentScan(filename string, conf *Config) { // the big main loop + httpJobs := make(chan string, 5700) + telnetJobs := make(chan string, 5000) + sshJobs := make(chan string, 5000) + + var wg sync.WaitGroup + + var httpCount, sshCount, telnetCount uint64 + // helper to spawn workers + spawn := func(n int, jobs chan string, fn func(string), counter *uint64) { + for w := 0; w < n; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for ip := range jobs { + fn(ip) + atomic.AddUint64(counter, 1) + } + }() + } + } + + // spawn workers + spawn(340, httpJobs, grabbanner, &httpCount) + spawn(150, sshJobs, func(ip string) { BannerSSH(fmt.Sprintf("%s:22", ip)) }, &sshCount) + spawn(150, telnetJobs, BannerTelnet, &telnetCount) + // feed ZMap file if present + file, err := os.Open(filename) + if err == nil { + scanner := bufio.NewScanner(file) + fed := 0 + for scanner.Scan() { + ip := strings.TrimSpace(scanner.Text()) + if ip == "" { + continue + } + httpJobs <- ip + telnetJobs <- ip + sshJobs <- ip + fed++ + } + file.Close() + if fed > 0 { + fmt.Printf("Fed %d IPs from ZMap file\n", fed) + } else { + fmt.Println("ZMap file contained no usable IPs") + } + } else { + fmt.Println("No ZMap file found") + } + go startGenerators(conf.Channelfeeders, httpJobs, sshJobs, telnetJobs) // parallel feeders + go func() { + ticker := time.NewTicker(20 * time.Second) + defer ticker.Stop() + for range ticker.C { + fmt.Printf("[STATUS] HTTP:%d SSH:%d TELNET:%d | Queues H:%d S:%d T:%d\n", + atomic.LoadUint64(&httpCount), + atomic.LoadUint64(&sshCount), + atomic.LoadUint64(&telnetCount), + len(httpJobs), len(sshJobs), len(telnetJobs)) + + } + }() + wg.Wait() +} + +func BannerTelnet(ip string) { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:23", ip), 1*time.Second) + if err != nil { + // host not listening + // store.InsertTelnetBanner(ip, false, "", "") + return + } + defer conn.Close() + conn.SetReadDeadline(time.Now().Add(4 * time.Second)) + + buf := make([]byte, 2048) + var sb bytes.Buffer + for { + n, err := conn.Read(buf) + if err != nil { + break + } + sb.Write(buf[:n]) + } + + clean := stripTelnetControls(sb.Bytes()) + finalbanner := strings.TrimSpace(string(clean)) + firstLine := strings.SplitN(finalbanner, "\n", 2)[0] + + fmt.Println(blue+"[TELNET SCANNER] HOST FOUND", ip, reset) + store.InsertTelnetBanner(ip, true, firstLine, finalbanner) +} + +func BannerSSH(ip string) { + conn, err := net.DialTimeout("tcp", ip, 1500*time.Millisecond) + if err != nil { + // host not listening + //store.InsertSSHBanner(ip, false, "") + return + } + defer conn.Close() + + reader := bufio.NewReader(conn) + banner, err := reader.ReadString('\n') + if err != nil { + // connection established but banner read failed + //store.InsertSSHBanner(ip, true, "") + return + } + + // success: got a banner line + store.InsertSSHBanner(ip, true, banner) +} + +// StripTelnetControls used to remove the binary data from telnet connections to extract strings +func stripTelnetControls(data []byte) []byte { // the ANNOYING IAC CODES + out := make([]byte, 0, len(data)) + i := 0 + for i < len(data) { + if data[i] == 0xFF && i+2 < len(data) { + i += 3 + continue + } + out = append(out, data[i]) + i++ + } + return out +} diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..73d46d4 --- /dev/null +++ b/release.sh @@ -0,0 +1,2 @@ +#!/bin/bash +go build -ldflags "-s -w" main.go \ No newline at end of file