Penetration testing allows organizations to focus on potential safety weaknesses in a community and supply a necessity to repair vulnerabilities earlier than they’re compromised by a malicious actor.
On this article, we’re going to create a easy, moderately sturdy, community vulnerability scanner utilizing Go, a language that could be very appropriate for community programming since it’s designed with concurrency in thoughts and has a terrific normal library.
1. Setting Up Our Challenge
Create a Vulnerability Scanner
We need to construct a easy CLI device that may have the ability to scan a community of hosts, discover open ports, operating providers and uncover attainable vulnerability. The scanner goes to be quite simple to begin, however will develop more and more succesful as we layer on options.
So, first, we’ll create a brand new Go undertaking:
mkdir goscan
cd goscan
go mod init github.com/yourusername/goscan
This initializes a brand new Go module for our undertaking, which is able to assist us handle dependencies.
Configuring Packages & Surroundings
For our scanner, we’ll leverage a number of Go packages:
bundle principal
import (
"fmt"
"internet"
"os"
"strconv"
"sync"
"time"
)
func principal() {
fmt.Println("GoScan - Community Vulnerability Scanner")
}
That is simply our preliminary setup. This can be sufficient for some preliminary options, however we’ll add extra imports on demand. Now different normal library packages like internet will take care to do many of the networking that we want and sync will do concurrency, and so forth.
Moral Concerns and Dangers with Community Scanning
Now earlier than we soar into implementation, we must always contact on some moral issues round community scanning. Unauthorized community scanning or enumeration is against the law in lots of components of the world and is handled as a vector for a cyber assault. You have to at all times observe these guidelines:
- Permission: Solely scan nonce networks and techniques that you simply personal or have specific permission to scan.
- Scope: Outline a transparent scope in your scanning and don’t exceed it.
- Timing: Don’t go for hyper-scanning that may carry down providers or increase safety alerts.
- Disclosure: When you uncover vulnerabilities, please achieve this responsibly by reporting them to the suitable system house owners.
- Authorized Compliance: Perceive and adjust to native legal guidelines governing community scanning.
Misuse of scanning instruments can lead to authorized motion, system injury, or unintentional denial of service. Our scanner will embrace safeguards like price limiting, however the accountability in the end lies with the person to make use of it ethically.
2. Easy Port Scanner
Vulnerability evaluation is predicated on port scanning. The potential susceptible providers which are being provided on every of those open ports is the knowledge that we’re on the lookout for. Now, let’s write a easy port scanner in Go.
Low-Stage Implementation of Port Scanning
Port Scanning: Attempt to set up a connection to each attainable port on a goal host. If the connection succeeds, the port is open; if it fails, the port is closed or filtered. For this performance, Go’s internet bundle has received us lined.
So, right here is our model of a easy port scanner:
bundle principal
import (
"fmt"
"internet"
"time"
)
func scanPort(host string, port int, timeout time.Length) bool {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return false
}
conn.Shut()
return true
}
func principal() {
host := "localhost"
timeout := time.Second * 2
fmt.Printf("Scanning host: %sn", host)
for port := 1; port <= 1024; port++ {
if scanPort(host, port, timeout) {
fmt.Printf("Port %d is openn", port)
}
}
fmt.Println("Scan full")
}
Utilizing the Internet Bundle
The code above makes use of the Go internet bundle, which provides community I/O interfaces and features. So, what are the principle items?
- internet.DialTimeout: This perform tries to hook up with TCP community tackle with a timeout. It returns a connection, and an error, if any.
- Connection Dealing with: If it connects with out difficulty, we all know it’s open, and we shut the connection immediately to open up sources.
- Timeout Parameter: We specify a timeout to keep away from hanging on any open ports which are filtered. Two seconds is an effective preliminary worth, however this may be tuned in keeping with the community circumstances.
Testing Our First Scan
Now let’s run our easy scanner towards our localhost, the place we could have some providers operating.
- Save the code to a file named
principal.go
- Run it with
go run principal.go
This can present what native ports are open. On a standard dev machine you might need 80 (HTTP), 443 (HTTPS), or any variety of database ports in use primarily based on what providers you may have up.
Right here’s some pattern output you would possibly get:
Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan full
Utilizing this fundamental scanner works, nevertheless it comes with some huge drawbacks:
- Pace: It’s painfully gradual because it scans ports sequentially.
- Info: Simply tells us whether or not a port is open, no service info.
- Restricted Vary: We’re solely going to scan the primary 1024 ports.
These restrictions render our scanner impractical for use within the precise world.
3. Enhancing it from right here: Multi-threaded scanning
Why the First Model is Gradual
Our first port scanner works, though it’s painfully gradual to be usable. The problem is its sequential methodology — probing one port at a time. When a bunch has a lot of closed/filtered ports, we waste time ready on a connection to day trip on every port earlier than we transfer to the opposite.
To point out you the issue, let’s check out the timing of our fundamental scanner:
- The worst case for scanning the primary 1024 ports would take a most of 2048 seconds (greater than 34 minutes) with 2 second timeout
- However even when the connections to the closed ports instantly fail, this methodology is inefficient as a result of community latency.
This one-by-one method is a bottleneck for any actual vulnerability scanning device.
Including Threading Assist
Go is especially good at concurrency utilizing goroutines and channels. So, we leverage these options to attempt to scan a number of ports directly which will increase efficiency considerably.
Now, let’s create a multithreaded port scanner:
bundle principal
import (
"fmt"
"internet"
"sync"
"time"
)
kind Outcome struct {
Port int
State bool
}
func scanPort(host string, port int, timeout time.Length) Outcome {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return Outcome{Port: port, State: false}
}
conn.Shut()
return Outcome{Port: port, State: true}
}
func scanPorts(host string, begin, finish int, timeout time.Length) []Outcome {
var outcomes []Outcome
var wg sync.WaitGroup
resultChan := make(chan Outcome, finish-begin+1)
semaphore := make(chan struct{}, 100)
for port := begin; port <= finish; port++ {
wg.Add(1)
go func(p int) {
defer wg.Carried out()
semaphore <- struct{}{}
defer func() { <-semaphore }()
end result := scanPort(host, p, timeout)
resultChan <- end result
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for end result := vary resultChan {
if end result.State {
outcomes = append(outcomes, end result)
}
}
return outcomes
}
func principal() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Millisecond * 500
fmt.Printf("Scanning %s from port %d to %dn", host, startPort, endPort)
startTime := time.Now()
outcomes := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("nScan accomplished in %sn", elapsed)
fmt.Printf("Discovered %d open ports:n", len(outcomes))
for _, end result := vary outcomes {
fmt.Printf("Port %d is openn", end result.Port)
}
}
Outcomes from A number of Threads
Now, allow us to check out the efficiency features in addition to concurrency mechanisms we added to our improved scanner:
- Goroutines: To make the scanning environment friendly we hearth up a goroutine for each port that we have to scan, so whereas we’re checking one port we are able to verify different ports concurrently.
- WaitGroup: The sync. WaitGroupAs we induce goroutines, We need to wait for his or her completion. WaitGroup helps us to trace all operating goroutines and await them to finish.
- Outcome Channel: We create a buffers channel for outcomes from all of the goroutines so as.
- Semaphore Sample: A semaphore is used, applied utilizing a channel, that limits the variety of scans which are allowed in parallel. It’s what prevents us from overwhelming the precise goal system and even our personal machine with so many connection open.
- Decreased Timeout: Since we run many of those scans in a parallel style, we use a decrease timeout.
The efficiency hole is substantial. So, once we implement this, it might probably allow us to scan 1024 ports in minutes, and definitely lower than half an hour.
Pattern output:
Scanning localhost from port 1 to 1024
Scan accomplished in 3.2s
Discovered 3 open ports:
Port 22 is open
Port 80 is open
Port 443 is open
The multi-threaded method scales very properly for bigger port ranges and a number of hosts. The semaphore sample ensures that we don’t run out of system sources regardless of scanning over a thousand ports.
4. Including Service Detection
Now that now we have a quick, environment friendly port scanner, the following step is to know what providers are operating on these open ports. That is generally often called “service fingerprinting” or “banner grabbing,” a course of by which we hook up with open ports and look at the information returned.
Implementation of Banner Grabbing
Banner grabbing is once we open a service and skim the response (banner) it sends us. So it’s a great way of figuring out if one thing runs, as many providers establish themselves in these banners.
Now let’s add banner grabbing to our scanner:
bundle principal
import (
"bufio"
"fmt"
"internet"
"strings"
"sync"
"time"
)
kind ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
}
func grabBanner(host string, port int, timeout time.Length) (string, error) {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return "", err
}
defer conn.Shut()
conn.SetReadDeadline(time.Now().Add(timeout))
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0rnrn")
} else {
}
reader := bufio.NewReader(conn)
banner, err := reader.ReadString('n')
if err != nil {
return "", err
}
return strings.TrimSpace(banner), nil
}
func identifyService(port int, banner string) (string, string) {
commonPorts := map[int]string{
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
143: "IMAP",
443: "HTTPS",
3306: "MySQL",
5432: "PostgreSQL",
6379: "Redis",
8080: "HTTP-Proxy",
27017: "MongoDB",
}
service := "Unknown"
if s, exists := commonPorts[port]; exists {
service = s
}
model := "Unknown"
lowerBanner := strings.ToLower(banner)
if strings.Incorporates(lowerBanner, "ssh") {
service = "SSH"
components := strings.Cut up(banner, " ")
if len(components) >= 2 {
model = components[1]
}
}
if strings.Incorporates(lowerBanner, "http") || strings.Incorporates(lowerBanner, "apache") ||
strings.Incorporates(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Incorporates(banner, "Server:") {
components := strings.Cut up(banner, "Server:")
if len(components) >= 2 {
model = strings.TrimSpace(components[1])
}
}
}
return service, model
}
func scanPort(host string, port int, timeout time.Length) ScanResult {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return ScanResult{Port: port, State: false}
}
conn.Shut()
banner, err := grabBanner(host, port, timeout)
service := "Unknown"
model := "Unknown"
if err == nil && banner != "" {
service, model = identifyService(port, banner)
}
return ScanResult{
Port: port,
State: true,
Service: service,
Banner: banner,
Model: model,
}
}
func scanPorts(host string, begin, finish int, timeout time.Length) []ScanResult {
var outcomes []ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, finish-begin+1)
semaphore := make(chan struct{}, 100)
for port := begin; port <= finish; port++ {
wg.Add(1)
go func(p int) {
defer wg.Carried out()
semaphore <- struct{}{}
defer func() { <-semaphore }()
end result := scanPort(host, p, timeout)
resultChan <- end result
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for end result := vary resultChan {
if end result.State {
outcomes = append(outcomes, end result)
}
}
return outcomes
}
func principal() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Millisecond * 800
fmt.Printf("Scanning %s from port %d to %dn", host, startPort, endPort)
startTime := time.Now()
outcomes := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("nScan accomplished in %sn", elapsed)
fmt.Printf("Discovered %d open ports:nn", len(outcomes))
fmt.Println("PORTtSERVICEtVERSIONtBANNER")
fmt.Println("----t-------t-------t------")
for _, end result := vary outcomes {
bannerPreview := ""
if len(end result.Banner) > 30 {
bannerPreview = end result.Banner[:30] + "..."
} else {
bannerPreview = end result.Banner
}
fmt.Printf("%dtpercentstpercentstpercentsn",
end result.Port,
end result.Service,
end result.Model,
bannerPreview)
}
}
Figuring out Operating Providers
We use two principal methods for service detection:
- Port-based identification: By mapping onto frequent port numbers (e.g., port 80 is HTTP) now we have a probable guess to the service.
- Banner evaluation: We take the banner textual content and search for service identifiers and model info.
The primary perform, grabBanner, tries to seize the primary response from a service. Some providers resembling HTTP require us to ship a request and obtain a reply, for which we use add case-specific instances.
Fundamental Model Detection
Model detection is vital for the identification of vulnerabilities. The place attainable, our scanner parses service banners to tug model info:
- SSH: Often gives model information within the type of “SSH-2. 0-OpenSSH_7.4”
- HTTP servers: Often reply with their model info in response headers resembling “Server: Apache/2.4.29”
- Database servers: May disclose model info of their welcome messages
Now the output returns much more info for each open port:
Scanning localhost from port 1 to 1024
Scan accomplished in 5.4s
Discovered 3 open ports:
PORT SERVICE VERSION BANNER
---- ------- ------- ------
22 SSH 2.0 SSH-2.0-OpenSSH_8.4p1 Ubuntu-6
80 HTTP Apache/2.4.41 Server: Apache/2.4.41 (Ubuntu)
443 HTTPS Unknown Connection closed by overseas...
This enhanced info is rather more beneficial for vulnerability evaluation.
5. Vulnerability Detection Implementation
Now that we are able to enumerate the providers operating and what model they’re, we’re going to implement detection for the vulnerabilities. The service info can be analyzed and in contrast towards identified vulnerabilities.
Writing Easy Vulnerability Checks
We are going to type a database from identified vulnerabilities primarily based on frequent providers and variations. For simplicity, we’ll create an in-code vulnerability database, although in a real-world situation, a scanner would almost certainly question exterior vulnerability databases (resembling CVE or NVD).
Now, let’s develop our code out to detect vulnerabilities:
bundle principal
import (
"bufio"
"fmt"
"internet"
"strings"
"sync"
"time"
)
kind ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
Vulnerabilities []Vulnerability
}
kind Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = []struct {
Service string
Model string
Vulnerability Vulnerability
}{
{
Service: "SSH",
Model: "OpenSSH_7.4",
Vulnerability: Vulnerability{
ID: "CVE-2017-15906",
Description: "The process_open perform in sftp-server.c in OpenSSH earlier than 7.6 doesn't correctly forestall write operations in read-only mode",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2017-15906",
},
},
{
Service: "HTTP",
Model: "Apache/2.4.29",
Vulnerability: Vulnerability{
ID: "CVE-2019-0211",
Description: "Apache HTTP Server 2.4.17 to 2.4.38 - Native privilege escalation by way of mod_prefork and mod_http2",
Severity: "Excessive",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2019-0211",
},
},
{
Service: "HTTP",
Model: "Apache/2.4.41",
Vulnerability: Vulnerability{
ID: "CVE-2020-9490",
Description: "A specifically crafted worth for the 'Cache-Digest' header could cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41",
Severity: "Excessive",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2020-9490",
},
},
{
Service: "MySQL",
Model: "5.7",
Vulnerability: Vulnerability{
ID: "CVE-2020-2922",
Description: "Vulnerability in MySQL Server permits unauthorized customers to acquire delicate info",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2020-2922",
},
},
}
func checkVulnerabilities(service, model string) []Vulnerability {
var vulnerabilities []Vulnerability
for _, vuln := vary vulnerabilityDB {
if vuln.Service == service && strings.Incorporates(model, vuln.Model) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
func grabBanner(host string, port int, timeout time.Length) (string, error) {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return "", err
}
defer conn.Shut()
conn.SetReadDeadline(time.Now().Add(timeout))
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0rnHost: %srnrn", host)
} else {
}
reader := bufio.NewReader(conn)
banner, err := reader.ReadString('n')
if err != nil {
return "", err
}
return strings.TrimSpace(banner), nil
}
func identifyService(port int, banner string) (string, string) {
commonPorts := map[int]string{
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
143: "IMAP",
443: "HTTPS",
3306: "MySQL",
5432: "PostgreSQL",
6379: "Redis",
8080: "HTTP-Proxy",
27017: "MongoDB",
}
service := "Unknown"
if s, exists := commonPorts[port]; exists {
service = s
}
model := "Unknown"
lowerBanner := strings.ToLower(banner)
if strings.Incorporates(lowerBanner, "ssh") {
service = "SSH"
components := strings.Cut up(banner, " ")
if len(components) >= 2 {
model = components[1]
}
}
if strings.Incorporates(lowerBanner, "http") || strings.Incorporates(lowerBanner, "apache") ||
strings.Incorporates(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Incorporates(banner, "Server:") {
components := strings.Cut up(banner, "Server:")
if len(components) >= 2 {
model = strings.TrimSpace(components[1])
}
}
}
return service, model
}
func scanPort(host string, port int, timeout time.Length) ScanResult {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return ScanResult{Port: port, State: false}
}
conn.Shut()
banner, err := grabBanner(host, port, timeout)
service := "Unknown"
model := "Unknown"
if err == nil && banner != "" {
service, model = identifyService(port, banner)
}
vulnerabilities := checkVulnerabilities(service, model)
return ScanResult{
Port: port,
State: true,
Service: service,
Banner: banner,
Model: model,
Vulnerabilities: vulnerabilities,
}
}
func scanPorts(host string, begin, finish int, timeout time.Length) []ScanResult {
var outcomes []ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, finish-begin+1)
semaphore := make(chan struct{}, 100)
for port := begin; port <= finish; port++ {
wg.Add(1)
go func(p int) {
defer wg.Carried out()
semaphore <- struct{}{}
defer func() { <-semaphore }()
end result := scanPort(host, p, timeout)
resultChan <- end result
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for end result := vary resultChan {
if end result.State {
outcomes = append(outcomes, end result)
}
}
return outcomes
}
func principal() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Second * 1
fmt.Printf("Scanning %s from port %d to %dn", host, startPort, endPort)
startTime := time.Now()
outcomes := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("nScan accomplished in %sn", elapsed)
fmt.Printf("Discovered %d open ports:nn", len(outcomes))
fmt.Println("PORTtSERVICEtVERSION")
fmt.Println("----t-------t-------")
for _, end result := vary outcomes {
fmt.Printf("%dtpercentstpercentsn",
end result.Port,
end result.Service,
end result.Model)
if len(end result.Vulnerabilities) > 0 {
fmt.Println(" Vulnerabilities:")
for _, vuln := vary end result.Vulnerabilities {
fmt.Printf(" [%s] %s - %sn",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Printf(" Reference: %snn", vuln.Reference)
}
}
}
}bundle principal
import (
"bufio"
"fmt"
"internet"
"strings"
"sync"
"time"
)
kind ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
Vulnerabilities []Vulnerability
}
kind Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = []struct {
Service string
Model string
Vulnerability Vulnerability
}{
{
Service: "SSH",
Model: "OpenSSH_7.4",
Vulnerability: Vulnerability{
ID: "CVE-2017-15906",
Description: "The process_open perform in sftp-server.c in OpenSSH earlier than 7.6 doesn't correctly forestall write operations in read-only mode",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2017-15906",
},
},
{
Service: "HTTP",
Model: "Apache/2.4.29",
Vulnerability: Vulnerability{
ID: "CVE-2019-0211",
Description: "Apache HTTP Server 2.4.17 to 2.4.38 - Native privilege escalation by way of mod_prefork and mod_http2",
Severity: "Excessive",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2019-0211",
},
},
{
Service: "HTTP",
Model: "Apache/2.4.41",
Vulnerability: Vulnerability{
ID: "CVE-2020-9490",
Description: "A specifically crafted worth for the 'Cache-Digest' header could cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41",
Severity: "Excessive",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2020-9490",
},
},
{
Service: "MySQL",
Model: "5.7",
Vulnerability: Vulnerability{
ID: "CVE-2020-2922",
Description: "Vulnerability in MySQL Server permits unauthorized customers to acquire delicate info",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/element/CVE-2020-2922",
},
},
}
func checkVulnerabilities(service, model string) []Vulnerability {
var vulnerabilities []Vulnerability
for _, vuln := vary vulnerabilityDB {
if vuln.Service == service && strings.Incorporates(model, vuln.Model) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
func grabBanner(host string, port int, timeout time.Length) (string, error) {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return "", err
}
defer conn.Shut()
conn.SetReadDeadline(time.Now().Add(timeout))
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0rnHost: %srnrn", host)
} else {
}
reader := bufio.NewReader(conn)
banner, err := reader.ReadString('n')
if err != nil {
return "", err
}
return strings.TrimSpace(banner), nil
}
func identifyService(port int, banner string) (string, string) {
commonPorts := map[int]string{
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
143: "IMAP",
443: "HTTPS",
3306: "MySQL",
5432: "PostgreSQL",
6379: "Redis",
8080: "HTTP-Proxy",
27017: "MongoDB",
}
service := "Unknown"
if s, exists := commonPorts[port]; exists {
service = s
}
model := "Unknown"
lowerBanner := strings.ToLower(banner)
if strings.Incorporates(lowerBanner, "ssh") {
service = "SSH"
components := strings.Cut up(banner, " ")
if len(components) >= 2 {
model = components[1]
}
}
if strings.Incorporates(lowerBanner, "http") || strings.Incorporates(lowerBanner, "apache") ||
strings.Incorporates(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Incorporates(banner, "Server:") {
components := strings.Cut up(banner, "Server:")
if len(components) >= 2 {
model = strings.TrimSpace(components[1])
}
}
}
return service, model
}
func scanPort(host string, port int, timeout time.Length) ScanResult {
goal := fmt.Sprintf("%s:%d", host, port)
conn, err := internet.DialTimeout("tcp", goal, timeout)
if err != nil {
return ScanResult{Port: port, State: false}
}
conn.Shut()
banner, err := grabBanner(host, port, timeout)
service := "Unknown"
model := "Unknown"
if err == nil && banner != "" {
service, model = identifyService(port, banner)
}
vulnerabilities := checkVulnerabilities(service, model)
return ScanResult{
Port: port,
State: true,
Service: service,
Banner: banner,
Model: model,
Vulnerabilities: vulnerabilities,
}
}
func scanPorts(host string, begin, finish int, timeout time.Length) []ScanResult {
var outcomes []ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, finish-begin+1)
semaphore := make(chan struct{}, 100)
for port := begin; port <= finish; port++ {
wg.Add(1)
go func(p int) {
defer wg.Carried out()
semaphore <- struct{}{}
defer func() { <-semaphore }()
end result := scanPort(host, p, timeout)
resultChan <- end result
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for end result := vary resultChan {
if end result.State {
outcomes = append(outcomes, end result)
}
}
return outcomes
}
func principal() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Second * 1
fmt.Printf("Scanning %s from port %d to %dn", host, startPort, endPort)
startTime := time.Now()
outcomes := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("nScan accomplished in %sn", elapsed)
fmt.Printf("Discovered %d open ports:nn", len(outcomes))
fmt.Println("PORTtSERVICEtVERSION")
fmt.Println("----t-------t-------")
for _, end result := vary outcomes {
fmt.Printf("%dtpercentstpercentsn",
end result.Port,
end result.Service,
end result.Model)
if len(end result.Vulnerabilities) > 0 {
fmt.Println(" Vulnerabilities:")
for _, vuln := vary end result.Vulnerabilities {
fmt.Printf(" [%s] %s - %sn",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Printf(" Reference: %snn", vuln.Reference)
}
}
}
}
Model-Primarily based Matching of Vulnerabilities
We’ve got a naive version-matching method for vulnerability detection:
- Direct Matching: Right here, we match the service kind and model to our vulnerability database.
- Partial Matching: For susceptible model matching, we carry out containment checks on the model string, permitting us to establish susceptible techniques even when the model string comprises additional info.
In an actual scanner this matching could be extra complicated, accounting for:
- Model ranges (i.e. variations 2.4.0 to 2.4.38 are affected)
- Configuration-specific vulnerabilities
- OS-specific points
- Extra nuanced model comparisons
Reporting What We Discover
Reporting the outcomes is the final step within the vulnerability detection and that must be accomplished in a concise and actionable format. Our scanner now:
- Lists all open ports with service and model info
- For every susceptible service, shows:
- The vulnerability ID (e.g., CVE quantity)
- An outline of the vulnerability
- Severity ranking
- Reference hyperlink for extra info
Pattern output:
Scanning localhost from port 1 to 1024
Scan accomplished in 6.2s
Discovered 3 open ports:
PORT SERVICE VERSION
---- ------- -------
22 SSH OpenSSH_7.4p1
Vulnerabilities:
[Medium] CVE-2017-15906 - The process_open perform in sftp-server.c in OpenSSH earlier than 7.6 doesn't correctly forestall write operations in read-only mode
Reference: https://nvd.nist.gov/vuln/element/CVE-2017-15906
80 HTTP Apache/2.4.41
Vulnerabilities:
[High] CVE-2020-9490 - A specifically crafted worth for the 'Cache-Digest' header could cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41
Reference: https://nvd.nist.gov/vuln/element/CVE-2020-9490
443 HTTPS Unknown
This thorough vulnerability knowledge guides cybersecurity specialists to promptly pinpoint and rank safety issues that require decision.
Closing Touches and Utilization
Now you may have a fundamental vulnerability scanner with service detection and vulnerability matching; now allow us to polish it slightly in order that it’s extra sensible to make use of in the true world.
Command Line Arguments
Our scanner must be configurable by way of command-line flags that may set targets, port ranges, and scan choices. That is easy with Go’s flag bundle.
Subsequent, let’s add command-line arguments:
bundle principal
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"internet"
"os"
"strings"
"sync"
"time"
)
kind ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
Vulnerabilities []Vulnerability
}
kind Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = []struct {
Service string
Model string
Vulnerability Vulnerability
}{
}
func principal() {
hostPtr := flag.String("host", "", "Goal host to scan (required)")
startPortPtr := flag.Int("begin", 1, "Beginning port quantity")
endPortPtr := flag.Int("finish", 1024, "Ending port quantity")
timeoutPtr := flag.Int("timeout", 1000, "Timeout in milliseconds")
concurrencyPtr := flag.Int("concurrency", 100, "Variety of concurrent scans")
formatPtr := flag.String("format", "textual content", "Output format: textual content, json, or csv")
verbosePtr := flag.Bool("verbose", false, "Present verbose output together with banners")
outputFilePtr := flag.String("output", "", "Output file (default is stdout)")
flag.Parse()
if *hostPtr == "" {
fmt.Println("Error: host is required")
flag.Utilization()
os.Exit(1)
}
if *startPortPtr < 1 || *startPortPtr > 65535 {
fmt.Println("Error: beginning port should be between 1 and 65535")
os.Exit(1)
}
if *endPortPtr < 1 || *endPortPtr > 65535 {
fmt.Println("Error: ending port should be between 1 and 65535")
os.Exit(1)
}
if *startPortPtr > *endPortPtr {
fmt.Println("Error: beginning port should be lower than or equal to ending port")
os.Exit(1)
}
timeout := time.Length(*timeoutPtr) * time.Millisecond
var outputFile *os.File
var err error
if *outputFilePtr != "" {
outputFile, err = os.Create(*outputFilePtr)
if err != nil {
fmt.Printf("Error creating output file: %vn", err)
os.Exit(1)
}
defer outputFile.Shut()
} else {
outputFile = os.Stdout
}
fmt.Fprintf(outputFile, "Scanning %s from port %d to %dn", *hostPtr, *startPortPtr, *endPortPtr)
startTime := time.Now()
var outcomes []ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, *endPortPtr-*startPortPtr+1)
semaphore := make(chan struct{}, *concurrencyPtr)
for port := *startPortPtr; port <= *endPortPtr; port++ {
wg.Add(1)
go func(p int) {
defer wg.Carried out()
semaphore <- struct{}{}
defer func() { <-semaphore }()
end result := scanPort(*hostPtr, p, timeout)
resultChan <- end result
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for end result := vary resultChan {
if end result.State {
outcomes = append(outcomes, end result)
}
}
elapsed := time.Since(startTime)
swap *formatPtr {
case "json":
outputJSON(outputFile, outcomes, elapsed)
case "csv":
outputCSV(outputFile, outcomes, elapsed, *verbosePtr)
default:
outputText(outputFile, outcomes, elapsed, *verbosePtr)
}
}
func outputText(w *os.File, outcomes []ScanResult, elapsed time.Length, verbose bool) {
fmt.Fprintf(w, "nScan accomplished in %sn", elapsed)
fmt.Fprintf(w, "Discovered %d open ports:nn", len(outcomes))
if len(outcomes) == 0 {
fmt.Fprintf(w, "No open ports discovered.n")
return
}
fmt.Fprintf(w, "PORTtSERVICEtVERSIONn")
fmt.Fprintf(w, "----t-------t-------n")
for _, end result := vary outcomes {
fmt.Fprintf(w, "%dtpercentstpercentsn",
end result.Port,
end result.Service,
end result.Model)
if verbose {
fmt.Fprintf(w, " Banner: %sn", end result.Banner)
}
if len(end result.Vulnerabilities) > 0 {
fmt.Fprintf(w, " Vulnerabilities:n")
for _, vuln := vary end result.Vulnerabilities {
fmt.Fprintf(w, " [%s] %s - %sn",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Fprintf(w, " Reference: %snn", vuln.Reference)
}
}
}
}
func outputJSON(w *os.File, outcomes []ScanResult, elapsed time.Length) {
output := struct {
ScanTime string `json:"scan_time"`
ElapsedTime string `json:"elapsed_time"`
TotalPorts int `json:"total_ports"`
OpenPorts int `json:"open_ports"`
Outcomes []ScanResult `json:"outcomes"`
}{
ScanTime: time.Now().Format(time.RFC3339),
ElapsedTime: elapsed.String(),
TotalPorts: 0,
OpenPorts: len(outcomes),
Outcomes: outcomes,
}
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
encoder.Encode(output)
}
func outputCSV(w *os.File, outcomes []ScanResult, elapsed time.Length, verbose bool) {
fmt.Fprintf(w, "Port,Service,Model,Vulnerability ID,Severity,Descriptionn")
for _, end result := vary outcomes {
if len(end result.Vulnerabilities) == 0 {
fmt.Fprintf(w, "%d,%s,%s,,,n",
end result.Port,
escapeCSV(end result.Service),
escapeCSV(end result.Model))
} else {
for _, vuln := vary end result.Vulnerabilities {
fmt.Fprintf(w, "%d,%s,%s,%s,%s,%sn",
end result.Port,
escapeCSV(end result.Service),
escapeCSV(end result.Model),
escapeCSV(vuln.ID),
escapeCSV(vuln.Severity),
escapeCSV(vuln.Description))
}
}
}
fmt.Fprintf(w, "n# Scan accomplished in %s, discovered %d open portsn",
elapsed, len(outcomes))
}
func escapeCSV(s string) string {
if strings.Incorporates(s, ",") || strings.Incorporates(s, """) || strings.Incorporates(s, "n") {
return """ + strings.ReplaceAll(s, """, """") + """
}
return s
}
Output Formatting
Our scanner can now output to 3 codecs:
- Textual content: Simple to learn, straightforward to put in writing, nice for interactive use.
- JSON: Structured output helpful for machine processing and integration with different instruments.
- CSV: A spreadsheet-compatible format for evaluation and reporting.
The output textual content additionally gives extra info resembling uncooked banner info if the verbose flag is ready. That is additionally useful for debugging or deep-dive evaluation.
Instance Utilization and Outcomes
So, listed here are some potentialities if you’re going to use our scanner for various events:
Fundamental scan of a single host:
$ go run principal.go -host instance.com
Scan a particular port vary:
$ go run principal.go -host instance.com -start 80 -end 443
Save outcomes to a JSON file:
$ go run principal.go -host instance.com -format json -output outcomes.json
Verbose scan with elevated timeout:
$ go run principal.go -host instance.com -verbose -timeout 2000
Scan with larger concurrency for sooner outcomes:
$ go run principal.go -host instance.com -concurrency 200
Instance textual content output:
Scanning instance.com from port 1 to 1024
Scan accomplished in 12.6s
Discovered 3 open ports:
PORT SERVICE VERSION
---- ------- -------
22 SSH OpenSSH_7.4p1
Vulnerabilities:
[Medium] CVE-2017-15906 - The process_open perform in sftp-server.c in OpenSSH earlier than 7.6 doesn't correctly forestall write operations in read-only mode
Reference: https://nvd.nist.gov/vuln/element/CVE-2017-15906
80 HTTP Apache/2.4.41
Vulnerabilities:
[High] CVE-2020-9490 - A specifically crafted worth for the 'Cache-Digest' header could cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41
Reference: https://nvd.nist.gov/vuln/element/CVE-2020-9490
443 HTTPS nginx/1.18.0
JSON output instance:
{
"scan_time": "2025-03-18T14:30:00Z",
"elapsed_time": "12.6s",
"total_ports": 1024,
"open_ports": 3,
"outcomes": [
{
"Port": 22,
"State": true,
"Service": "SSH",
"Banner": "SSH-2.0-OpenSSH_7.4p1",
"Version": "OpenSSH_7.4p1",
"Vulnerabilities": [
{
"ID": "CVE-2017-15906",
"Description": "The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode",
"Severity": "Medium",
"Reference": "https://nvd.nist.gov/vuln/detail/CVE-2017-15906"
}
]
},
{
"Port": 80,
"State": true,
"Service": "HTTP",
"Banner": "HTTP/1.1 200 OKrnServer: Apache/2.4.41",
"Model": "Apache/2.4.41",
"Vulnerabilities": [
{
"ID": "CVE-2020-9490",
"Description": "A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41",
"Severity": "High",
"Reference": "https://nvd.nist.gov/vuln/detail/CVE-2020-9490"
}
]
},
{
"Port": 443,
"State": true,
"Service": "HTTPS",
"Banner": "HTTP/1.1 200 OKrnServer: nginx/1.18.0",
"Model": "nginx/1.18.0",
"Vulnerabilities": []
}
]
}
We’ve constructed a strong community vulnerability scanner in Go that demonstrates the language’s suitability for safety instruments. Our scanner rapidly opens up ports, identifies providers operating on them, and determines whether or not or not identified vulnerabilities are current.
It gives helpful details about providers operating on a community, together with multi-threading, service fingerprinting, and varied output codecs.
Remember the fact that instruments like a scanner ought to solely be utilized in moral and authorized parameters, with correct authorization to scan the goal techniques. When performed responsibly, common vulnerability scanning is a crucial facet of excellent safety posture that may assist defend your techniques from threats.
You will discover the whole supply code for this undertaking on GitHub