Home / Uncategorized / Building a Network Vulnerability Scanner with Go — SitePoint

Building a Network Vulnerability Scanner with Go — SitePoint

Penetration testing enables organizations to target potential security weaknesses in a network and provide a need to fix vulnerabilities before they are compromised by a malicious actor.

In this article, we are going to create a simple, reasonably robust, network vulnerability scanner using Go, a language that is very suitable for network programming since it is designed with concurrency in mind and has a great standard library.

1. Setting Up Our Project

Create a Vulnerability Scanner

We want to build a simple CLI tool that would be able to scan a network of hosts, find open ports, running services and discover possible vulnerability. The scanner is going to be very simple to start, but will grow increasingly capable as we layer on features.

So, first, we will create a new Go project:

mkdir goscan
cd goscan
go mod init github.com/yourusername/goscan

This initializes a new Go module for our project, which will help us manage dependencies.

Configuring Packages & Environment

For our scanner, we’ll leverage several Go packages:

package main

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    fmt.Println("GoScan - Network Vulnerability Scanner")
}

This is just our initial setup. This will be enough for some initial features, but we’ll add more imports on demand. Now other standard library packages like net will take care to do most of the networking that we need and sync will do concurrency, etc.

Ethical Considerations and Risks with Network Scanning

Now before we jump into implementation, we should touch on some ethical considerations around network scanning. Unauthorized network scanning or enumeration is illegal in many parts of the world and is treated as a vector for a cyber attack. You must always follow these rules:

  1. Permission: Only scan nonce networks and systems that you own or have explicit permission to scan.
  2. Scope: Define a clear scope for your scanning and don’t exceed it.
  3. Timing: Don’t go for hyper-scanning that can bring down services or raise security alerts.
  4. Disclosure: If you discover vulnerabilities, please do so responsibly by reporting them to the appropriate system owners.
  5. Legal Compliance: Understand and comply with local laws governing network scanning.

Misuse of scanning tools can result in legal action, system damage, or accidental denial of service. Our scanner will include safeguards like rate limiting, but the responsibility ultimately lies with the user to employ it ethically.

2. Simple Port Scanner

Vulnerability assessment is based on port scanning. The potential vulnerable services that are being offered on each of these open ports is the information that we are looking for. Now, let’s write a simple port scanner in Go.

Low-Level Implementation of Port Scanning

Port Scanning: Try to establish a connection to every possible port on a target host. If the connection succeeds, the port is open; if it fails, the port is closed or filtered. For this functionality, Go’s net package has got us covered.

So, here is our version of a simple port scanner:

package main

import (
    "fmt"
    "net"
    "time"
)

func scanPort(host string, port int, timeout time.Duration) bool {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    
    if err != nil {
        return false
    }
    
    conn.Close()
    return true
}

func main() {
    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 complete")
}

Using the Net Package

The code above makes use of the Go net package, which supplies network I/O interfaces and functions. So, what are the main pieces?

  1. net.DialTimeout: This function tries to connect to TCP network address with a timeout. It returns a connection, and an error, if any.
  2. Connection Handling: If it connects without issue, we know it is open, and we close the connection straight away to open up resources.
  3. Timeout Parameter: We specify a timeout to avoid hanging on any open ports that are filtered. Two seconds is a good initial value, but this can be tuned according to the network conditions.

Testing Our First Scan

Now let’s run our simple scanner against our localhost, where we may have some services running.

  1. Save the code to a file named main.go
  2. Run it with go run main.go

This will show what local ports are open. On a normal dev machine you might have 80 (HTTP), 443 (HTTPS), or any number of database ports in use based on what services you have up.

Here’s some sample output you might get:

Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan complete

Using this basic scanner works, but it comes with some big drawbacks:

  1. Speed: It’s painfully slow since it scans ports sequentially.
  2. Information: Just tells us whether a port is open, no service information.
  3. Limited Range: We’re only going to scan the first 1024 ports.

These restrictions render our scanner impractical to be used in the actual world.

