This commit is contained in:
meera 2026-06-30 15:49:12 +03:00
parent 64667645a0
commit b8cf9f418b
5 changed files with 1475 additions and 0 deletions

768
frontend.go Executable file
View File

@ -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, `<html><head><title>Pixalia scan</title>`+css+`</head><body>
<nav>
<a href="/http">HTTP Result data</a>
<a href="/telnet">Telnet Result</a>
<a href="/ssh">SSH Results</a>
</nav>
<h1>Welcome to Pixalia scan!</h1>
<h2>Select a tab above to view scan results from Pixalia scan.</h2>
<p>for now, only telnet and http and SSH are available, more will be added soon</p>
</body></html>`)
}
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(`
<html><head><title>HTTP Results</title>` + css + `</head><body>
<nav>
<a href="/http">HTTP Results</a>
<a href="/telnet">Telnet Results</a>
<a href="/ssh">SSH Results</a>
</nav>
<h2>HTTP Banners</h2>
<form method="get">
<label><input type="checkbox" name="listening" value="1" {{if eq .FilterListening "1"}}checked{{end}}> Listening only</label>
<label><input type="checkbox" name="title" value="1" {{if eq .FilterTitle "1"}}checked{{end}}> Has title</label>
<label><input type="checkbox" name="interesting" value="1" {{if eq .FilterInteresting "1"}}checked{{end}}> Hide boring results</label>
<input type="text" name="q" value="{{.Search}}" placeholder="Search IP, title, server, content-type, redirect">
<input type="submit" value="Filter">
</form>
{{if .HasResults}}
<table>
<tr>
<th>IP</th><th>Listening</th><th>Title</th><th>Server</th><th>Content-Type</th>
<th>Response</th><th>Length</th><th>Redirect</th>
</tr>
{{range .Hosts}}
<tr>
<td><a href="/http/{{.Ip}}">{{.Ip}}</a></td>
<td>{{.IsListening}}</td>
<td>{{.Title}}</td>
<td>{{.Server}}</td>
<td>{{.ContentType}}</td>
<td>{{.Resp}}</td>
<td>{{.ContentLength}}</td>
<td>{{.Redirect}}</td>
</tr>
{{end}}
</table>
{{else}}
<div class="error-box">
<h3>No results found :(</h3>
<p>Try adjusting your filters or search terms.</p>
</div>
{{end}}
</body></html>`))
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(`
<html><head><title>SSH Results</title>` + css + `</head><body>
<nav>
<a href="/http">HTTP Results</a>
<a href="/telnet">Telnet Results</a>
<a href="/ssh">SSH Results</a>
</nav>
<h2>SSH Banners</h2>
<form method="get">
<label><input type="checkbox" name="listening" value="1" {{if eq .FilterListening "1"}}checked{{end}}> Listening only</label>
<label><input type="checkbox" name="title" value="1" {{if eq .FilterTitle "1"}}checked{{end}}> Has title</label>
<input type="text" name="q" value="{{.Search}}" placeholder="Search IP or title">
<input type="submit" value="Filter">
</form>
{{if .HasResults}}
<table>
<tr><th>IP</th><th>Listening</th><th>Title</th></tr>
{{range .Hosts}}
<tr>
<td><a href="/ssh/{{.Ip}}">{{.Ip}}</a></td>
<td>{{.IsListening}}</td>
<td>{{.Banner}}</td>
</tr>
{{end}}
</table>
{{else}}
<div class="error-box">
<h3>No results found :(</h3>
<p>Try adjusting your filters or search terms.</p>
<p>dont worry, we will eventually update our database for your desired results.</p>
</div>
{{end}}
</body></html>`))
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(`
<html><head><title>{{.Title}}</title>` + css + `</head><body>
<nav>
<a href="/http">HTTP Results</a>
<a href="/telnet">Telnet Results</a>
<a href="/ssh">SSH Results</a>
</nav>
<h2>HTTP Host {{.Ip}}</h2>
<p><b>Title:</b> {{.Title}}</p>
<p><b>Response:</b> {{.Resp}}</p>
<p><b>Server:</b> {{.Server}}</p>
<p><b>Content-Type:</b> {{.ContentType}}</p>
<p><b>Content-Length:</b> {{.ContentLength}}</p>
<p><b>Redirect:</b> {{.Redirect}}</p>
{{ if .MetaTags}}
<h3>Meta Tags</h3>
<pre>{{.MetaTags}}</pre>
{{ end }}
<h3>Raw Headers</h3>
<pre>{{.Headers}}</pre>
<h3>Body Snippet</h3>
<pre>{{.BodySnippet}}</pre>
<p><a href="http://{{.Ip}}" target="_blank">Visit Site</a></p>
</body></html>`))
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(`
<html><head><title>Telnet Results</title>` + css + `</head><body>
<nav>
<a href="/http">HTTP Results</a>
<a href="/telnet">Telnet Results</a>
</nav>
<h2>Telnet Banners</h2>
<form method="get">
<label><input type="checkbox" name="listening" value="1" {{if eq .FilterListening "1"}}checked{{end}}> Listening only</label>
<label><input type="checkbox" name="banner" value="1" {{if eq .FilterBanner "1"}}checked{{end}}> Has banner</label>
<input type="submit" value="Filter">
</form>
<input type="text" id="search" placeholder="Search IP or title">
<button onclick="filterTable()">Search</button>
<table id="telnetTable">
<tr><th>IP</th><th>Listening</th><th>Firstline</th></tr>
{{range .Hosts}}
<tr>
<td><a href="/telnet/{{.Ip}}">{{.Ip}}</a></td>
<td>{{.IsListening}}</td>
<td>{{.Firstline}}</td>
</tr>
{{end}}
</table>
<script>
function filterTable() {
let input = document.getElementById("search").value.toLowerCase();
let rows = document.querySelectorAll("#telnetTable tr");
for (let i=1; i<rows.length; i++) {
let text = rows[i].innerText.toLowerCase();
rows[i].style.display = text.includes(input) ? "" : "none";
}
}
document.addEventListener("DOMContentLoaded", function() {
let tables = document.querySelectorAll("table");
tables.forEach(function(table) {
let rows = table.querySelectorAll("tr");
rows.forEach(function(row) {
// Get all cell text in the row
let text = row.innerText.trim();
if (text === "") {
row.remove(); // kill empty row
}
});
});
});
</script>
</body></html>`))
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(`
<html><head><title>Telnet Host {{.Ip}}</title>` + css + `</head><body>
<nav>
<a href="/http">HTTP Results</a>
<a href="/telnet">Telnet Results</a>
</nav>
<h2>Telnet Host {{.Ip}}</h2>
<pre>{{.Banner}}</pre>
</body></html>`))
tmpl.Execute(w, map[string]interface{}{
"Ip": ipAddr,
"Banner": banner,
})
}
const css = `<style>
body {
font-family: "Segoe UI", Arial, sans-serif;
background-color: #121212;
color: #e0e0e0;
margin: 0;
padding: 0;
}
.error-box {
margin: 20px auto;
padding: 20px;
max-width: 600px;
background: #1e1e1e;
border: 1px solid #444;
border-radius: 6px;
text-align: center;
color: #ff6b6b;
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
}
.error-box h3 {
margin: 0 0 10px;
}
nav {
background: #1f1f1f;
padding: 12px 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.6);
}
nav a {
color: #ffffff;
margin-right: 20px;
text-decoration: none;
font-weight: 500;
}
nav a:hover {
color: #4dabf7;
}
h1, h2 {
margin: 20px;
color: #ffffff;
}
form {
margin: 20px;
}
label {
margin-right: 15px;
}
input[type="submit"] {
background: #4dabf7;
border: none;
padding: 6px 12px;
color: #fff;
cursor: pointer;
border-radius: 4px;
}
input[type="submit"]:hover {
background: #339af0;
}
input[type="text"] {
padding: 8px;
margin: 20px;
width: 300px;
border: 1px solid #444;
border-radius: 4px;
background: #1e1e1e;
color: #e0e0e0;
}
table {
width: 95%;
margin: 20px auto;
border-collapse: collapse;
background: #1e1e1e;
border-radius: 6px;
overflow: hidden;
}
th {
background: #2c2c2c;
color: #ffffff;
text-align: left;
padding: 10px;
}
td {
padding: 10px;
border-bottom: 1px solid #333;
}
tr:hover {
background-color: #2a2a2a;
}
a {
color: #4dabf7;
}
a:hover {
text-decoration: underline;
}
pre {
background: #1e1e1e;
padding: 15px;
border: 1px solid #333;
border-radius: 6px;
overflow-x: auto;
margin: 20px;
font-family: Consolas, monospace;
}
button, input[type="submit"] {
background: #4dabf7;
border: none;
padding: 8px 16px;
color: #fff;
cursor: pointer;
border-radius: 4px;
font-weight: 500;
transition: all 0.2s ease-in-out;
box-shadow: 0 2px 4px rgba(0,0,0,0.4);
}
button:hover, input[type="submit"]:hover {
background: #339af0;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.6);
}
button:active, input[type="submit"]:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0,0,0,0.4);
}
</style>`
// 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(`
<html><head><title>Admin Panel</title>` + css + `</head><body>
<h2>Admin Panel</h2>
<form method="post">
<label>Delete IP:</label>
<input type="text" name="delete_ip" placeholder="Enter IP to delete">
<input type="submit" value="Delete">
</form>
<form method="post">
<label>Add Excluded Title:</label>
<input type="text" name="add_title" placeholder="Enter title phrase">
<input type="submit" value="Add">
</form>
<h3>Current Excluded Titles</h3>
<ul>
{{range .}}
<li>{{.}}
<form method="post" style="display:inline;">
<input type="hidden" name="remove_title" value="{{.}}">
<input type="submit" value="Remove">
</form>
</li>
{{end}}
</ul>
</body></html>`))
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)
})
}

11
go.mod Executable file
View File

@ -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

8
go.sum Executable file
View File

@ -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=

686
main.go Executable file
View File

@ -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
}

2
release.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
go build -ldflags "-s -w" main.go