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}}
+
+
+ | IP | Listening | Title | Server | Content-Type |
+ Response | Length | Redirect |
+
+ {{range .Hosts}}
+
+ | {{.Ip}} |
+ {{.IsListening}} |
+ {{.Title}} |
+ {{.Server}} |
+ {{.ContentType}} |
+ {{.Resp}} |
+ {{.ContentLength}} |
+ {{.Redirect}} |
+
+ {{end}}
+
+{{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}}
+
+ | IP | Listening | Title |
+ {{range .Hosts}}
+
+ | {{.Ip}} |
+ {{.IsListening}} |
+ {{.Banner}} |
+
+ {{end}}
+
+ {{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
+
+
+
+
+ | IP | Listening | Firstline |
+ {{range .Hosts}}
+
+ | {{.Ip}} |
+ {{.IsListening}} |
+ {{.Firstline}} |
+
+ {{end}}
+
+
+ `))
+ 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
+
+ {{range .}}
+ - {{.}}
+
+
+ {{end}}
+
+ `))
+ 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