3. Improving it from here: Multi-threaded scanning

Why the First Version is Slow

Our first port scanner works, although it’s painfully slow to be usable. The issue is its sequential method — probing one port at a time. When a host has lots of closed/filtered ports, we waste time waiting on a connection to time out on each port before we move to the other.

To show you the problem, let’s take a look at the timing of our basic scanner:

  • The worst case for scanning the first 1024 ports would take a maximum of 2048 seconds (more than 34 minutes) with 2 second timeout
  • But even when the connections to the closed ports immediately fail, this method is inefficient due to the network latency.

This one-by-one approach is a bottleneck for any real vulnerability scanning tool.

Adding Threading Support

Go is particularly good at concurrency using goroutines and channels. So, we leverage these features to try and scan multiple ports at once which increases performance significantly.

Now, let’s create a multithreaded port scanner:

package main

import (
    "fmt"
    "net"
    "sync"
    "time"
)

type Result struct {
    Port  int
    State bool
}

func scanPort(host string, port int, timeout time.Duration) Result {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    
    if err != nil {
        return Result{Port: port, State: false}
    }
    
    conn.Close()
    return Result{Port: port, State: true}
}

func scanPorts(host string, start, end int, timeout time.Duration) []Result {
    var results []Result
    var wg sync.WaitGroup
    
    
    resultChan := make(chan Result, end-start+1)
    
    
    
    semaphore := make(chan struct{}, 100) 
    
    
    for port := start; port  end; port++ {
        wg.Add(1)
        go func(p int) {
            defer wg.Done()
            
            
            semaphore - struct{}{}
            defer func() { -semaphore }() 
            
            result := scanPort(host, p, timeout)
            resultChan - result
        }(port)
    }
    
    
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    
    
    for result := range resultChan {
        if result.State {
            results = append(results, result)
        }
    }
    
    return results
}

func main() {
    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()
    
    results := scanPorts(host, startPort, endPort, timeout)
    
    elapsed := time.Since(startTime)
    
    fmt.Printf("nScan completed in %sn", elapsed)
    fmt.Printf("Found %d open ports:n", len(results))
    
    for _, result := range results {
        fmt.Printf("Port %d is openn", result.Port)
    }
}

Results from Multiple Threads

Now, let us take a look at the performance gains as well as concurrency mechanisms we added to our improved scanner:

  1. Goroutines: To make the scanning efficient we fire up a goroutine for every port that we need to scan, so while we are checking one port we can check other ports simultaneously.
  2. WaitGroup: The sync. WaitGroupAs we induce goroutines, We want to wait for their completion. WaitGroup helps us to track all running goroutines and wait for them to complete.
  3. Result Channel: We create a buffers channel for results from all the goroutines in order.
  4. Semaphore Pattern: A semaphore is used, implemented using a channel, that limits the number of scans that are allowed in parallel. It’s what prevents us from overwhelming the actual target system or even our own machine with so many connection open.
  5. Reduced Timeout: Since we run many of these scans in a parallel fashion, we use a lower timeout.

The performance gap is substantial. So, when we implement this, it can let us scan 1024 ports in minutes, and certainly less than half an hour.

Sample output:

Scanning localhost from port 1 to 1024
Scan completed in 3.2s
Found 3 open ports:
Port 22 is open
Port 80 is open
Port 443 is open

The multi-threaded approach scales very well for larger port ranges and multiple hosts. The semaphore pattern guarantees that we do not run out of system resources despite scanning over a thousand ports.

4. Adding Service Detection

Now that we have a fast, efficient port scanner, the next step is to know what services are running on those open ports. This is commonly known as “service fingerprinting” or “banner grabbing,” a process by which we connect to open ports and examine the data returned.

Implementation of Banner Grabbing

Banner grabbing is when we open a service and read the response (banner) it sends us. So it’s a good way of identifying if something runs, as many services identify themselves in these banners.

Now let’s add banner grabbing to our scanner:

package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
    "sync"
    "time"
)

