mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-07-13 21:02:44 +08:00
476 lines
13 KiB
Go
476 lines
13 KiB
Go
package Core
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/shadow1ng/fscan/Common"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ServiceInfo defines the service identification result information
|
|
type ServiceInfo struct {
|
|
Name string // Service name, such as http, ssh, etc.
|
|
Banner string // Banner information returned by the service
|
|
Version string // Service version number
|
|
Extras map[string]string // Other additional information, such as operating system, product name, etc.
|
|
}
|
|
|
|
// Result defines the result of a single detection
|
|
type Result struct {
|
|
Service Service // Identified service information
|
|
Banner string // Service banner
|
|
Extras map[string]string // Additional information
|
|
Send []byte // Probe data sent
|
|
Recv []byte // Response data received
|
|
}
|
|
|
|
// Service defines the basic information of a service
|
|
type Service struct {
|
|
Name string // Service name
|
|
Extras map[string]string // Additional service attributes
|
|
}
|
|
|
|
// Info defines the context information for a single port probe
|
|
type Info struct {
|
|
Address string // Target IP address
|
|
Port int // Target port
|
|
Conn net.Conn // Network connection
|
|
Result Result // Detection result
|
|
Found bool // Whether the service was successfully identified
|
|
}
|
|
|
|
// PortInfoScanner defines a port service identifier
|
|
type PortInfoScanner struct {
|
|
Address string // Target IP address
|
|
Port int // Target port
|
|
Conn net.Conn // Network connection
|
|
Timeout time.Duration // Timeout duration
|
|
info *Info // Detection context
|
|
}
|
|
|
|
// Predefined basic probes
|
|
var (
|
|
null = new(Probe) // Empty probe, used for basic protocol identification
|
|
common = new(Probe) // Common probe, used for common service identification
|
|
)
|
|
|
|
// NewPortInfoScanner creates a new port service identifier instance
|
|
func NewPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration) *PortInfoScanner {
|
|
return &PortInfoScanner{
|
|
Address: addr,
|
|
Port: port,
|
|
Conn: conn,
|
|
Timeout: timeout,
|
|
info: &Info{
|
|
Address: addr,
|
|
Port: port,
|
|
Conn: conn,
|
|
Result: Result{
|
|
Service: Service{},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Identify performs service identification and returns the result
|
|
func (s *PortInfoScanner) Identify() (*ServiceInfo, error) {
|
|
Common.LogDebug(fmt.Sprintf("Starting to identify service %s:%d", s.Address, s.Port))
|
|
s.info.PortInfo()
|
|
|
|
// Construct the return result
|
|
serviceInfo := &ServiceInfo{
|
|
Name: s.info.Result.Service.Name,
|
|
Banner: s.info.Result.Banner,
|
|
Version: s.info.Result.Service.Extras["version"],
|
|
Extras: make(map[string]string),
|
|
}
|
|
|
|
// Copy additional information
|
|
for k, v := range s.info.Result.Service.Extras {
|
|
serviceInfo.Extras[k] = v
|
|
}
|
|
|
|
Common.LogDebug(fmt.Sprintf("Service identification completed %s:%d => %s", s.Address, s.Port, serviceInfo.Name))
|
|
return serviceInfo, nil
|
|
}
|
|
|
|
// PortInfo executes the main logic of port service identification
|
|
func (i *Info) PortInfo() {
|
|
// 1. First try to read the initial response from the service
|
|
if response, err := i.Read(); err == nil && len(response) > 0 {
|
|
Common.LogDebug(fmt.Sprintf("Received initial response: %d bytes", len(response)))
|
|
|
|
// Check the response using basic probes
|
|
Common.LogDebug("Attempting to check response using basic probes (null/common)")
|
|
if i.tryProbes(response, []*Probe{null, common}) {
|
|
Common.LogDebug("Basic probe matching successful")
|
|
return
|
|
}
|
|
Common.LogDebug("Basic probes did not match")
|
|
} else if err != nil {
|
|
Common.LogDebug(fmt.Sprintf("Failed to read initial response: %v", err))
|
|
}
|
|
|
|
// Record used probes to avoid duplication
|
|
usedProbes := make(map[string]struct{})
|
|
|
|
// 2. Try to use port-specific probes
|
|
Common.LogDebug(fmt.Sprintf("Attempting to use dedicated probes for port %d", i.Port))
|
|
if i.processPortMapProbes(usedProbes) {
|
|
Common.LogDebug("Port-specific probe matching successful")
|
|
return
|
|
}
|
|
Common.LogDebug("Port-specific probes did not match")
|
|
|
|
// 3. Use the default probe list
|
|
Common.LogDebug("Attempting to use default probe list")
|
|
if i.processDefaultProbes(usedProbes) {
|
|
Common.LogDebug("Default probe matching successful")
|
|
return
|
|
}
|
|
Common.LogDebug("Default probes did not match")
|
|
|
|
// 4. If all probes fail, mark as unknown service
|
|
if strings.TrimSpace(i.Result.Service.Name) == "" {
|
|
Common.LogDebug("Service not recognized, marking as unknown")
|
|
i.Result.Service.Name = "unknown"
|
|
}
|
|
}
|
|
|
|
// tryProbes attempts to use the specified probe list to check the response
|
|
func (i *Info) tryProbes(response []byte, probes []*Probe) bool {
|
|
for _, probe := range probes {
|
|
Common.LogDebug(fmt.Sprintf("Attempting probe: %s", probe.Name))
|
|
i.GetInfo(response, probe)
|
|
if i.Found {
|
|
Common.LogDebug(fmt.Sprintf("Probe %s matched successfully", probe.Name))
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// processPortMapProbes processes dedicated probes in the port mapping
|
|
func (i *Info) processPortMapProbes(usedProbes map[string]struct{}) bool {
|
|
// Check if port-specific probes exist
|
|
if len(Common.PortMap[i.Port]) == 0 {
|
|
Common.LogDebug(fmt.Sprintf("Port %d has no dedicated probes", i.Port))
|
|
return false
|
|
}
|
|
|
|
// Iterate through port-specific probes
|
|
for _, name := range Common.PortMap[i.Port] {
|
|
Common.LogDebug(fmt.Sprintf("Attempting port-specific probe: %s", name))
|
|
usedProbes[name] = struct{}{}
|
|
probe := v.ProbesMapKName[name]
|
|
|
|
// Decode probe data
|
|
probeData, err := DecodeData(probe.Data)
|
|
if err != nil || len(probeData) == 0 {
|
|
Common.LogDebug(fmt.Sprintf("Failed to decode probe data for %s", name))
|
|
continue
|
|
}
|
|
|
|
// Send probe data and get response
|
|
Common.LogDebug(fmt.Sprintf("Sending probe data: %d bytes", len(probeData)))
|
|
if response := i.Connect(probeData); len(response) > 0 {
|
|
Common.LogDebug(fmt.Sprintf("Received response: %d bytes", len(response)))
|
|
|
|
// Check response with current probe
|
|
i.GetInfo(response, &probe)
|
|
if i.Found {
|
|
return true
|
|
}
|
|
|
|
// Perform additional checks based on probe type
|
|
switch name {
|
|
case "GenericLines":
|
|
if i.tryProbes(response, []*Probe{null}) {
|
|
return true
|
|
}
|
|
case "NULL":
|
|
continue
|
|
default:
|
|
if i.tryProbes(response, []*Probe{common}) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// processDefaultProbes processes the default probe list
|
|
func (i *Info) processDefaultProbes(usedProbes map[string]struct{}) bool {
|
|
failCount := 0
|
|
const maxFailures = 10 // Maximum failure count
|
|
|
|
// Iterate through the default probe list
|
|
for _, name := range Common.DefaultMap {
|
|
// Skip already used probes
|
|
if _, used := usedProbes[name]; used {
|
|
continue
|
|
}
|
|
|
|
probe := v.ProbesMapKName[name]
|
|
probeData, err := DecodeData(probe.Data)
|
|
if err != nil || len(probeData) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Send probe data and get response
|
|
response := i.Connect(probeData)
|
|
if len(response) == 0 {
|
|
failCount++
|
|
if failCount > maxFailures {
|
|
return false
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check response with current probe
|
|
i.GetInfo(response, &probe)
|
|
if i.Found {
|
|
return true
|
|
}
|
|
|
|
// Perform additional checks based on probe type
|
|
switch name {
|
|
case "GenericLines":
|
|
if i.tryProbes(response, []*Probe{null}) {
|
|
return true
|
|
}
|
|
case "NULL":
|
|
continue
|
|
default:
|
|
if i.tryProbes(response, []*Probe{common}) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Try to use other probes in the port mapping
|
|
if len(Common.PortMap[i.Port]) > 0 {
|
|
for _, mappedName := range Common.PortMap[i.Port] {
|
|
usedProbes[mappedName] = struct{}{}
|
|
mappedProbe := v.ProbesMapKName[mappedName]
|
|
i.GetInfo(response, &mappedProbe)
|
|
if i.Found {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetInfo analyzes response data and extracts service information
|
|
func (i *Info) GetInfo(response []byte, probe *Probe) {
|
|
Common.LogDebug(fmt.Sprintf("Starting to analyze response data, length: %d", len(response)))
|
|
|
|
// Check response data validity
|
|
if len(response) <= 0 {
|
|
Common.LogDebug("Response data is empty")
|
|
return
|
|
}
|
|
|
|
result := &i.Result
|
|
var (
|
|
softMatch Match
|
|
softFound bool
|
|
)
|
|
|
|
// Process main matching rules
|
|
Common.LogDebug(fmt.Sprintf("Processing main matching rules for probe %s", probe.Name))
|
|
if matched, match := i.processMatches(response, probe.Matchs); matched {
|
|
Common.LogDebug("Hard match found")
|
|
return
|
|
} else if match != nil {
|
|
Common.LogDebug("Soft match found")
|
|
softFound = true
|
|
softMatch = *match
|
|
}
|
|
|
|
// Process fallback matching rules
|
|
if probe.Fallback != "" {
|
|
Common.LogDebug(fmt.Sprintf("Attempting fallback match: %s", probe.Fallback))
|
|
if fbProbe, ok := v.ProbesMapKName[probe.Fallback]; ok {
|
|
if matched, match := i.processMatches(response, fbProbe.Matchs); matched {
|
|
Common.LogDebug("Fallback match successful")
|
|
return
|
|
} else if match != nil {
|
|
Common.LogDebug("Fallback soft match found")
|
|
softFound = true
|
|
softMatch = *match
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle case when no match is found
|
|
if !i.Found {
|
|
Common.LogDebug("No hard match found, handling no match case")
|
|
i.handleNoMatch(response, result, softFound, softMatch)
|
|
}
|
|
}
|
|
|
|
// processMatches processes the set of matching rules
|
|
func (i *Info) processMatches(response []byte, matches *[]Match) (bool, *Match) {
|
|
Common.LogDebug(fmt.Sprintf("Starting to process matching rules, total %d rules", len(*matches)))
|
|
var softMatch *Match
|
|
|
|
for _, match := range *matches {
|
|
if !match.MatchPattern(response) {
|
|
continue
|
|
}
|
|
|
|
if !match.IsSoft {
|
|
Common.LogDebug(fmt.Sprintf("Hard match found: %s", match.Service))
|
|
i.handleHardMatch(response, &match)
|
|
return true, nil
|
|
} else if softMatch == nil {
|
|
Common.LogDebug(fmt.Sprintf("Soft match found: %s", match.Service))
|
|
tmpMatch := match
|
|
softMatch = &tmpMatch
|
|
}
|
|
}
|
|
|
|
return false, softMatch
|
|
}
|
|
|
|
// handleHardMatch handles hard match results
|
|
func (i *Info) handleHardMatch(response []byte, match *Match) {
|
|
Common.LogDebug(fmt.Sprintf("Processing hard match result: %s", match.Service))
|
|
result := &i.Result
|
|
extras := match.ParseVersionInfo(response)
|
|
extrasMap := extras.ToMap()
|
|
|
|
result.Service.Name = match.Service
|
|
result.Extras = extrasMap
|
|
result.Banner = trimBanner(response)
|
|
result.Service.Extras = extrasMap
|
|
|
|
// Special handling for microsoft-ds service
|
|
if result.Service.Name == "microsoft-ds" {
|
|
Common.LogDebug("Special handling for microsoft-ds service")
|
|
result.Service.Extras["hostname"] = result.Banner
|
|
}
|
|
|
|
i.Found = true
|
|
Common.LogDebug(fmt.Sprintf("Service identification result: %s, Banner: %s", result.Service.Name, result.Banner))
|
|
}
|
|
|
|
// handleNoMatch handles the case when no match is found
|
|
func (i *Info) handleNoMatch(response []byte, result *Result, softFound bool, softMatch Match) {
|
|
Common.LogDebug("Handling no match case")
|
|
result.Banner = trimBanner(response)
|
|
|
|
if !softFound {
|
|
// Try to identify HTTP service
|
|
if strings.Contains(result.Banner, "HTTP/") ||
|
|
strings.Contains(result.Banner, "html") {
|
|
Common.LogDebug("Identified as HTTP service")
|
|
result.Service.Name = "http"
|
|
} else {
|
|
Common.LogDebug("Unknown service")
|
|
result.Service.Name = "unknown"
|
|
}
|
|
} else {
|
|
Common.LogDebug("Using soft match result")
|
|
extras := softMatch.ParseVersionInfo(response)
|
|
result.Service.Extras = extras.ToMap()
|
|
result.Service.Name = softMatch.Service
|
|
i.Found = true
|
|
Common.LogDebug(fmt.Sprintf("Soft match service: %s", result.Service.Name))
|
|
}
|
|
}
|
|
|
|
// Connect sends data and gets response
|
|
func (i *Info) Connect(msg []byte) []byte {
|
|
i.Write(msg)
|
|
reply, _ := i.Read()
|
|
return reply
|
|
}
|
|
|
|
const WrTimeout = 5 // Default read/write timeout (seconds)
|
|
|
|
// Write writes data to the connection
|
|
func (i *Info) Write(msg []byte) error {
|
|
if i.Conn == nil {
|
|
return nil
|
|
}
|
|
|
|
// Set write timeout
|
|
i.Conn.SetWriteDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
|
|
|
|
// Write data
|
|
_, err := i.Conn.Write(msg)
|
|
if err != nil && strings.Contains(err.Error(), "close") {
|
|
i.Conn.Close()
|
|
// Retry when connection is closed
|
|
i.Conn, err = net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second)
|
|
if err == nil {
|
|
i.Conn.SetWriteDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
|
|
_, err = i.Conn.Write(msg)
|
|
}
|
|
}
|
|
|
|
// Record sent data
|
|
if err == nil {
|
|
i.Result.Send = msg
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Read reads response from the connection
|
|
func (i *Info) Read() ([]byte, error) {
|
|
if i.Conn == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Set read timeout
|
|
i.Conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
|
|
|
|
// Read data
|
|
result, err := readFromConn(i.Conn)
|
|
if err != nil && strings.Contains(err.Error(), "close") {
|
|
return result, err
|
|
}
|
|
|
|
// Record received data
|
|
if len(result) > 0 {
|
|
i.Result.Recv = result
|
|
}
|
|
|
|
return result, err
|
|
}
|
|
|
|
// readFromConn helper function to read data from connection
|
|
func readFromConn(conn net.Conn) ([]byte, error) {
|
|
size := 2 * 1024 // Read buffer size
|
|
var result []byte
|
|
|
|
for {
|
|
buf := make([]byte, size)
|
|
count, err := conn.Read(buf)
|
|
|
|
if count > 0 {
|
|
result = append(result, buf[:count]...)
|
|
}
|
|
|
|
if err != nil {
|
|
if len(result) > 0 {
|
|
return result, nil
|
|
}
|
|
if err == io.EOF {
|
|
return result, nil
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
if count < size {
|
|
return result, nil
|
|
}
|
|
}
|
|
} |