Penetration testing permits 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 may be very appropriate for community programming since it’s designed with concurrency in thoughts and has an ideal normal library.
1. Setting Up Our Venture
Create a Vulnerability Scanner
We wish to construct a easy CLI instrument that may be capable of scan a community of hosts, discover open ports, operating providers and uncover attainable vulnerability. The scanner goes to be quite simple to start out, however will develop more and more succesful as we layer on options.
So, first, we are going to 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 can assist us handle dependencies.
Configuring Packages & Setting
For our scanner, we’ll leverage a number of Go packages:
package deal essential
import (
"fmt"
"internet"
"os"
"strconv"
"sync"
"time"
)
func essential() {
fmt.Println("GoScan - Community Vulnerability Scanner")
}
That is simply our preliminary setup. This might 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 a lot of the networking that we’d like and sync will do concurrency, and so on.
Moral Concerns and Dangers with Community Scanning
Now earlier than we leap into implementation, we must always contact on some moral issues round community scanning. Unauthorized community scanning or enumeration is unlawful in lots of components of the world and is handled as a vector for a cyber assault. You should all the time 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 convey down providers or elevate safety alerts.
- Disclosure: If you happen to uncover vulnerabilities, please accomplish that 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 harm, or unintentional denial of service. Our scanner will embrace safeguards like price limiting, however the duty 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 might be being provided on every of those open ports is the data that we’re searching for. Now, let’s write a easy port scanner in Go.
Low-Degree 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 package deal has received us lined.
So, right here is our model of a easy port scanner:
package deal essential
import (
"fmt"
"internet"
"time"
)
func scanPort(host string, port int, timeout time.Period) 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 essential() {
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 Web Package deal
The code above makes use of the Go internet package deal, which provides community I/O interfaces and features. So, what are the primary items?
- internet.DialTimeout: This operate tries to connect with TCP community deal with 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 assets.
- Timeout Parameter: We specify a timeout to keep away from hanging on any open ports which might be filtered. Two seconds is an effective preliminary worth, however this may be tuned in line 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
essential.go
- Run it with
go run essential.go
This may present what native ports are open. On a traditional dev machine you might need 80 (HTTP), 443 (HTTPS), or any variety of database ports in use based mostly on what providers you’ve 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 primary scanner works, however it comes with some massive 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 Sluggish
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 number has plenty 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 indicate you the issue, let’s check out the timing of our primary 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 because of the community latency.
This one-by-one strategy is a bottleneck for any actual vulnerability scanning instrument.
Including Threading Assist
Go is especially good at concurrency utilizing goroutines and channels. So, we leverage these options to try to scan a number of ports without delay which will increase efficiency considerably.
Now, let’s create a multithreaded port scanner:
package deal essential
import (
"fmt"
"internet"
"sync"
"time"
)
sort Outcome struct {
Port int
State bool
}
func scanPort(host string, port int, timeout time.Period) 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.Period) []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.Performed()
semaphore <- struct{}{}
defer func() { <-semaphore }()
outcome := scanPort(host, p, timeout)
resultChan <- outcome
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for outcome := vary resultChan {
if outcome.State {
outcomes = append(outcomes, outcome)
}
}
return outcomes
}
func essential() {
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 _, outcome := vary outcomes {
fmt.Printf("Port %d is openn", outcome.Port)
}
}
Outcomes from A number of Threads
Now, allow us to check out the efficiency positive factors 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 will verify different ports concurrently.
- WaitGroup: The sync. WaitGroupAs we induce goroutines, We wish 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, carried out utilizing a channel, that limits the variety of scans which might be allowed in parallel. It’s what prevents us from overwhelming the precise goal system and even our personal machine with so many connection open.
- Lowered Timeout: Since we run many of those scans in a parallel style, we use a decrease timeout.
The efficiency hole is substantial. So, after we implement this, it may possibly allow us to scan 1024 ports in minutes, and positively 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 strategy scales very properly for bigger port ranges and a number of hosts. The semaphore sample ensures that we don’t run out of system assets regardless of scanning over a thousand ports.
4. Including Service Detection
Now that we have now a quick, environment friendly port scanner, the subsequent step is to know what providers are operating on these open ports. That is generally referred to as “service fingerprinting” or “banner grabbing,” a course of by which we connect with open ports and study the info returned.
Implementation of Banner Grabbing
Banner grabbing is after we open a service and browse 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:
package deal essential
import (
"bufio"
"fmt"
"internet"
"strings"
"sync"
"time"
)
sort ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
}
func grabBanner(host string, port int, timeout time.Period) (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.Comprises(lowerBanner, "ssh") {
service = "SSH"
components := strings.Break up(banner, " ")
if len(components) >= 2 {
model = components[1]
}
}
if strings.Comprises(lowerBanner, "http") || strings.Comprises(lowerBanner, "apache") ||
strings.Comprises(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Comprises(banner, "Server:") {
components := strings.Break up(banner, "Server:")
if len(components) >= 2 {
model = strings.TrimSpace(components[1])
}
}
}
return service, model
}
func scanPort(host string, port int, timeout time.Period) 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.Period) []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.Performed()
semaphore <- struct{}{}
defer func() { <-semaphore }()
outcome := scanPort(host, p, timeout)
resultChan <- outcome
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for outcome := vary resultChan {
if outcome.State {
outcomes = append(outcomes, outcome)
}
}
return outcomes
}
func essential() {
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 _, outcome := vary outcomes {
bannerPreview := ""
if len(outcome.Banner) > 30 {
bannerPreview = outcome.Banner[:30] + "..."
} else {
bannerPreview = outcome.Banner
}
fmt.Printf("%dtpercentstpercentstpercentsn",
outcome.Port,
outcome.Service,
outcome.Model,
bannerPreview)
}
}
Figuring out Operating Providers
We use two essential methods for service detection:
- Port-based identification: By mapping onto frequent port numbers (e.g., port 80 is HTTP) we have now a possible guess to the service.
- Banner evaluation: We take the banner textual content and search for service identifiers and model info.
The primary operate, 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 circumstances.
Primary Model Detection
Model detection is vital for the identification of vulnerabilities. The place attainable, our scanner parses service banners to drag model info:
- SSH: Often offers 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 international...
This enhanced info is way more beneficial for vulnerability evaluation.
5. Vulnerability Detection Implementation
Now that we will enumerate the providers operating and what model they’re, we’re going to implement detection for the vulnerabilities. The service info might be analyzed and in contrast towards identified vulnerabilities.
Writing Easy Vulnerability Checks
We are going to kind a database from identified vulnerabilities based mostly on frequent providers and variations. For simplicity, we are going to create an in-code vulnerability database, although in a real-world situation, a scanner would more than likely question exterior vulnerability databases (resembling CVE or NVD).
Now, let’s develop our code out to detect vulnerabilities:
package deal essential
import (
"bufio"
"fmt"
"internet"
"strings"
"sync"
"time"
)
sort ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
Vulnerabilities []Vulnerability
}
sort 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 operate 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 may 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.Comprises(model, vuln.Model) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
func grabBanner(host string, port int, timeout time.Period) (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.Comprises(lowerBanner, "ssh") {
service = "SSH"
components := strings.Break up(banner, " ")
if len(components) >= 2 {
model = components[1]
}
}
if strings.Comprises(lowerBanner, "http") || strings.Comprises(lowerBanner, "apache") ||
strings.Comprises(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Comprises(banner, "Server:") {
components := strings.Break up(banner, "Server:")
if len(components) >= 2 {
model = strings.TrimSpace(components[1])
}
}
}
return service, model
}
func scanPort(host string, port int, timeout time.Period) 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.Period) []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.Performed()
semaphore <- struct{}{}
defer func() { <-semaphore }()
outcome := scanPort(host, p, timeout)
resultChan <- outcome
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for outcome := vary resultChan {
if outcome.State {
outcomes = append(outcomes, outcome)
}
}
return outcomes
}
func essential() {
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 _, outcome := vary outcomes {
fmt.Printf("%dtpercentstpercentsn",
outcome.Port,
outcome.Service,
outcome.Model)
if len(outcome.Vulnerabilities) > 0 {
fmt.Println(" Vulnerabilities:")
for _, vuln := vary outcome.Vulnerabilities {
fmt.Printf(" [%s] %s - %sn",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Printf(" Reference: %snn", vuln.Reference)
}
}
}
}package deal essential
import (
"bufio"
"fmt"
"internet"
"strings"
"sync"
"time"
)
sort ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
Vulnerabilities []Vulnerability
}
sort 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 operate 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 may 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.Comprises(model, vuln.Model) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
func grabBanner(host string, port int, timeout time.Period) (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.Comprises(lowerBanner, "ssh") {
service = "SSH"
components := strings.Break up(banner, " ")
if len(components) >= 2 {
model = components[1]
}
}
if strings.Comprises(lowerBanner, "http") || strings.Comprises(lowerBanner, "apache") ||
strings.Comprises(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Comprises(banner, "Server:") {
components := strings.Break up(banner, "Server:")
if len(components) >= 2 {
model = strings.TrimSpace(components[1])
}
}
}
return service, model
}
func scanPort(host string, port int, timeout time.Period) 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.Period) []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.Performed()
semaphore <- struct{}{}
defer func() { <-semaphore }()
outcome := scanPort(host, p, timeout)
resultChan <- outcome
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for outcome := vary resultChan {
if outcome.State {
outcomes = append(outcomes, outcome)
}
}
return outcomes
}
func essential() {
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 _, outcome := vary outcomes {
fmt.Printf("%dtpercentstpercentsn",
outcome.Port,
outcome.Service,
outcome.Model)
if len(outcome.Vulnerabilities) > 0 {
fmt.Println(" Vulnerabilities:")
for _, vuln := vary outcome.Vulnerabilities {
fmt.Printf(" [%s] %s - %sn",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Printf(" Reference: %snn", vuln.Reference)
}
}
}
}
Model-Based mostly Matching of Vulnerabilities
We now have a naive version-matching strategy for vulnerability detection:
- Direct Matching: Right here, we match the service sort 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 further info.
In an actual scanner this matching can be extra advanced, 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 executed 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 score
- 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 operate 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 may 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 information guides cybersecurity specialists to promptly pinpoint and rank safety issues that require decision.
Remaining Touches and Utilization
Now you’ve a primary vulnerability scanner with service detection and vulnerability matching; now allow us to polish it a little bit in order that it’s extra sensible to make use of in the true world.
Command Line Arguments
Our scanner ought to be configurable through command-line flags that may set targets, port ranges, and scan choices. That is easy with Go’s flag package deal.
Subsequent, let’s add command-line arguments:
package deal essential
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"internet"
"os"
"strings"
"sync"
"time"
)
sort ScanResult struct {
Port int
State bool
Service string
Banner string
Model string
Vulnerabilities []Vulnerability
}
sort Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = []struct {
Service string
Model string
Vulnerability Vulnerability
}{
}
func essential() {
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.Period(*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.Performed()
semaphore <- struct{}{}
defer func() { <-semaphore }()
outcome := scanPort(*hostPtr, p, timeout)
resultChan <- outcome
}(port)
}
go func() {
wg.Wait()
shut(resultChan)
}()
for outcome := vary resultChan {
if outcome.State {
outcomes = append(outcomes, outcome)
}
}
elapsed := time.Since(startTime)
change *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.Period, 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 _, outcome := vary outcomes {
fmt.Fprintf(w, "%dtpercentstpercentsn",
outcome.Port,
outcome.Service,
outcome.Model)
if verbose {
fmt.Fprintf(w, " Banner: %sn", outcome.Banner)
}
if len(outcome.Vulnerabilities) > 0 {
fmt.Fprintf(w, " Vulnerabilities:n")
for _, vuln := vary outcome.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.Period) {
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.Period, verbose bool) {
fmt.Fprintf(w, "Port,Service,Model,Vulnerability ID,Severity,Descriptionn")
for _, outcome := vary outcomes {
if len(outcome.Vulnerabilities) == 0 {
fmt.Fprintf(w, "%d,%s,%s,,,n",
outcome.Port,
escapeCSV(outcome.Service),
escapeCSV(outcome.Model))
} else {
for _, vuln := vary outcome.Vulnerabilities {
fmt.Fprintf(w, "%d,%s,%s,%s,%s,%sn",
outcome.Port,
escapeCSV(outcome.Service),
escapeCSV(outcome.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.Comprises(s, ",") || strings.Comprises(s, """) || strings.Comprises(s, "n") {
return """ + strings.ReplaceAll(s, """, """") + """
}
return s
}
Output Formatting
Our scanner can now output to a few codecs:
- Textual content: Straightforward to learn, straightforward to jot down, 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 offers 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 below are some prospects if you’re going to use our scanner for various events:
Primary scan of a single host:
$ go run essential.go -host instance.com
Scan a selected port vary:
$ go run essential.go -host instance.com -start 80 -end 443
Save outcomes to a JSON file:
$ go run essential.go -host instance.com -format json -output outcomes.json
Verbose scan with elevated timeout:
$ go run essential.go -host instance.com -verbose -timeout 2000
Scan with increased concurrency for quicker outcomes:
$ go run essential.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 operate 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 may 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 shortly 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 numerous output codecs.
Understand that instruments like a scanner ought to solely be utilized in moral and authorized parameters, with correct authorization to scan the goal techniques. When carried out responsibly, common vulnerability scanning is a crucial side of fine safety posture that may assist defend your techniques from threats.
You’ll find the whole supply code for this undertaking on GitHub