type ScanResult struct {
    Port     int
    State    bool
    Service  string
    Banner   string
    Version  string
}

func grabBanner(host string, port int, timeout time.Duration) (string, error) {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    if err != nil {
        return "", err
    }
    defer conn.Close()
    
    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
    }
    
    version := "Unknown"
    
    lowerBanner := strings.ToLower(banner)
    
    
    if strings.Contains(lowerBanner, "ssh") {
        service = "SSH"
        parts := strings.Split(banner, " ")
        if len(parts) >= 2 {
            version = parts[1]
        }
    }
    
    
    if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") || 
       strings.Contains(lowerBanner, "nginx") {
        if port == 443 {
            service = "HTTPS"
        } else {
            service = "HTTP"
        }
        
        
        if strings.Contains(banner, "Server:") {
            parts := strings.Split(banner, "Server:")
            if len(parts) >= 2 {
                version = strings.TrimSpace(parts[1])
            }
        }
    }
    
    return service, version
}

func scanPort(host string, port int, timeout time.Duration) ScanResult {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    
    if err != nil {
        return ScanResult{Port: port, State: false}
    }
    
    conn.Close()
    
    banner, err := grabBanner(host, port, timeout)
    
    service := "Unknown"
    version := "Unknown"
    
    if err == nil && banner != "" {
        service, version = identifyService(port, banner)
    }
    
    return ScanResult{
        Port:    port,
        State:   true,
        Service: service,
        Banner:  banner,
        Version: version,
    }
}

func scanPorts(host string, start, end int, timeout time.Duration) []ScanResult {
    var results []ScanResult
    var wg sync.WaitGroup
    
    resultChan := make(chan ScanResult, end-start+1)
    
    semaphore := make(chan struct{}, 100)
    
    for port := start; port  end; port++ {
        wg.Add(1)
        go func(p int) {
            defer wg.Done()
            
            semaphore - struct{}{}
            defer func() { -semaphore }()
            
            result := scanPort(host, p, timeout)
            resultChan - result
        }(port)
    }
    
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    
    for result := range resultChan {
        if result.State {
            results = append(results, result)
        }
    }
    
    return results
}

func main() {
    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()
    
    results := scanPorts(host, startPort, endPort, timeout)
    
    elapsed := time.Since(startTime)
    
    fmt.Printf("nScan completed in %sn", elapsed)
    fmt.Printf("Found %d open ports:nn", len(results))
    
    fmt.Println("PORTtSERVICEtVERSIONtBANNER")
    fmt.Println("----t-------t-------t------")
    for _, result := range results {
        bannerPreview := ""
        if len(result.Banner) > 30 {
            bannerPreview = result.Banner[:30] + "..."
        } else {
            bannerPreview = result.Banner
        }
        
        fmt.Printf("%dt%st%st%sn", 
            result.Port, 
            result.Service, 
            result.Version, 
            bannerPreview)
    }
}

Identifying Running Services

We use two main strategies for service detection:

  1. Port-based identification: By mapping onto common port numbers (e.g., port 80 is HTTP) we have a likely guess to the service.
  2. Banner analysis: We take the banner text and look for service identifiers and version information.

The first function, grabBanner, tries to grab the first response from a service. Some services such as HTTP require us to send a request and receive a reply, for which we use add case-specific cases.

Basic Version Detection

Version detection is important for the identification of vulnerabilities. Where possible, our scanner parses service banners to pull version information:

  1. SSH: Usually provides version info in the form of “SSH-2. 0-OpenSSH_7.4”
  2. HTTP servers: Usually respond with their version information in response headers such as “Server: Apache/2.4.29”
  3. Database servers: Might disclose version information in their welcome messages

Now the output returns a lot more information for every open port:

Scanning localhost from port 1 to 1024
Scan completed in 5.4s
Found 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 foreign...

This enhanced information is much more valuable for vulnerability assessment.

5. Vulnerability Detection Implementation

Now that we can enumerate the services running and what version they are, we are going to implement detection for the vulnerabilities. The service information will be analyzed and compared against known vulnerabilities.

