init
This commit is contained in:
parent
64667645a0
commit
b8cf9f418b
768
frontend.go
Executable file
768
frontend.go
Executable 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
11
go.mod
Executable 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
8
go.sum
Executable 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
686
main.go
Executable 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
2
release.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
go build -ldflags "-s -w" main.go
|
||||
Loading…
Reference in New Issue
Block a user