portscraper/main.go
2025-07-27 14:01:46 +02:00

370 lines
9.1 KiB
Go

package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
)
type OpenPort struct {
IP string
Port int
Hostname string
Fingerprint string
}
const batchSize = 255
var minecraftServers []string
var (
printMutex sync.Mutex
fileLock sync.Mutex
openPorts []OpenPort
openPortsLock sync.Mutex
ipLineMap = make(map[string]int)
openPortMsgs []string
summaryFile *os.File
)
func clearScreen() {
fmt.Print("\033[2J")
}
func moveCursor(row, col int) {
fmt.Printf("\033[%d;%dH", row, col)
}
func clearLine() {
fmt.Print("\033[K")
}
func safePrint(format string, a ...interface{}) {
printMutex.Lock()
defer printMutex.Unlock()
fmt.Printf(format, a...)
}
func safePrintln(a ...interface{}) {
printMutex.Lock()
defer printMutex.Unlock()
fmt.Println(a...)
}
func printStatusLine(ip, msg string) {
if line, ok := ipLineMap[ip]; ok {
moveCursor(line, 0)
clearLine()
fmt.Printf("[%s] %s", ip, msg)
}
}
func printOpenPortLine(msg string) {
openPortsLock.Lock()
defer openPortsLock.Unlock()
openPortMsgs = append(openPortMsgs, msg)
moveCursor(len(ipLineMap)+2+len(openPortMsgs), 0)
clearLine()
fmt.Println(msg)
}
func grabBanner(ip string, port int) string {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 1*time.Second)
if err != nil {
return ""
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
return strings.TrimSpace(string(buf[:n]))
}
func identifyServiceAndOS(ip string, port int) string {
data, err := os.ReadFile("services.json")
if err != nil {
return ""
}
var services map[string]map[string]string
json.Unmarshal(data, &services)
portStr := strconv.Itoa(port)
if info, ok := services[portStr]; ok {
banner := grabBanner(ip, port)
if banner != "" {
return fmt.Sprintf("%s banner: %s", info["name"], banner)
}
return info["name"]
}
return ""
}
func queryMinecraftServer(ip string, port int) string {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 3*time.Second)
if err != nil {
return ""
}
defer conn.Close()
writeVarInt := func(val int) []byte {
var out []byte
for {
temp := byte(val & 0x7F)
val >>= 7
if val != 0 {
temp |= 0x80
}
out = append(out, temp)
if val == 0 {
break
}
}
return out
}
protocolVersion := 754
serverAddr := ip
state := 1
// so confusing for me i actually had to comment stuff
var payload []byte
payload = append(payload, 0x00) // fackin packet ID for handshake
payload = append(payload, writeVarInt(protocolVersion)...) // protocol version
payload = append(payload, writeVarInt(len(serverAddr))...) // address length
payload = append(payload, []byte(serverAddr)...) // address
payload = append(payload, byte(port>>8), byte(port&0xFF)) // port
payload = append(payload, byte(state)) // next state: status
packet := append(writeVarInt(len(payload)), payload...) // full packet = length + payload
conn.Write(packet) // send handshake
conn.Write([]byte{0x01, 0x00}) // send status request
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil || n == 0 {
return ""
}
start := bytes.IndexByte(buf, '{')
if start == -1 {
return ""
}
jsonData := string(buf[start:n])
var status struct {
Version struct {
Name string `json:"name"`
} `json:"version"`
Description interface{} `json:"description"`
Players struct {
Online int `json:"online"`
Max int `json:"max"`
} `json:"players"`
}
if err := json.Unmarshal([]byte(jsonData), &status); err != nil {
return ""
}
desc := ""
switch v := status.Description.(type) {
case string:
desc = v
case map[string]interface{}:
if text, ok := v["text"].(string); ok {
desc = text
}
}
return fmt.Sprintf("%s | %d/%d players | %s", status.Version.Name, status.Players.Online, status.Players.Max, desc)
}
func scanPort(ip string, port int, wg *sync.WaitGroup) {
printStatusLine(ip, fmt.Sprintf("scanning port %d...", port))
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 500*time.Millisecond)
if err == nil {
conn.Close()
host, err := net.LookupAddr(ip)
hostname := "unknown hostname"
if err == nil && len(host) > 0 {
hostname = strings.TrimSuffix(host[0], ".")
}
fingerprint := identifyServiceAndOS(ip, port)
if port == 25565 {
minecraftServers = append(minecraftServers, ip)
}
openPortsLock.Lock()
openPorts = append(openPorts, OpenPort{ip, port, hostname, fingerprint})
line := fmt.Sprintf(" - %s : port %d open - hostname: %s", ip, port, hostname)
if fingerprint != "" {
line += " | " + fingerprint
}
fileLock.Lock()
summaryFile.WriteString(line + "\n")
fileLock.Unlock()
openPortsLock.Unlock()
msg := fmt.Sprintf("[%s] port %d is OPEN (%s)", ip, port, hostname)
if fingerprint != "" {
msg += " | " + fingerprint
}
printOpenPortLine(msg)
printStatusLine(ip, fmt.Sprintf("open port found: %d", port))
}
}
func scanPortsForIP(ip string, portStart, portEnd int) {
const maxGoroutines = 50
guard := make(chan struct{}, maxGoroutines)
var wg sync.WaitGroup
for port := portStart; port <= portEnd; port++ {
guard <- struct{}{}
wg.Add(1)
go func(p int) {
defer wg.Done()
scanPort(ip, p, &wg)
<-guard
}(port)
}
wg.Wait()
printStatusLine(ip, "scan done.")
}
func ipRange(start, end string) ([]string, error) {
startIP := net.ParseIP(start).To4()
endIP := net.ParseIP(end).To4()
if startIP == nil || endIP == nil {
return nil, fmt.Errorf("invalid IP")
}
var ips []string
for ip := startIP; !ipAfter(ip, endIP); ip = nextIP(ip) {
ips = append(ips, ip.String())
}
return ips, nil
}
func ipAfter(a, b net.IP) bool {
for i := 0; i < 4; i++ {
if a[i] > b[i] {
return true
}
if a[i] < b[i] {
return false
}
}
return false
}
func nextIP(ip net.IP) net.IP {
next := make(net.IP, len(ip))
copy(next, ip)
for i := 3; i >= 0; i-- {
next[i]++
if next[i] != 0 {
break
}
}
return next
}
func main() {
var err error
summaryFile, err = os.Create("summary.txt")
if err != nil {
fmt.Println("[!] failed to open summary file:", err)
os.Exit(1)
}
defer summaryFile.Close()
reader := bufio.NewReader(os.Stdin)
fmt.Print("enter IP range (e.g., 192.168.1.1-192.168.1.20): ")
ipInput, _ := reader.ReadString('\n')
ipInput = strings.TrimSpace(ipInput)
if ipInput == "" {
ipInput = "192.168.0.0-192.168.255.255"
}
fmt.Print("port range (e.g., 1-65535): ")
portInput, _ := reader.ReadString('\n')
portInput = strings.TrimSpace(portInput)
if portInput == "" {
portInput = "1-10000"
}
ipParts := strings.Split(ipInput, "-")
portParts := strings.Split(portInput, "-")
if len(ipParts) != 2 || len(portParts) != 2 {
fmt.Println("[!] bad input.")
os.Exit(1)
}
ipList, err := ipRange(ipParts[0], ipParts[1])
if err != nil {
fmt.Println("[!] bad IP range:", err)
os.Exit(1)
}
portStart, _ := strconv.Atoi(portParts[0])
portEnd, _ := strconv.Atoi(portParts[1])
fmt.Println("--------------------------------------------------")
fmt.Printf("scanning %s to %s\n", ipParts[0], ipParts[1])
fmt.Printf("ports %d to %d\n", portStart, portEnd)
fmt.Println("started at:", time.Now())
fmt.Println("--------------------------------------------------")
clearScreen()
for idx, ip := range ipList {
ipLineMap[ip] = idx + 1
printStatusLine(ip, "waiting to scan...")
}
var wg sync.WaitGroup
for i := 0; i < len(ipList); i += batchSize {
end := i + batchSize
if end > len(ipList) {
end = len(ipList)
}
batch := ipList[i:end]
for _, ip := range batch {
wg.Add(1)
go func(ip string) {
defer wg.Done()
scanPortsForIP(ip, portStart, portEnd)
}(ip)
}
wg.Wait()
}
moveCursor(len(ipLineMap)+3+len(openPortMsgs), 0)
clearLine()
fmt.Println("\n--------------------------------------------------")
fmt.Println("scan done at:", time.Now())
fmt.Println("--------------------------------------------------")
if len(minecraftServers) > 0 {
safePrintln("[*] querying Minecraft servers on port 25565...")
for _, ip := range minecraftServers {
status := queryMinecraftServer(ip, 25565)
if status != "" {
safePrintln("[MC] Server at", ip, "responded.")
summaryFile.WriteString("[MC] " + ip + ":25565 " + status + "\n")
} else {
summaryFile.WriteString("[MC] " + ip + ":25565 no response or malformed\n")
}
}
}
if len(openPorts) == 0 {
summaryFile.WriteString("no open ports found.\n")
} else {
summaryFile.WriteString("[+] scan summary: open ports found with fingerprints above\n")
summaryFile.WriteString(fmt.Sprintf("[+] scanned %s to %s\n[+] ports %d to %d\n[+] %d scanned\n[+] %d hits\n", ipParts[0], ipParts[1], portStart, portEnd, len(ipList), len(openPorts)))
}
fmt.Println("[+] scan summary written to summary.txt")
}