Writing Simple Vulnerability Tests

We will form a database from known vulnerabilities based on common services and versions. For simplicity, we will create an in-code vulnerability database, though in a real-world scenario, a scanner would most likely query external vulnerability databases (such as CVE or NVD).

Now, let’s grow our code out to detect vulnerabilities:

package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
    "sync"
    "time"
)

type ScanResult struct {
    Port          int
    State         bool
    Service       string
    Banner        string
    Version       string
    Vulnerabilities []Vulnerability
}

type Vulnerability struct {
    ID          string
    Description string
    Severity    string
    Reference   string
}

var vulnerabilityDB = []struct {
    Service     string
    Version     string
    Vulnerability Vulnerability
}{
    {
        Service: "SSH",
        Version: "OpenSSH_7.4",
        Vulnerability: Vulnerability{
            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",
        },
    },
    {
        Service: "HTTP",
        Version: "Apache/2.4.29",
        Vulnerability: Vulnerability{
            ID:          "CVE-2019-0211",
            Description: "Apache HTTP Server 2.4.17 to 2.4.38 - Local privilege escalation through mod_prefork and mod_http2",
            Severity:    "High",
            Reference:   "https://nvd.nist.gov/vuln/detail/CVE-2019-0211",
        },
    },
    {
        Service: "HTTP",
        Version: "Apache/2.4.41",
        Vulnerability: Vulnerability{
            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",
        },
    },
    {
        Service: "MySQL",
        Version: "5.7",
        Vulnerability: Vulnerability{
            ID:          "CVE-2020-2922",
            Description: "Vulnerability in MySQL Server allows unauthorized users to obtain sensitive information",
            Severity:    "Medium",
            Reference:   "https://nvd.nist.gov/vuln/detail/CVE-2020-2922",
        },
    },
    
}


func checkVulnerabilities(service, version string) []Vulnerability {
    var vulnerabilities []Vulnerability
    
    for _, vuln := range vulnerabilityDB {
        
        if vuln.Service == service && strings.Contains(version, vuln.Version) {
            vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
        }
    }
    
    return vulnerabilities
}


func grabBanner(host string, port int, timeout time.Duration) (string, error) {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    if err != nil {
        return "", err
    }
    defer conn.Close()
    
    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
    }
    
    version := "Unknown"
    
    lowerBanner := strings.ToLower(banner)
    
    if strings.Contains(lowerBanner, "ssh") {
        service = "SSH"
        parts := strings.Split(banner, " ")
        if len(parts) >= 2 {
            version = parts[1]
        }
    }
    
    if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") || 
       strings.Contains(lowerBanner, "nginx") {
        if port == 443 {
            service = "HTTPS"
        } else {
            service = "HTTP"
        }
        
        if strings.Contains(banner, "Server:") {
            parts := strings.Split(banner, "Server:")
            if len(parts) >= 2 {
                version = strings.TrimSpace(parts[1])
            }
        }
    }
    
    return service, version
}

func scanPort(host string, port int, timeout time.Duration) ScanResult {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    
    if err != nil {
        return ScanResult{Port: port, State: false}
    }
    
    conn.Close()
    
    banner, err := grabBanner(host, port, timeout)
    
    service := "Unknown"
    version := "Unknown"
    
    if err == nil && banner != "" {
        service, version = identifyService(port, banner)
    }
    
    vulnerabilities := checkVulnerabilities(service, version)
    
    return ScanResult{
        Port:           port,
        State:          true,
        Service:        service,
        Banner:         banner,
        Version:        version,
        Vulnerabilities: vulnerabilities,
    }
}

func scanPorts(host string, start, end int, timeout time.Duration) []ScanResult {
    var results []ScanResult
    var wg sync.WaitGroup
    
    resultChan := make(chan ScanResult, end-start+1)
    
    semaphore := make(chan struct{}, 100)
    
    for port := start; port  end; port++ {
        wg.Add(1)
        go func(p int) {
            defer wg.Done()
            
            semaphore - struct{}{}
            defer func() { -semaphore }()
            
            result := scanPort(host, p, timeout)
            resultChan - result
        }(port)
    }
    
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    
    for result := range resultChan {
        if result.State {
            results = append(results, result)
        }
    }
    
    return results
}

