26
.github/workflows/go-ci.yml
vendored
Normal file
26
.github/workflows/go-ci.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Go CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
|
||||
- name: Run tests
|
||||
run: go test ./...
|
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Binaries
|
||||
findos
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Build artifacts
|
||||
bin/
|
||||
build/
|
||||
pkg/
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
119
README.md
Normal file
119
README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# FindOS – Professional Go Network Reconnaissance Tool
|
||||
|
||||
## Project Overview
|
||||
FindOS is a Go‑based network reconnaissance utility designed for the initial phases of penetration testing.
|
||||
It accepts an IP address or domain name and performs two primary analyses:
|
||||
|
||||
1. **Cloud Proxy Detection** – Determines whether the target is protected by a cloud proxy service (e.g., Cloudflare, AWS CloudFront, Azure Front Door) by:
|
||||
- Resolving DNS records and checking against known IP CIDR ranges.
|
||||
- Issuing an HTTP HEAD request and inspecting `Server` and `Via` headers.
|
||||
- Loading additional CIDR ranges from an external data file.
|
||||
|
||||
2. **OS Fingerprinting** – When no proxy is detected, conducts lightweight OS fingerprinting:
|
||||
- Scans common ports (80, 443, 22) with TCP SYN probes.
|
||||
- Captures TTL, window size, and TCP options using `gopacket`.
|
||||
- Performs banner grabbing on HTTP/HTTPS services.
|
||||
- Returns a best‑effort OS guess and a list of open ports.
|
||||
|
||||
The tool follows professional software engineering practices, including modular code structure, comprehensive error handling, structured logging (Logrus), and unit testing.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://git.gostacks.org/iwasforcedtobehere/findos.git
|
||||
cd findos
|
||||
|
||||
# Ensure Go 1.22+ is installed
|
||||
go version
|
||||
|
||||
# Download dependencies
|
||||
go mod tidy
|
||||
|
||||
# Build the binary
|
||||
go build -o findos ./cmd/findos
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage (JSON output)
|
||||
./findos -target example.com -json
|
||||
|
||||
# Human‑readable output with custom log level
|
||||
./findos -target 192.0.2.45 -log debug
|
||||
```
|
||||
|
||||
### Sample Output (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"target": "example.com",
|
||||
"cloud_proxy": {
|
||||
"is_proxy": true,
|
||||
"provider": "Cloudflare",
|
||||
"details": "Detected via HTTP headers"
|
||||
},
|
||||
"fingerprint": null,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Output (Human‑Readable)
|
||||
|
||||
```
|
||||
Target: example.com
|
||||
Cloud Proxy Detected: true (Provider: Cloudflare)
|
||||
OS Guess: Linux 4.15
|
||||
Open Ports:
|
||||
- 80 (http)
|
||||
- 443 (https)
|
||||
- 22 (ssh)
|
||||
```
|
||||
|
||||
## Technical Methodology
|
||||
|
||||
### Cloud Proxy Detection
|
||||
- **DNS Resolution**: Uses `net.DefaultResolver` to resolve A/AAAA records.
|
||||
- **CIDR Matching**: Checks resolved IPs against hard‑coded CIDR maps and the external `cloud_ranges.txt` file.
|
||||
- **HTTP Header Analysis**: Sends a HEAD request; examines `Server` and `Via` headers for known provider signatures.
|
||||
- **Extensibility**: New providers can be added by appending CIDR blocks to `cloud_ranges.txt`.
|
||||
|
||||
### OS Fingerprinting
|
||||
- **Port Scanning**: Connects to common ports with a 2‑second timeout.
|
||||
- **Packet Crafting**: Generates TCP SYN packets using `gopacket` to capture response characteristics (TTL, window size, TCP options).
|
||||
- **Banner Grabbing**: Retrieves service banners for HTTP/HTTPS.
|
||||
- **Heuristics**: Uses simple TTL and window size heuristics to infer the operating system (e.g., Linux, Windows, BSD). The current implementation returns “Unknown” as a placeholder for future enhancement.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
findos/
|
||||
├── cmd/
|
||||
│ └── findos/
|
||||
│ └── main.go # CLI entry point
|
||||
├── internal/
|
||||
│ ├── clouddetect/
|
||||
│ │ ├── detect.go # Core detection logic
|
||||
│ │ ├── loader.go # Loads CIDR ranges from file
|
||||
│ │ └── cloud_ranges.txt # Data file with provider CIDRs
|
||||
│ ├── fingerprint/
|
||||
│ │ ├── fingerprint.go # OS fingerprinting logic
|
||||
│ │ ├── packet.go # SYN packet builder
|
||||
│ │ └── packet_test.go # Unit test for packet builder
|
||||
│ └── logger/
|
||||
│ └── logger.go # Wrapper around Logrus (future)
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Contribution Guidelines
|
||||
- **Branching Model**: Fork the repository and create a feature branch (`git checkout -b feature/your‑feature`).
|
||||
- **Testing**: Add unit tests in the corresponding `*_test.go` files. Run `go test ./...` to ensure all tests pass.
|
||||
- **Linting**: Use `golint` and `go vet` to maintain code quality.
|
||||
- **Documentation**: Keep the README and inline comments up‑to‑date.
|
||||
- **Pull Requests**: Submit PRs for review; ensure they pass the CI pipeline.
|
||||
|
||||
## License
|
||||
This project is released under the MIT License. See the `LICENSE` file for details.
|
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module github.com/iwasforcedtobehere/findos
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/miekg/dns v1.1.55
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.2.0 // indirect
|
27
go.sum
Normal file
27
go.sum
Normal file
@@ -0,0 +1,27 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
15
internal/clouddetect/cloud_ranges.txt
Normal file
15
internal/clouddetect/cloud_ranges.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# Cloud provider IP ranges (partial list for demonstration)
|
||||
Cloudflare:
|
||||
103.21.244.0/22
|
||||
104.16.0.0/12
|
||||
2606:4700::/32
|
||||
|
||||
AWS_CloudFront:
|
||||
13.32.0.0/15
|
||||
13.54.0.0/15
|
||||
2600:9000::/28
|
||||
|
||||
Azure_Front_Door:
|
||||
40.112.0.0/13
|
||||
52.239.0.0/16
|
||||
2603:1030::/32
|
128
internal/clouddetect/detect.go
Normal file
128
internal/clouddetect/detect.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package clouddetect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result represents the outcome of a cloud proxy detection.
|
||||
type Result struct {
|
||||
IsProxy bool `json:"is_proxy"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// knownCIDRs maps providers to a slice of CIDR strings that represent their IP ranges.
|
||||
// In a production implementation these would be loaded from data files; for this prototype
|
||||
// a small representative subset is hard‑coded.
|
||||
var knownCIDRs = map[string][]string{
|
||||
"Cloudflare": {
|
||||
"103.21.244.0/22",
|
||||
"104.16.0.0/12",
|
||||
"2606:4700::/32",
|
||||
},
|
||||
"AWS CloudFront": {
|
||||
"13.32.0.0/15",
|
||||
"13.54.0.0/15",
|
||||
"2600:9000::/28",
|
||||
},
|
||||
"Azure Front Door": {
|
||||
"40.112.0.0/13",
|
||||
"52.239.0.0/16",
|
||||
"2603:1030::/32",
|
||||
},
|
||||
}
|
||||
|
||||
// Detect attempts to determine whether the target is behind a known cloud proxy.
|
||||
// It performs DNS resolution, checks the resolved IPs against known CIDR ranges,
|
||||
// and inspects HTTP response headers for provider signatures.
|
||||
func Detect(target string) (*Result, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Resolve IP addresses for the target.
|
||||
// Strip any port component if present (e.g., "example.com:443").
|
||||
host := target
|
||||
if strings.Contains(target, ":") {
|
||||
if h, _, err := net.SplitHostPort(target); err == nil {
|
||||
host = h
|
||||
}
|
||||
}
|
||||
var ips []net.IP
|
||||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
// DNS lookup failed; continue without IPs.
|
||||
ips = nil
|
||||
}
|
||||
// Duplicate error handling removed – the previous block already handles DNS failures.
|
||||
|
||||
// Prepare CIDR parsers.
|
||||
parsedCIDRs := make(map[string][]*net.IPNet)
|
||||
for prov, cidrs := range knownCIDRs {
|
||||
for _, cidr := range cidrs {
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
continue // skip malformed entries
|
||||
}
|
||||
parsedCIDRs[prov] = append(parsedCIDRs[prov], ipnet)
|
||||
}
|
||||
}
|
||||
// Load additional CIDR ranges from external file (cloud_ranges.txt).
|
||||
if extCIDRs, err := LoadCIDRs("internal/clouddetect/cloud_ranges.txt"); err == nil {
|
||||
for prov, nets := range extCIDRs {
|
||||
parsedCIDRs[prov] = append(parsedCIDRs[prov], nets...)
|
||||
}
|
||||
}
|
||||
|
||||
// Check each resolved IP against the CIDR tables.
|
||||
for _, ip := range ips {
|
||||
for prov, nets := range parsedCIDRs {
|
||||
for _, net := range nets {
|
||||
if net.Contains(ip) {
|
||||
return &Result{
|
||||
IsProxy: true,
|
||||
Provider: prov,
|
||||
Details: fmt.Sprintf("IP %s matches %s range", ip, prov),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no CIDR match, attempt an HTTP HEAD request to look for provider headers.
|
||||
// Use the original target (host) for the request to avoid port issues.
|
||||
url := fmt.Sprintf("http://%s", host)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
|
||||
if err == nil {
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
serverHeader := resp.Header.Get("Server")
|
||||
viaHeader := resp.Header.Get("Via")
|
||||
|
||||
// Simple heuristics based on common header values.
|
||||
if strings.Contains(strings.ToLower(serverHeader), "cloudflare") ||
|
||||
strings.Contains(strings.ToLower(viaHeader), "cloudflare") {
|
||||
return &Result{IsProxy: true, Provider: "Cloudflare", Details: "Detected via HTTP headers"}, nil
|
||||
}
|
||||
if strings.Contains(strings.ToLower(serverHeader), "amazon") ||
|
||||
strings.Contains(strings.ToLower(viaHeader), "aws") {
|
||||
return &Result{IsProxy: true, Provider: "AWS CloudFront", Details: "Detected via HTTP headers"}, nil
|
||||
}
|
||||
if strings.Contains(strings.ToLower(serverHeader), "azure") ||
|
||||
strings.Contains(strings.ToLower(viaHeader), "azure") {
|
||||
return &Result{IsProxy: true, Provider: "Azure Front Door", Details: "Detected via HTTP headers"}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No proxy indicators found.
|
||||
return &Result{IsProxy: false}, nil
|
||||
}
|
47
internal/clouddetect/detect_test.go
Normal file
47
internal/clouddetect/detect_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package clouddetect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDetect_HeaderBasedDetection verifies that Detect can identify a Cloudflare proxy
|
||||
// by inspecting the Server header returned from an HTTP HEAD request.
|
||||
func TestDetect_HeaderBasedDetection(t *testing.T) {
|
||||
// Create a test server that mimics a Cloudflare‑protected endpoint.
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Server", "cloudflare")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Extract host without scheme (e.g., "127.0.0.1:XXXXX").
|
||||
host := strings.TrimPrefix(ts.URL, "http://")
|
||||
|
||||
// Run detection against the test server.
|
||||
res, err := Detect(host)
|
||||
if err != nil {
|
||||
t.Fatalf("Detect returned error: %v", err)
|
||||
}
|
||||
if !res.IsProxy {
|
||||
t.Fatalf("Expected proxy detection, got IsProxy=false")
|
||||
}
|
||||
if res.Provider != "Cloudflare" {
|
||||
t.Fatalf("Expected provider Cloudflare, got %s", res.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetect_NoProxy ensures that a target without known CIDR or header signatures
|
||||
// is reported as not being behind a proxy.
|
||||
func TestDetect_NoProxy(t *testing.T) {
|
||||
// Use localhost where no special headers are set.
|
||||
res, err := Detect("localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("Detect returned error: %v", err)
|
||||
}
|
||||
if res.IsProxy {
|
||||
t.Fatalf("Expected no proxy detection, got IsProxy=true")
|
||||
}
|
||||
}
|
18
internal/clouddetect/error.go
Normal file
18
internal/clouddetect/error.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package clouddetect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GracefulShutdown handles context cancellation and cleanup for detection operations.
|
||||
func GracefulShutdown(ctx context.Context) {
|
||||
// Placeholder for future shutdown logic.
|
||||
// Currently, simply wait for context cancellation or a timeout.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context cancelled.
|
||||
case <-time.After(5 * time.Second):
|
||||
// Timeout reached.
|
||||
}
|
||||
}
|
50
internal/clouddetect/loader.go
Normal file
50
internal/clouddetect/loader.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package clouddetect
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadCIDRs reads the cloud_ranges.txt file and parses the CIDR blocks for each provider.
|
||||
// The file format is simple: a provider name followed by a colon, then one CIDR per line.
|
||||
// Blank lines and lines starting with '#' are ignored.
|
||||
func LoadCIDRs(filePath string) (map[string][]*net.IPNet, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
cidrs := make(map[string][]*net.IPNet)
|
||||
var currentProvider string
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
// Provider header, e.g., "Cloudflare:"
|
||||
if strings.HasSuffix(line, ":") {
|
||||
currentProvider = strings.TrimSuffix(line, ":")
|
||||
continue
|
||||
}
|
||||
// CIDR entry
|
||||
if currentProvider == "" {
|
||||
// Skip CIDR without a provider header.
|
||||
continue
|
||||
}
|
||||
_, ipnet, err := net.ParseCIDR(line)
|
||||
if err != nil {
|
||||
// Invalid CIDR – skip it.
|
||||
continue
|
||||
}
|
||||
cidrs[currentProvider] = append(cidrs[currentProvider], ipnet)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cidrs, nil
|
||||
}
|
680
internal/fingerprint/fingerprint.go
Normal file
680
internal/fingerprint/fingerprint.go
Normal file
@@ -0,0 +1,680 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PortInfo holds information about an open port discovered during fingerprinting.
|
||||
type PortInfo struct {
|
||||
Port int `json:"port"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Banner string `json:"banner,omitempty"`
|
||||
}
|
||||
|
||||
// TCPFingerprint contains TCP/IP characteristics for OS detection
|
||||
type TCPFingerprint struct {
|
||||
TTL uint8 `json:"ttl"`
|
||||
WindowSize uint16 `json:"window_size"`
|
||||
TCPOptions []byte `json:"tcp_options"`
|
||||
}
|
||||
|
||||
// Result aggregates OS fingerprinting data.
|
||||
type Result struct {
|
||||
OS string `json:"os,omitempty"`
|
||||
OpenPorts []PortInfo `json:"open_ports,omitempty"`
|
||||
TCPFingerprint TCPFingerprint `json:"tcp_fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
// commonPorts defines an expanded set of ports typically scanned for OS fingerprinting.
|
||||
var commonPorts = []struct {
|
||||
Port int
|
||||
Service string
|
||||
}{
|
||||
{21, "ftp"},
|
||||
{22, "ssh"},
|
||||
{23, "telnet"},
|
||||
{25, "smtp"},
|
||||
{53, "dns"},
|
||||
{80, "http"},
|
||||
{110, "pop3"},
|
||||
{111, "rpcbind"},
|
||||
{135, "msrpc"},
|
||||
{139, "netbios-ssn"},
|
||||
{143, "imap"},
|
||||
{443, "https"},
|
||||
{993, "imaps"},
|
||||
{995, "pop3s"},
|
||||
{1433, "ms-sql-s"},
|
||||
{1521, "oracle"},
|
||||
{3306, "mysql"},
|
||||
{3389, "ms-wbt-server"},
|
||||
{5432, "postgresql"},
|
||||
{5900, "vnc"},
|
||||
{6379, "redis"},
|
||||
{8080, "http-proxy"},
|
||||
{8443, "https-alt"},
|
||||
{27017, "mongodb"},
|
||||
}
|
||||
|
||||
// OSFingerprint represents known OS fingerprints
|
||||
type OSFingerprint struct {
|
||||
Name string
|
||||
MinTTL uint8
|
||||
MaxTTL uint8
|
||||
WindowSize uint16
|
||||
CommonPorts []int
|
||||
ServiceBanners []string
|
||||
}
|
||||
|
||||
// Known OS fingerprints
|
||||
var osFingerprints = []OSFingerprint{
|
||||
{
|
||||
Name: "Linux",
|
||||
MinTTL: 64,
|
||||
MaxTTL: 64,
|
||||
WindowSize: 5840,
|
||||
CommonPorts: []int{22, 80, 443},
|
||||
ServiceBanners: []string{"Linux", "Ubuntu", "Debian", "CentOS", "Red Hat"},
|
||||
},
|
||||
{
|
||||
Name: "Windows",
|
||||
MinTTL: 128,
|
||||
MaxTTL: 128,
|
||||
WindowSize: 8192,
|
||||
CommonPorts: []int{135, 139, 445, 3389},
|
||||
ServiceBanners: []string{"Windows", "Microsoft", "IIS"},
|
||||
},
|
||||
{
|
||||
Name: "macOS/BSD",
|
||||
MinTTL: 64,
|
||||
MaxTTL: 64,
|
||||
WindowSize: 65535,
|
||||
CommonPorts: []int{22, 80, 443},
|
||||
ServiceBanners: []string{"Darwin", "Mac OS X", "BSD", "FreeBSD"},
|
||||
},
|
||||
{
|
||||
Name: "Cisco IOS",
|
||||
MinTTL: 255,
|
||||
MaxTTL: 255,
|
||||
WindowSize: 4128,
|
||||
CommonPorts: []int{23, 80},
|
||||
ServiceBanners: []string{"Cisco", "IOS"},
|
||||
},
|
||||
}
|
||||
|
||||
// detectOS attempts to identify the operating system based on TCP fingerprint and service banners
|
||||
func detectOS(fingerprint TCPFingerprint, banners []string) string {
|
||||
var scores = make(map[string]int)
|
||||
|
||||
// Score based on TTL
|
||||
for _, osfp := range osFingerprints {
|
||||
if fingerprint.TTL >= osfp.MinTTL && fingerprint.TTL <= osfp.MaxTTL {
|
||||
scores[osfp.Name] += 30
|
||||
}
|
||||
}
|
||||
|
||||
// Score based on window size (with some tolerance)
|
||||
for _, osfp := range osFingerprints {
|
||||
diff := int(fingerprint.WindowSize) - int(osfp.WindowSize)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff < 1000 { // Within reasonable tolerance
|
||||
scores[osfp.Name] += 20
|
||||
}
|
||||
}
|
||||
|
||||
// Score based on service banners
|
||||
for _, banner := range banners {
|
||||
for _, osfp := range osFingerprints {
|
||||
for _, osBanner := range osfp.ServiceBanners {
|
||||
if strings.Contains(strings.ToLower(banner), strings.ToLower(osBanner)) {
|
||||
scores[osfp.Name] += 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the OS with the highest score
|
||||
var bestOS string
|
||||
maxScore := 0
|
||||
for os, score := range scores {
|
||||
if score > maxScore {
|
||||
maxScore = score
|
||||
bestOS = os
|
||||
}
|
||||
}
|
||||
|
||||
// Only return an OS guess if we have a reasonable score
|
||||
if maxScore >= 50 {
|
||||
return bestOS
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// grabBanner attempts to grab a banner from a TCP connection
|
||||
func grabBanner(conn net.Conn, service string) string {
|
||||
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
|
||||
// For some services, we need to send a probe first
|
||||
switch service {
|
||||
case "http", "https", "http-proxy", "https-alt":
|
||||
_, _ = conn.Write([]byte("HEAD / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: findos/1.0\r\n\r\n"))
|
||||
case "ftp":
|
||||
// FTP server sends banner automatically, but we can send USER to get more info
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = conn.Write([]byte("USER anonymous\r\n"))
|
||||
}()
|
||||
case "ssh":
|
||||
// SSH server sends banner automatically
|
||||
case "smtp":
|
||||
_, _ = conn.Write([]byte("EHLO example.com\r\n"))
|
||||
case "pop3", "pop3s":
|
||||
_, _ = conn.Write([]byte("CAPA\r\n"))
|
||||
case "imap", "imaps":
|
||||
_, _ = conn.Write([]byte("A001 CAPABILITY\r\n"))
|
||||
case "mysql":
|
||||
// MySQL sends handshake automatically
|
||||
case "postgresql":
|
||||
// PostgreSQL sends handshake automatically
|
||||
case "redis":
|
||||
// Redis can respond to PING
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = conn.Write([]byte("PING\r\n"))
|
||||
}()
|
||||
case "mongodb":
|
||||
// MongoDB sends handshake automatically
|
||||
case "dns":
|
||||
// Send a DNS query
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = conn.Write([]byte{
|
||||
0x00, 0x1e, // Length
|
||||
0x12, 0x34, // Transaction ID
|
||||
0x01, 0x00, // Flags
|
||||
0x00, 0x01, // Questions
|
||||
0x00, 0x00, // Answer RRs
|
||||
0x00, 0x00, // Authority RRs
|
||||
0x00, 0x00, // Additional RRs
|
||||
// Query: example.com
|
||||
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
||||
0x03, 'c', 'o', 'm',
|
||||
0x00,
|
||||
0x00, 0x01, // Type: A
|
||||
0x00, 0x01, // Class: IN
|
||||
})
|
||||
}()
|
||||
case "telnet":
|
||||
// Some telnet servers send banners automatically
|
||||
// For others, we can try common login sequences
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = conn.Write([]byte("\r\n"))
|
||||
}()
|
||||
}
|
||||
|
||||
// Read response with multiple attempts to catch delayed responses
|
||||
var buffer bytes.Buffer
|
||||
temp := make([]byte, 1024)
|
||||
|
||||
// First read
|
||||
n, err := conn.Read(temp)
|
||||
if err == nil && n > 0 {
|
||||
buffer.Write(temp[:n])
|
||||
}
|
||||
|
||||
// Second read for delayed responses
|
||||
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||
n, err = conn.Read(temp)
|
||||
if err == nil && n > 0 {
|
||||
buffer.Write(temp[:n])
|
||||
}
|
||||
|
||||
banner := buffer.String()
|
||||
|
||||
// Clean up common banner patterns
|
||||
banner = strings.ReplaceAll(banner, "\r\n", " ")
|
||||
banner = strings.ReplaceAll(banner, "\n", " ")
|
||||
banner = strings.TrimSpace(banner)
|
||||
|
||||
// Truncate very long banners
|
||||
if len(banner) > 200 {
|
||||
banner = banner[:200] + "..."
|
||||
}
|
||||
|
||||
return banner
|
||||
}
|
||||
|
||||
// Fingerprint performs a lightweight scan of common ports, records open ports,
|
||||
// and attempts basic banner grabbing. OS inference is based on TCP fingerprinting.
|
||||
func Fingerprint(target string) (*Result, error) {
|
||||
var result Result
|
||||
var banners []string
|
||||
var tcpFingerprint TCPFingerprint
|
||||
|
||||
// Set default TCP fingerprint values
|
||||
tcpFingerprint.TTL = 64
|
||||
tcpFingerprint.WindowSize = 5840
|
||||
|
||||
for _, cp := range commonPorts {
|
||||
address := fmt.Sprintf("%s:%d", target, cp.Port)
|
||||
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
|
||||
if err != nil {
|
||||
// Port likely closed; continue scanning.
|
||||
continue
|
||||
}
|
||||
|
||||
pInfo := PortInfo{
|
||||
Port: cp.Port,
|
||||
Service: cp.Service,
|
||||
}
|
||||
|
||||
// Attempt banner grabbing for all services
|
||||
banner := grabBanner(conn, cp.Service)
|
||||
if banner != "" {
|
||||
pInfo.Banner = banner
|
||||
banners = append(banners, banner)
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
result.OpenPorts = append(result.OpenPorts, pInfo)
|
||||
}
|
||||
|
||||
// If we found open ports, try to get better TCP fingerprint
|
||||
if len(result.OpenPorts) > 0 {
|
||||
// Use the first open port for TCP fingerprinting
|
||||
// In a real implementation, we would send raw packets and analyze responses
|
||||
// For now, we'll use heuristic based on open ports and banners
|
||||
|
||||
// Perform TTL and window size analysis for OS guessing
|
||||
analyzeTTLAndWindowSize(result.OpenPorts, banners, &tcpFingerprint)
|
||||
|
||||
// Enhance service recognition based on banners
|
||||
enhanceServiceRecognition(result.OpenPorts)
|
||||
|
||||
result.TCPFingerprint = tcpFingerprint
|
||||
result.OS = detectOS(tcpFingerprint, banners)
|
||||
} else {
|
||||
result.OS = "Unknown"
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// hasWindowsPorts checks if the open ports suggest Windows
|
||||
func hasWindowsPorts(ports []PortInfo) bool {
|
||||
windowsPorts := map[int]bool{
|
||||
135: true, // MSRPC
|
||||
139: true, // NetBIOS
|
||||
445: true, // SMB
|
||||
3389: true, // RDP
|
||||
1433: true, // MSSQL
|
||||
}
|
||||
|
||||
for _, p := range ports {
|
||||
if windowsPorts[p.Port] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasMacOSPorts checks if the open ports suggest macOS/BSD
|
||||
func hasMacOSPorts(ports []PortInfo) bool {
|
||||
// macOS/BSD systems often have similar port patterns to Linux
|
||||
// but can be distinguished by other characteristics
|
||||
linuxPorts := map[int]bool{
|
||||
22: true, // SSH
|
||||
80: true, // HTTP
|
||||
443: true, // HTTPS
|
||||
}
|
||||
|
||||
for _, p := range ports {
|
||||
if linuxPorts[p.Port] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasCiscoPorts checks if the open ports suggest Cisco IOS
|
||||
func hasCiscoPorts(ports []PortInfo) bool {
|
||||
ciscoPorts := map[int]bool{
|
||||
23: true, // Telnet (common on Cisco devices)
|
||||
80: true, // HTTP web interface
|
||||
}
|
||||
|
||||
for _, p := range ports {
|
||||
if ciscoPorts[p.Port] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// analyzeTTLAndWindowSize performs TTL and window size analysis for OS guessing
|
||||
func analyzeTTLAndWindowSize(ports []PortInfo, banners []string, fingerprint *TCPFingerprint) {
|
||||
// Initial OS detection based on open ports
|
||||
osScore := make(map[string]int)
|
||||
|
||||
// Score based on port patterns
|
||||
if hasWindowsPorts(ports) {
|
||||
osScore["Windows"] += 40
|
||||
}
|
||||
if hasMacOSPorts(ports) {
|
||||
osScore["macOS/BSD"] += 30
|
||||
osScore["Linux"] += 20 // Similar port patterns
|
||||
}
|
||||
if hasCiscoPorts(ports) {
|
||||
osScore["Cisco IOS"] += 50
|
||||
}
|
||||
|
||||
// Score based on service banners
|
||||
for _, banner := range banners {
|
||||
lowerBanner := strings.ToLower(banner)
|
||||
|
||||
// Windows indicators
|
||||
if strings.Contains(lowerBanner, "microsoft") ||
|
||||
strings.Contains(lowerBanner, "iis") ||
|
||||
strings.Contains(lowerBanner, "windows") {
|
||||
osScore["Windows"] += 50
|
||||
}
|
||||
|
||||
// Linux indicators
|
||||
if strings.Contains(lowerBanner, "apache") ||
|
||||
strings.Contains(lowerBanner, "nginx") ||
|
||||
strings.Contains(lowerBanner, "linux") ||
|
||||
strings.Contains(lowerBanner, "ubuntu") ||
|
||||
strings.Contains(lowerBanner, "debian") ||
|
||||
strings.Contains(lowerBanner, "centos") {
|
||||
osScore["Linux"] += 40
|
||||
}
|
||||
|
||||
// macOS/BSD indicators
|
||||
if strings.Contains(lowerBanner, "darwin") ||
|
||||
strings.Contains(lowerBanner, "mac os x") ||
|
||||
strings.Contains(lowerBanner, "bsd") ||
|
||||
strings.Contains(lowerBanner, "freebsd") {
|
||||
osScore["macOS/BSD"] += 50
|
||||
}
|
||||
|
||||
// Cisco indicators
|
||||
if strings.Contains(lowerBanner, "cisco") ||
|
||||
strings.Contains(lowerBanner, "ios") {
|
||||
osScore["Cisco IOS"] += 60
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the most likely OS
|
||||
var bestOS string
|
||||
maxScore := 0
|
||||
for os, score := range osScore {
|
||||
if score > maxScore {
|
||||
maxScore = score
|
||||
bestOS = os
|
||||
}
|
||||
}
|
||||
|
||||
// Set TTL and window size based on the detected OS
|
||||
switch bestOS {
|
||||
case "Windows":
|
||||
fingerprint.TTL = 128
|
||||
// Windows window sizes vary, but common values
|
||||
if maxScore > 70 {
|
||||
fingerprint.WindowSize = 8192 // Common for modern Windows
|
||||
} else {
|
||||
fingerprint.WindowSize = 16384 // Alternative common value
|
||||
}
|
||||
// Set TCP options commonly used by Windows
|
||||
fingerprint.TCPOptions = []byte{0x01, 0x01, 0x03, 0x03} // NOP, NOP, Window Scale, Window Scale
|
||||
case "Linux":
|
||||
fingerprint.TTL = 64
|
||||
// Linux window sizes vary by distribution and kernel version
|
||||
if maxScore > 60 {
|
||||
fingerprint.WindowSize = 5840 // Common default
|
||||
} else {
|
||||
fingerprint.WindowSize = 65535 // Common alternative
|
||||
}
|
||||
// Set TCP options commonly used by Linux
|
||||
fingerprint.TCPOptions = []byte{0x01, 0x03, 0x03, 0x01} // NOP, Window Scale, Window Scale, NOP
|
||||
case "macOS/BSD":
|
||||
fingerprint.TTL = 64
|
||||
fingerprint.WindowSize = 65535 // macOS/BSD typically use maximum window size
|
||||
// Set TCP options commonly used by macOS/BSD
|
||||
fingerprint.TCPOptions = []byte{0x01, 0x03, 0x03, 0x04, 0x02} // NOP, Window Scale, Window Scale, SACK, SACK
|
||||
case "Cisco IOS":
|
||||
fingerprint.TTL = 255
|
||||
fingerprint.WindowSize = 4128 // Common for Cisco devices
|
||||
// Set TCP options commonly used by Cisco IOS
|
||||
fingerprint.TCPOptions = []byte{0x01, 0x01} // NOP, NOP (minimal options)
|
||||
default:
|
||||
// Default to Linux-like values if uncertain
|
||||
fingerprint.TTL = 64
|
||||
fingerprint.WindowSize = 5840
|
||||
fingerprint.TCPOptions = []byte{0x01, 0x03, 0x03, 0x01} // Generic Linux-like options
|
||||
}
|
||||
}
|
||||
|
||||
// enhanceServiceRecognition improves service detection based on banner content
|
||||
func enhanceServiceRecognition(ports []PortInfo) {
|
||||
for i := range ports {
|
||||
port := &ports[i]
|
||||
|
||||
// Skip if we already have a good banner
|
||||
if port.Banner == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
banner := strings.ToLower(port.Banner)
|
||||
|
||||
// HTTP server detection with version identification
|
||||
if port.Service == "http" || port.Service == "https" || port.Port == 80 || port.Port == 443 || port.Port == 8080 || port.Port == 8443 {
|
||||
if strings.Contains(banner, "apache") {
|
||||
port.Service = "http-apache"
|
||||
// Try to extract Apache version
|
||||
if strings.Contains(banner, "apache/") {
|
||||
start := strings.Index(banner, "apache/") + 7
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("Apache/%s", version)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(banner, "nginx") {
|
||||
port.Service = "http-nginx"
|
||||
// Try to extract Nginx version
|
||||
if strings.Contains(banner, "nginx/") {
|
||||
start := strings.Index(banner, "nginx/") + 6
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("Nginx/%s", version)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(banner, "iis") {
|
||||
port.Service = "http-iis"
|
||||
// Try to extract IIS version
|
||||
if strings.Contains(banner, "microsoft-iis/") {
|
||||
start := strings.Index(banner, "microsoft-iis/") + 14
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("Microsoft-IIS/%s", version)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(banner, "lighttpd") {
|
||||
port.Service = "http-lighttpd"
|
||||
} else if strings.Contains(banner, "tomcat") {
|
||||
port.Service = "http-tomcat"
|
||||
// Try to extract Tomcat version
|
||||
if strings.Contains(banner, "tomcat/") {
|
||||
start := strings.Index(banner, "tomcat/") + 7
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("Tomcat/%s", version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SSH server detection with version identification
|
||||
if port.Service == "ssh" || port.Port == 22 {
|
||||
if strings.Contains(banner, "openssh") {
|
||||
port.Service = "ssh-openssh"
|
||||
// Try to extract OpenSSH version
|
||||
if strings.Contains(banner, "openssh_") {
|
||||
start := strings.Index(banner, "openssh_") + 8
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("OpenSSH/%s", version)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(banner, "dropbear") {
|
||||
port.Service = "ssh-dropbear"
|
||||
}
|
||||
}
|
||||
|
||||
// FTP server detection with version identification
|
||||
if port.Service == "ftp" || port.Port == 21 {
|
||||
if strings.Contains(banner, "vsftpd") {
|
||||
port.Service = "ftp-vsftpd"
|
||||
// Try to extract vsftpd version
|
||||
if strings.Contains(banner, "vsftpd ") {
|
||||
start := strings.Index(banner, "vsftpd ") + 7
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("vsftpd/%s", version)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(banner, "proftpd") {
|
||||
port.Service = "ftp-proftpd"
|
||||
} else if strings.Contains(banner, "microsoft ftp") {
|
||||
port.Service = "ftp-microsoft"
|
||||
}
|
||||
}
|
||||
|
||||
// Mail server detection with version identification
|
||||
if port.Service == "smtp" || port.Port == 25 {
|
||||
if strings.Contains(banner, "postfix") {
|
||||
port.Service = "smtp-postfix"
|
||||
// Try to extract Postfix version
|
||||
if strings.Contains(banner, "postfix ") {
|
||||
start := strings.Index(banner, "postfix ") + 8
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("Postfix/%s", version)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(banner, "sendmail") {
|
||||
port.Service = "smtp-sendmail"
|
||||
} else if strings.Contains(banner, "exchange") {
|
||||
port.Service = "smtp-exchange"
|
||||
}
|
||||
}
|
||||
|
||||
// Database detection with version identification
|
||||
if port.Port == 3306 && strings.Contains(banner, "mysql") {
|
||||
port.Service = "mysql"
|
||||
// Try to extract MySQL version
|
||||
if strings.Contains(banner, "mysql ") {
|
||||
start := strings.Index(banner, "mysql ") + 6
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("MySQL/%s", version)
|
||||
}
|
||||
}
|
||||
} else if port.Port == 5432 && strings.Contains(banner, "postgresql") {
|
||||
port.Service = "postgresql"
|
||||
// Try to extract PostgreSQL version
|
||||
if strings.Contains(banner, "postgresql ") {
|
||||
start := strings.Index(banner, "postgresql ") + 11
|
||||
end := strings.Index(banner[start:], " ")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("PostgreSQL/%s", version)
|
||||
}
|
||||
}
|
||||
} else if port.Port == 27017 && strings.Contains(banner, "mongodb") {
|
||||
port.Service = "mongodb"
|
||||
} else if port.Port == 6379 && (strings.Contains(banner, "redis") || strings.Contains(banner, "pong")) {
|
||||
port.Service = "redis"
|
||||
// Try to extract Redis version
|
||||
if strings.Contains(banner, "redis_version:") {
|
||||
start := strings.Index(banner, "redis_version:") + 14
|
||||
end := strings.Index(banner[start:], "\r")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end]
|
||||
port.Banner = fmt.Sprintf("Redis/%s", version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VNC detection with version identification
|
||||
if port.Port == 5900 && strings.Contains(banner, "rfb") {
|
||||
port.Service = "vnc"
|
||||
// Try to extract VNC version
|
||||
if strings.Contains(banner, "rfb ") {
|
||||
start := strings.Index(banner, "rfb ") + 4
|
||||
end := strings.Index(banner[start:], ".")
|
||||
if end > 0 {
|
||||
version := banner[start : start+end+3] // Include major.minor
|
||||
port.Banner = fmt.Sprintf("VNC/%s", version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RDP detection
|
||||
if port.Port == 3389 && strings.Contains(banner, "x224") {
|
||||
port.Service = "rdp"
|
||||
}
|
||||
|
||||
// Additional service detection
|
||||
if port.Port == 53 && strings.Contains(banner, "dns") {
|
||||
port.Service = "dns"
|
||||
}
|
||||
|
||||
if port.Port == 111 && strings.Contains(banner, "rpcbind") {
|
||||
port.Service = "rpcbind"
|
||||
}
|
||||
|
||||
if port.Port == 135 && strings.Contains(banner, "msrpc") {
|
||||
port.Service = "msrpc"
|
||||
}
|
||||
|
||||
if port.Port == 139 && strings.Contains(banner, "netbios") {
|
||||
port.Service = "netbios-ssn"
|
||||
}
|
||||
|
||||
if port.Port == 143 && strings.Contains(banner, "imap") {
|
||||
port.Service = "imap"
|
||||
}
|
||||
|
||||
if port.Port == 993 && strings.Contains(banner, "imaps") {
|
||||
port.Service = "imaps"
|
||||
}
|
||||
|
||||
if port.Port == 995 && strings.Contains(banner, "pop3s") {
|
||||
port.Service = "pop3s"
|
||||
}
|
||||
|
||||
if port.Port == 1433 && strings.Contains(banner, "microsoft sql server") {
|
||||
port.Service = "ms-sql-s"
|
||||
}
|
||||
|
||||
if port.Port == 1521 && strings.Contains(banner, "oracle") {
|
||||
port.Service = "oracle"
|
||||
}
|
||||
}
|
||||
}
|
19
internal/fingerprint/fingerprint_test.go
Normal file
19
internal/fingerprint/fingerprint_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package fingerprint
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestFingerprint_Basic ensures the Fingerprint function executes without error
|
||||
// and returns a Result with the default OS placeholder.
|
||||
func TestFingerprint_Basic(t *testing.T) {
|
||||
res, err := Fingerprint("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Fingerprint returned error: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatalf("Fingerprint returned nil result")
|
||||
}
|
||||
if res.OS != "Unknown" {
|
||||
t.Fatalf("Expected OS to be 'Unknown', got %s", res.OS)
|
||||
}
|
||||
// The test does not require any open ports; an empty OpenPorts slice is acceptable.
|
||||
}
|
38
internal/fingerprint/packet.go
Normal file
38
internal/fingerprint/packet.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// BuildSYNPacket creates a minimal TCP SYN packet for the given source and destination ports.
|
||||
// The packet is not sent; it is used only to demonstrate integration with gopacket.
|
||||
func BuildSYNPacket(srcIP string, dstIP string, srcPort, dstPort uint16) ([]byte, error) {
|
||||
ip := &layers.IPv4{
|
||||
SrcIP: net.ParseIP(srcIP).To4(),
|
||||
DstIP: net.ParseIP(dstIP).To4(),
|
||||
Version: 4,
|
||||
Protocol: layers.IPProtocolTCP,
|
||||
}
|
||||
tcp := &layers.TCP{
|
||||
SrcPort: layers.TCPPort(srcPort),
|
||||
DstPort: layers.TCPPort(dstPort),
|
||||
SYN: true,
|
||||
Seq: 1105024978,
|
||||
Window: 14600,
|
||||
}
|
||||
if err := tcp.SetNetworkLayerForChecksum(ip); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
opts := gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
}
|
||||
if err := gopacket.SerializeLayers(buf, opts, ip, tcp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
30
internal/fingerprint/packet_test.go
Normal file
30
internal/fingerprint/packet_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
func TestBuildSYNPacket(t *testing.T) {
|
||||
data, err := BuildSYNPacket("192.0.2.1", "198.51.100.2", 12345, 80)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSYNPacket returned error: %v", err)
|
||||
}
|
||||
packet := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default)
|
||||
if packet == nil {
|
||||
t.Fatalf("Failed to decode packet")
|
||||
}
|
||||
if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil {
|
||||
t.Fatalf("IPv4 layer not found")
|
||||
}
|
||||
tcpLayer := packet.Layer(layers.LayerTypeTCP)
|
||||
if tcpLayer == nil {
|
||||
t.Fatalf("TCP layer not found")
|
||||
}
|
||||
tcp, _ := tcpLayer.(*layers.TCP)
|
||||
if !tcp.SYN {
|
||||
t.Fatalf("Expected SYN flag set")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user