/*
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
kyoketsu, a Client-To-Client Network Enumeration System
Copyright (C) 2024 Russell Hrubesky, ChiralWorks Software LLC
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package kyoketsu
import (
"fmt"
"net"
"strings"
"sync"
"time"
)
var PORT_MAP = map[int]string{
22: "ssh", 23: "telnet", 53: "dns", 80: "http", 25: "smtp", 443: "https", 8080: "unknown", 8081: "unknown",
//8082: "unknown", 8085: "unknown", 8090: "unknown", 8091: "unknown", 9010: "unknown", 9012: "unknown", 10000: "unknown", 1433: "microsoft_sql",
3306: "mysql", 3050: "firebird", 5432: "postgres", 27017: "mongo", 6379: "redis", 8005: "tomcat", 6443: "kubernetes", 853: "dns-tls", 143: "imap",
389: "ldap", 445: "smb", 543: "kerberos", 544: "kerberos", 749: "kerberos", 760: "kerberos",
}
/*
Need to work with with a database schema in mind, and revolve functionality around that
*/
type Host struct {
Fqdn string // The FQDN of the address targeted as per the systems default resolver
IpAddress string // the IPv4 address (no ipv6 support yet)
PingResponse bool // boolean value representing if the host responded to ICMP
ListeningPorts map[int]string // list of maps depicting a port number -> service name
PortString string
Id int64
}
/*
Perform a concurrent TCP port dial on a host, either by domain name or IP.
:param addr: the address of fqdn to scan
:param portmap: a key/value pair of port numbers to service names to dial the host with
*/
func PortWalk(addr string, portmap map[int]string) *Host {
wg := &sync.WaitGroup{}
mu := &sync.Mutex{}
out := []*PortScanResult{}
for p, s := range portmap {
wg.Add(1)
go func(target string, p int, s string, mu *sync.Mutex) {
defer wg.Done()
scanout := singlePortScan(target, p, s)
mu.Lock()
out = append(out, scanout)
mu.Unlock()
}(addr, p, s, mu)
}
wg.Wait()
host := &Host{IpAddress: addr, ListeningPorts: map[int]string{}}
for i := range out {
if out[i].Listening {
host.ListeningPorts[out[i].PortNumber] = out[i].Service
host.PortString = fmt.Sprintf("%s,%v", host.PortString, out[i].PortNumber)
}
host.PortString = strings.TrimPrefix(host.PortString, ",")
}
return host
}
type PortScanResult struct {
// This is used to represent the results of a port scan against one host
PortNumber int `json:"port_number"` // The port number that was scanned
Service string `json:"service"` // the name of the service that the port was identified/mapped to
Protocol string `json:"protocol"` // The IP protocol (TCP/UDP)
Listening bool `json:"listening"` // A boolean value that depicts if the service is listening or not
}
type PortScanDirective struct {
// Struct for dependency injecting the dynamic port map used for scans
Pairs map[int]string
}
/*
Wrapper function to dependency inject the resource for a port -> service name mapping.
May move to a database, or something.
*/
func RetrieveScanDirectives() PortScanDirective {
return PortScanDirective{Pairs: PORT_MAP}
}
/*
Scans a single host on a single port
:param addr: the address to dial
:param port: the port number to dial
:param svcs: the name of the service that the port is associate with
*/
func singlePortScan(addr string, port int, svcs string) *PortScanResult {
address := fmt.Sprintf("%v:%d", addr, port)
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err != nil {
return &PortScanResult{PortNumber: port, Protocol: "tcp", Service: svcs, Listening: false}
}
conn.Close()
return &PortScanResult{PortNumber: port, Protocol: "tcp", Service: svcs, Listening: true}
}
/*
Perform a port scan sweep across an entire subnet
:param ip: the IPv4 address WITH CIDR notation
:param portmap: the mapping of ports to scan with (port number mapped to protocol name)
*/
func NetSweep(ip string, portmap map[int]string) ([]*Host, error) {
var err error
var addr *IpSubnetMapper
addr, err = GetNetworkAddresses(ip)
if err != nil {
return nil, err
}
returnhosts := []*Host{}
var wg sync.WaitGroup
mu := &sync.Mutex{}
for i := range addr.Ipv4s {
wg.Add(1)
go func(target string, mu *sync.Mutex) {
defer wg.Done()
out := PortWalk(target, portmap)
if len(out.ListeningPorts) > 0 {
dns, _ := net.LookupAddr(out.IpAddress)
out.Fqdn = strings.Join(dns, ", ")
mu.Lock()
returnhosts = append(returnhosts, out)
mu.Unlock()
}
}(addr.Ipv4s[i].String(), mu)
}
wg.Wait()
return returnhosts, nil
}