func main() {
    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()
    
    results := scanPorts(host, startPort, endPort, timeout)
    
    elapsed := time.Since(startTime)
    
    fmt.Printf("nScan completed in %sn", elapsed)
    fmt.Printf("Found %d open ports:nn", len(results))
    
    fmt.Println("PORTtSERVICEtVERSION")
    fmt.Println("----t-------t-------")
    for _, result := range results {
        fmt.Printf("%dt%st%sn", 
            result.Port, 
            result.Service, 
            result.Version)
        
        if len(result.Vulnerabilities) > 0 {
            fmt.Println("  Vulnerabilities:")
            for _, vuln := range result.Vulnerabilities {
                fmt.Printf("    [%s] %s - %sn", 
                    vuln.Severity, 
                    vuln.ID, 
                    vuln.Description)
                fmt.Printf("    Reference: %snn", vuln.Reference)
            }
        }
    }
}package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
    "sync"
    "time"
)

type ScanResult struct {
    Port          int
    State         bool
    Service       string
    Banner        string
    Version       string
    Vulnerabilities []Vulnerability
}

type Vulnerability struct {
    ID          string
    Description string
    Severity    string
    Reference   string
}

var vulnerabilityDB = []struct {
    Service     string
    Version     string
    Vulnerability Vulnerability
}{
    {
        Service: "SSH",
        Version: "OpenSSH_7.4",
        Vulnerability: Vulnerability{
            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",
        },
    },
    {
        Service: "HTTP",
        Version: "Apache/2.4.29",
        Vulnerability: Vulnerability{
            ID:          "CVE-2019-0211",
            Description: "Apache HTTP Server 2.4.17 to 2.4.38 - Local privilege escalation through mod_prefork and mod_http2",
            Severity:    "High",
            Reference:   "https://nvd.nist.gov/vuln/detail/CVE-2019-0211",
        },
    },
    {
        Service: "HTTP",
        Version: "Apache/2.4.41",
        Vulnerability: Vulnerability{
            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",
        },
    },
    {
        Service: "MySQL",
        Version: "5.7",
        Vulnerability: Vulnerability{
            ID:          "CVE-2020-2922",
            Description: "Vulnerability in MySQL Server allows unauthorized users to obtain sensitive information",
            Severity:    "Medium",
            Reference:   "https://nvd.nist.gov/vuln/detail/CVE-2020-2922",
        },
    },
    
}


func checkVulnerabilities(service, version string) []Vulnerability {
    var vulnerabilities []Vulnerability
    
    for _, vuln := range vulnerabilityDB {
        
        if vuln.Service == service && strings.Contains(version, vuln.Version) {
            vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
        }
    }
    
    return vulnerabilities
}


func grabBanner(host string, port int, timeout time.Duration) (string, error) {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    if err != nil {
        return "", err
    }
    defer conn.Close()
    
    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
    }
    
    version := "Unknown"
    
    lowerBanner := strings.ToLower(banner)
    
    if strings.Contains(lowerBanner, "ssh") {
        service = "SSH"
        parts := strings.Split(banner, " ")
        if len(parts) >= 2 {
            version = parts[1]
        }
    }
    
    if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") || 
       strings.Contains(lowerBanner, "nginx") {
        if port == 443 {
            service = "HTTPS"
        } else {
            service = "HTTP"
        }
        
        if strings.Contains(banner, "Server:") {
            parts := strings.Split(banner, "Server:")
            if len(parts) >= 2 {
                version = strings.TrimSpace(parts[1])
            }
        }
    }
    
    return service, version
}

func scanPort(host string, port int, timeout time.Duration) ScanResult {
    target := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.DialTimeout("tcp", target, timeout)
    
    if err != nil {
        return ScanResult{Port: port, State: false}
    }
    
    conn.Close()
    
    banner, err := grabBanner(host, port, timeout)
    
    service := "Unknown"
    version := "Unknown"
    
    if err == nil && banner != "" {
        service, version = identifyService(port, banner)
    }
    
    vulnerabilities := checkVulnerabilities(service, version)
    
    return ScanResult{
        Port:           port,
        State:          true,
        Service:        service,
        Banner:         banner,
        Version:        version,
        Vulnerabilities: vulnerabilities,
    }
}

func scanPorts(host string, start, end int, timeout time.Duration) []ScanResult {
    var results []ScanResult
    var wg sync.WaitGroup
    
    resultChan := make(chan ScanResult, end-start+1)
    
    semaphore := make(chan struct{}, 100)
    
    for port := start; port  end; port++ {
        wg.Add(1)
        go func(p int) {
            defer wg.Done()
            
            semaphore - struct{}{}
            defer func() { -semaphore }()
            
            result := scanPort(host, p, timeout)
            resultChan - result
        }(port)
    }
    
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    
    for result := range resultChan {
        if result.State {
            results = append(results, result)
        }
    }
    
    return results
}

func main() {
    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()
    
    results := scanPorts(host, startPort, endPort, timeout)
    
    elapsed := time.Since(startTime)
    
    fmt.Printf("nScan completed in %sn", elapsed)
    fmt.Printf("Found %d open ports:nn", len(results))
    
    fmt.Println("PORTtSERVICEtVERSION")
    fmt.Println("----t-------t-------")
    for _, result := range results {
        fmt.Printf("%dt%st%sn", 
            result.Port, 
            result.Service, 
            result.Version)
        
        if len(result.Vulnerabilities) > 0 {
            fmt.Println("  Vulnerabilities:")
            for _, vuln := range result.Vulnerabilities {
                fmt.Printf("    [%s] %s - %sn", 
                    vuln.Severity, 
                    vuln.ID, 
                    vuln.Description)
                fmt.Printf("    Reference: %snn", vuln.Reference)
            }
        }
    }
}

Version-Based Matching of Vulnerabilities

We have a naive version-matching approach for vulnerability detection:

  1. Direct Matching: Here, we match the service type and version to our vulnerability database.
  2. Partial Matching: For vulnerable version matching, we perform containment checks on the version string, allowing us to identify vulnerable systems even if the version string contains extra information.

In a real scanner this matching would be more complex, accounting for:

  • Version ranges (i.e. versions 2.4.0 to 2.4.38 are affected)
  • Configuration-specific vulnerabilities
  • OS-specific issues
  • More nuanced version comparisons

Reporting What We Find

Reporting the results is the last step in the vulnerability detection and that needs to be done in a concise and actionable format. Our scanner now:

  1. Lists all open ports with service and version information
  2. For each vulnerable service, displays:
    • The vulnerability ID (e.g., CVE number)
    • A description of the vulnerability
    • Severity rating
    • Reference link for more information

Sample output:

Scanning localhost from port 1 to 1024
Scan completed in 6.2s
Found 3 open ports:

PORT    SERVICE VERSION
----    ------- -------
22      SSH     OpenSSH_7.4p1
  Vulnerabilities:
    [Medium] CVE-2017-15906 - The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode
    Reference: https://nvd.nist.gov/vuln/detail/CVE-2017-15906

80      HTTP    Apache/2.4.41
  Vulnerabilities:
    [High] CVE-2020-9490 - A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41
    Reference: https://nvd.nist.gov/vuln/detail/CVE-2020-9490

443     HTTPS   Unknown

This thorough vulnerability data guides cybersecurity specialists to promptly pinpoint and rank security concerns that require resolution.

Final Touches and Usage

Now you have a basic vulnerability scanner with service detection and vulnerability matching; now let us polish it a little so that it is more practical to use in the real world.

Command Line Arguments

Our scanner should be configurable via command-line flags that can set targets, port ranges, and scan options. This is simple with Go’s flag package.

Next, let’s add command-line arguments:

package main

import (
    "bufio"
    "encoding/json"
    "flag"
    "fmt"
    "net"
    "os"
    "strings"
    "sync"
    "time"
)

type ScanResult struct {
    Port            int
    State           bool
    Service         string
    Banner          string
    Version         string
    Vulnerabilities []Vulnerability
}

type Vulnerability struct {
    ID          string
    Description string
    Severity    string
    Reference   string
}

var vulnerabilityDB = []struct {
    Service       string
    Version       string
    Vulnerability Vulnerability
}{
    
}


func main() {
    hostPtr := flag.String("host", "", "Target host to scan (required)")
    startPortPtr := flag.Int("start", 1, "Starting port number")
    endPortPtr := flag.Int("end", 1024, "Ending port number")
    timeoutPtr := flag.Int("timeout", 1000, "Timeout in milliseconds")
    concurrencyPtr := flag.Int("concurrency", 100, "Number of concurrent scans")
    formatPtr := flag.String("format", "text", "Output format: text, json, or csv")
    verbosePtr := flag.Bool("verbose", false, "Show verbose output including banners")
    outputFilePtr := flag.String("output", "", "Output file (default is stdout)")
    
    flag.Parse()
    
    if *hostPtr == "" {
        fmt.Println("Error: host is required")
        flag.Usage()
        os.Exit(1)
    }
    
    if *startPortPtr  1 || *startPortPtr > 65535 {
        fmt.Println("Error: starting port must be between 1 and 65535")
        os.Exit(1)
    }
    if *endPortPtr  1 || *endPortPtr > 65535 {
        fmt.Println("Error: ending port must be between 1 and 65535")
        os.Exit(1)
    }
    if *startPortPtr > *endPortPtr {
        fmt.Println("Error: starting port must be less than or equal to ending port")
        os.Exit(1)
    }
    
    timeout := time.Duration(*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.Close()
    } else {
        outputFile = os.Stdout
    }
    
    fmt.Fprintf(outputFile, "Scanning %s from port %d to %dn", *hostPtr, *startPortPtr, *endPortPtr)
    startTime := time.Now()
    
    var results []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.Done()
            
            semaphore - struct{}{}
            defer func() { -semaphore }()
            
            result := scanPort(*hostPtr, p, timeout)
            resultChan - result
        }(port)
    }
    
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    
    for result := range resultChan {
        if result.State {
            results = append(results, result)
        }
    }
    
    elapsed := time.Since(startTime)
    
    switch *formatPtr {
    case "json":
        outputJSON(outputFile, results, elapsed)
    case "csv":
        outputCSV(outputFile, results, elapsed, *verbosePtr)
    default:
        outputText(outputFile, results, elapsed, *verbosePtr)
    }
}

func outputText(w *os.File, results []ScanResult, elapsed time.Duration, verbose bool) {
    fmt.Fprintf(w, "nScan completed in %sn", elapsed)
    fmt.Fprintf(w, "Found %d open ports:nn", len(results))
    
    if len(results) == 0 {
        fmt.Fprintf(w, "No open ports found.n")
        return
    }
    
    fmt.Fprintf(w, "PORTtSERVICEtVERSIONn")
    fmt.Fprintf(w, "----t-------t-------n")
    
    for _, result := range results {
        fmt.Fprintf(w, "%dt%st%sn", 
            result.Port, 
            result.Service, 
            result.Version)
        
        if verbose {
            fmt.Fprintf(w, "  Banner: %sn", result.Banner)
        }
        
        if len(result.Vulnerabilities) > 0 {
            fmt.Fprintf(w, "  Vulnerabilities:n")
            for _, vuln := range 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, results []ScanResult, elapsed time.Duration) {
    output := struct {
        ScanTime   string       `json:"scan_time"`
        ElapsedTime string       `json:"elapsed_time"`
        TotalPorts int          `json:"total_ports"`
        OpenPorts  int          `json:"open_ports"`
        Results    []ScanResult `json:"results"`
    }{
        ScanTime:    time.Now().Format(time.RFC3339),
        ElapsedTime: elapsed.String(),
        TotalPorts:  0, 
        OpenPorts:   len(results),
        Results:     results,
    }
    
    encoder := json.NewEncoder(w)
    encoder.SetIndent("", "  ")
    encoder.Encode(output)
}

func outputCSV(w *os.File, results []ScanResult, elapsed time.Duration, verbose bool) {
    fmt.Fprintf(w, "Port,Service,Version,Vulnerability ID,Severity,Descriptionn")
    
    for _, result := range results {
        if len(result.Vulnerabilities) == 0 {
            fmt.Fprintf(w, "%d,%s,%s,,,n", 
                result.Port, 
                escapeCSV(result.Service), 
                escapeCSV(result.Version))
        } else {
            for _, vuln := range result.Vulnerabilities {
                fmt.Fprintf(w, "%d,%s,%s,%s,%s,%sn", 
                    result.Port, 
                    escapeCSV(result.Service), 
                    escapeCSV(result.Version),
                    escapeCSV(vuln.ID),
                    escapeCSV(vuln.Severity),
                    escapeCSV(vuln.Description))
            }
        }
    }
    
    fmt.Fprintf(w, "n# Scan completed in %s, found %d open portsn", 
        elapsed, len(results))
}

func escapeCSV(s string) string {
    if strings.Contains(s, ",") || strings.Contains(s, """) || strings.Contains(s, "n") {
        return """ + strings.ReplaceAll(s, """, """") + """
    }
    return s
}

Output Formatting

Our scanner can now output to three formats:

  1. Text: Easy to read, easy to write, great for interactive use.
  2. JSON: Structured output useful for machine processing and integration with other tools.
  3. CSV: A spreadsheet-compatible format for analysis and reporting.

The output text also provides more information such as raw banner information if the verbose flag is set. This is also handy for debugging or deep-dive analysis.

Example Usage and Results

So, here are some possibilities if you are going to use our scanner for different occasions:

Basic scan of a single host:

$ go run main.go -host example.com

Scan a specific port range:

$ go run main.go -host example.com -start 80 -end 443

Save results to a JSON file:

$ go run main.go -host example.com -format json -output results.json

Verbose scan with increased timeout:

$ go run main.go -host example.com -verbose -timeout 2000

Scan with higher concurrency for faster results:

$ go run main.go -host example.com -concurrency 200

Example text output:

Scanning example.com from port 1 to 1024
Scan completed in 12.6s
Found 3 open ports:

PORT    SERVICE VERSION
----    ------- -------
22      SSH     OpenSSH_7.4p1
  Vulnerabilities:
    [Medium] CVE-2017-15906 - The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode
    Reference: https://nvd.nist.gov/vuln/detail/CVE-2017-15906

80      HTTP    Apache/2.4.41
  Vulnerabilities:
    [High] CVE-2020-9490 - A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41
    Reference: https://nvd.nist.gov/vuln/detail/CVE-2020-9490

443     HTTPS   nginx/1.18.0

JSON output example:

{
  "scan_time": "2025-03-18T14:30:00Z",
  "elapsed_time": "12.6s",
  "total_ports": 1024,
  "open_ports": 3,
  "results": [
    {
      "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",
      "Version": "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",
      "Version": "nginx/1.18.0",
      "Vulnerabilities": []
    }
  ]
}

We’ve built a robust network vulnerability scanner in Go that demonstrates the language’s suitability for security tools. Our scanner quickly opens up ports, identifies services running on them, and determines whether or not known vulnerabilities are present.

It offers useful information about services running on a network, including multi-threading, service fingerprinting, and various output formats.

Keep in mind that tools like a scanner should only be used in ethical and legal parameters, with proper authorization to scan the target systems. When conducted responsibly, regular vulnerability scanning is a critical aspect of good security posture that can help protect your systems from threats.

You can find the complete source code for this project on GitHub

Leave a Reply

Your email address will not be published. Required fields are marked *