Private/Network.psm1
|
using namespace System.IO using namespace System.Net using namespace System.Text using namespace System.Net.Http using namespace System.Reflection using namespace System.Diagnostics using namespace System.Net.Sockets using namespace System.Net.Security using namespace System.Net.NetworkInformation using namespace System.Runtime.InteropServices using namespace System.Security.Authentication using namespace System.Text.RegularExpressions using module .\Config.psm1 using module .\Console.psm1 using module .\Enums.psm1 using module .\Utilities.psm1 using module .\DllUtils.psm1 class HostsEntry { [int]$LineNumber [string]$IPAddress [bool]$IsValidIP [string]$Hostname [string]$Comment HostsEntry([int]$LineNumber, [string]$IPAddress, [string]$Hostname, [string]$Comment) { $this.LineNumber = $LineNumber $this.IPAddress = $IPAddress $this.Hostname = $Hostname $this.Comment = $Comment.Trim() $testIP = $null $this.IsValidIP = [Net.IPAddress]::TryParse($IPAddress, [ref]$testIP) } [string] ToString() { if ([string]::IsNullOrWhiteSpace($this.Comment)) { return "$($this.IPAddress) $($this.Hostname)" } return "$($this.IPAddress) $($this.Hostname) # $($this.Comment)" } } class HostsFile { static [string] $DefaultPath = (Join-Path $Env:SystemRoot 'System32\drivers\etc\hosts') [string] $Path [System.Collections.Generic.List[HostsEntry]] $Entries hidden [string[]] $RawLines HostsFile([string]$Path) { if (!(Test-Path $Path)) { throw "Hosts file does not exist: $Path" } $this.Path = $Path $this.Entries = [System.Collections.Generic.List[HostsEntry]]::new() $this.Load() } static [HostsFile] Create() { return [HostsFile]::new([HostsFile]::DefaultPath) } static [HostsFile] Create([string]$Path) { return [HostsFile]::new($Path) } hidden [void] Load() { $this.RawLines = Get-Content $this.Path -ErrorAction Stop $commentLine = '^\s*#' $hostLine = '^\s*(?<IPAddress>\S+)\s+(?<Hostname>\S+)(\s*|\s+#(?<Comment>.*))$' for ($i = 0; $i -lt $this.RawLines.Length; $i++) { $line = $this.RawLines[$i] if (!($line -match $commentLine) -and ($line -match $hostLine)) { $comment = '' if ($Matches['Comment']) { $comment = $Matches['Comment'] } $entry = [HostsEntry]::new($i, $Matches['IPAddress'], $Matches['Hostname'], $comment) $this.Entries.Add($entry) } } } [HostsEntry[]] GetEntries() { return $this.Entries.ToArray() } [HostsEntry] GetByHostname([string]$Hostname) { foreach ($entry in $this.Entries) { if ($entry.Hostname -eq $Hostname) { return $entry } } return $null } [HostsEntry[]] GetByIPAddress([string]$IPAddress) { $results = [System.Collections.Generic.List[HostsEntry]]::new() foreach ($entry in $this.Entries) { if ($entry.IPAddress -eq $IPAddress) { $results.Add($entry) } } return $results.ToArray() } [void] Add([string]$IPAddress, [string]$Hostname) { $this.Add($IPAddress, $Hostname, "") } [void] Add([string]$IPAddress, [string]$Hostname, [string]$Comment) { $existing = $this.GetByHostname($Hostname) $newEntry = [HostsEntry]::new(-1, $IPAddress, $Hostname, $Comment) if ($existing) { $this.RawLines[$existing.LineNumber] = $newEntry.ToString() $existing.IPAddress = $IPAddress $existing.Comment = $Comment $existing.IsValidIP = [Net.IPAddress]::TryParse($IPAddress, [ref]([ipaddress]::Any)) } else { $this.RawLines += $newEntry.ToString() $newEntry.LineNumber = $this.RawLines.Length - 1 $this.Entries.Add($newEntry) } } [void] RemoveByHostname([string]$Hostname) { $remainingLines = [System.Collections.Generic.List[string]]::new() $newEntries = [System.Collections.Generic.List[HostsEntry]]::new() $lineNumber = 0 foreach ($entry in $this.Entries) { if ($entry.Hostname -ne $Hostname) { $remainingLines.Add($entry.ToString()) $entry.LineNumber = $lineNumber $newEntries.Add($entry) $lineNumber++ } } $this.RawLines = $remainingLines.ToArray() $this.Entries = $newEntries } [void] RemoveByIPAddress([string]$IPAddress) { $remainingLines = [System.Collections.Generic.List[string]]::new() $newEntries = [System.Collections.Generic.List[HostsEntry]]::new() $lineNumber = 0 foreach ($entry in $this.Entries) { if ($entry.IPAddress -ne $IPAddress) { $remainingLines.Add($entry.ToString()) $entry.LineNumber = $lineNumber $newEntries.Add($entry) $lineNumber++ } } $this.RawLines = $remainingLines.ToArray() $this.Entries = $newEntries } [void] Save() { $this.RawLines | Out-File -Encoding ascii -FilePath $this.Path -ErrorAction Stop } [void] Show() { notepad $this.Path } [string] ToString() { return $this.Path } } class NetworkManager { [string] $HostName static [System.Net.IPAddress[]] $IPAddresses static [string] $caller NetworkManager([string]$HostName) { $this.HostName = $HostName $this::IPAddresses = [System.Net.Dns]::GetHostAddresses($HostName) } static [string] GetResponse([string]$URL) { [System.Net.HttpWebRequest]$Request = [System.Net.HttpWebRequest]::Create($URL) $Request.Method = "GET" $Request.Timeout = 10000 [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]$Request.GetResponse() if ($Response.StatusCode -eq [System.Net.HttpStatusCode]::OK) { [System.IO.Stream]$ReceiveStream = $Response.GetResponseStream() [System.IO.StreamReader]$ReadStream = [System.IO.StreamReader]::new($ReceiveStream) [string]$Content = $ReadStream.ReadToEnd() $ReadStream.Close() $Response.Close() return $Content } else { throw "The request failed with status code: $($Response.StatusCode)" } } static [void] BlockAllOutbound() { if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { & sudo iptables -P OUTPUT DROP } else { netsh advfirewall set allprofiles firewallpolicy blockinbound, blockoutbound } } static [void] UnblockAllOutbound() { if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { & sudo iptables -P OUTPUT ACCEPT } else { netsh advfirewall set allprofiles firewallpolicy blockinbound, allowoutbound } } static [void] UploadFile([string]$SourcePath, [string]$DestinationURL) { Invoke-RestMethod -Uri $DestinationURL -Method Post -InFile $SourcePath } static [bool] TestConnection([string]$HostName) { [ValidateNotNullOrEmpty()][string]$HostName = $HostName if () { Add-Type -AssemblyName System.Net.NetworkInformation } $cs = $null; $cc = [NetworkManager]::caller $re = @{ true = @{ m = "Success"; c = "Green" }; false = @{ m = "Failed"; c = "Red" } } Write-Host "$cc Testing Connection ... " -ForegroundColor Blue -NoNewline try { [System.Net.NetworkInformation.PingReply]$PingReply = [System.Net.NetworkInformation.Ping]::new().Send($HostName) $cs = $PingReply.Status -eq [System.Net.NetworkInformation.IPStatus]::Success } catch [System.Net.Sockets.SocketException], [System.Net.NetworkInformation.PingException] { $cs = $false } catch { $cs = $false Write-Error $_ } $re = $re[$cs.ToString()] Write-Host $re.m -ForegroundColor $re.c return $cs } static [bool] IsIPv6AddressValid([string]$IP) { $IPv4Regex = '(((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))' $G = '[a-f\d]{1,4}' $Tail = @(":", "(:($G)?|$IPv4Regex)", ":($IPv4Regex|$G(:$G)?|)", "(:$IPv4Regex|:$G(:$IPv4Regex|(:$G){0,2})|:)", "((:$G){0,2}(:$IPv4Regex|(:$G){1,2})|:)", "((:$G){0,3}(:$IPv4Regex|(:$G){1,2})|:)", "((:$G){0,4}(:$IPv4Regex|(:$G){1,2})|:)") [string] $IPv6RegexString = $G $Tail | ForEach-Object { $IPv6RegexString = "${G}:($IPv6RegexString|$_)" } $IPv6RegexString = ":(:$G){0,5}((:$G){1,2}|:$IPv4Regex)|$IPv6RegexString" $IPv6RegexString = $IPv6RegexString -replace '\(' , '(?:' [regex] $IPv6Regex = $IPv6RegexString return ($IP -imatch "^$IPv6Regex$") } static [bool] IsMACAddressValid([string]$mac) { $RegEx = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9A-Fa-f]{2}){6}$" return ($mac -match $RegEx) } static [bool] IsSubNetMaskValid([string]$IP) { $RegEx = "^(254|252|248|240|224|192|128).0.0.0$|^255.(254|252|248|240|224|192|128|0).0.0$|^255.255.(254|252|248|240|224|192|128|0).0$|^255.255.255.(255|254|252|248|240|224|192|128|0)$" return ($IP -match $RegEx) } static [bool] IsIPv4AddressValid([string]$IP) { $RegEx = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" return ($IP -match $RegEx) } } class NetworkDevice { [string]$IpAddress = "" [string]$Hostname = "" [string]$MacAddress = "" [string]$Vendor = "" [string]$DeviceType = "Unknown" [System.Collections.Generic.List[int]]$OpenPorts = [System.Collections.Generic.List[int]]::new() # Active fingerprint fields [System.Nullable[int]]$Ttl [string]$HttpTitle = "" [string]$HttpServer = "" [string]$SshBanner = "" [string]$TlsSubject = "" [string]$SmbOs = "" [string]$SnmpDescr = "" [string]$NetbiosName = "" [System.Collections.Generic.List[string]]$MdnsServices = [System.Collections.Generic.List[string]]::new() [string]$SsdpServer = "" [string] get_OpenPortsDisplay() { if ($this.OpenPorts.Count -gt 0) { return $this.OpenPorts -join ", " } return "-" } [uint32] get_IpSortKey() { $addr = [System.Net.IPAddress]::Any if ([System.Net.IPAddress]::TryParse($this.IpAddress, [ref]$addr)) { $bytes = $addr.GetAddressBytes() if ($bytes.Length -eq 4) { return [uint32]($bytes[0] -shl 24 -bor $bytes[1] -shl 16 -bor $bytes[2] -shl 8 -bor $bytes[3]) } } return 0 } } class DeviceOverrides { static [string]$FilePath = [System.IO.Path]::Combine($env:LOCALAPPDATA, 'KillerScan', 'overrides.json') static [System.Collections.Generic.Dictionary[string, string]]$_overrides = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::OrdinalIgnoreCase) static [void] Load() { try { if (!(Test-Path [DeviceOverrides]::FilePath)) { return } $json = Get-Content [DeviceOverrides]::FilePath -Raw $loaded = ConvertFrom-Json $json -AsHashtable if ($null -ne $loaded) { [DeviceOverrides]::_overrides = [System.Collections.Generic.Dictionary[string, string]]::new($loaded, [System.StringComparer]::OrdinalIgnoreCase) } } catch { $null # Ignore JSON load errors } } static [void] Save() { try { $dir = [System.IO.Path]::GetDirectoryName([DeviceOverrides]::FilePath) if (!(Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } $json = [DeviceOverrides]::_overrides | ConvertTo-Json -Depth 1 $json | Out-File -FilePath [DeviceOverrides]::FilePath -Encoding UTF8 -Force } catch { $null # Ignore file save errors } } static [void] Set([string]$mac, [string]$deviceType) { if ([string]::IsNullOrEmpty($mac)) { return } $key = $mac.ToUpperInvariant() if ($null -eq $deviceType) { $null = [DeviceOverrides]::_overrides.Remove($key) } else { [DeviceOverrides]::_overrides[$key] = $deviceType } [DeviceOverrides]::Save() } static [string] Get([string]$mac) { if ([string]::IsNullOrEmpty($mac)) { return $null } $val = $null if ([DeviceOverrides]::_overrides.TryGetValue($mac.ToUpperInvariant(), [ref]$val)) { return $val } return $null } static [bool] Has([string]$mac) { if ([string]::IsNullOrEmpty($mac)) { return $false } return [DeviceOverrides]::_overrides.ContainsKey($mac.ToUpperInvariant()) } } class OuiLookup { static [System.Collections.Generic.Dictionary[string, string]] $OuiTable = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::OrdinalIgnoreCase) static [bool] $_loaded = $false static [void] Load() { if ([OuiLookup]::_loaded) { return } $moduleDir = Split-Path -Parent $MyInvocation.MyCommand.Path if ([string]::IsNullOrEmpty($moduleDir)) { $moduleDir = "." } $csvPath = Join-Path $moduleDir "KillerScan\oui.csv" if (!(Test-Path $csvPath)) { return } try { $data = Import-Csv $csvPath foreach ($row in $data) { $oui = $row.Assignment $vendor = $row."Organization Name" if (![string]::IsNullOrEmpty($oui)) { [OuiLookup]::OuiTable[$oui] = $vendor } } [OuiLookup]::_loaded = $true } catch { $null # Ignore CSV load errors } } static [string] GetVendor([string]$macAddress) { if (![OuiLookup]::_loaded) { [OuiLookup]::Load() } if ([string]::IsNullOrEmpty($macAddress) -or $macAddress.Length -lt 8) { return "" } $prefix = $macAddress.Replace(":", "").Replace("-", "").Substring(0, 6).ToUpperInvariant() $vendor = "" if ([OuiLookup]::OuiTable.TryGetValue($prefix, [ref]$vendor)) { return $vendor } return "" } static [int] get_Count() { return [OuiLookup]::OuiTable.Count } } class NetworkScanner { static [int[]]$ProbePorts = @(22, 53, 80, 443, 445, 515, 631, 902, 2179, 3389, 8006, 8123, 5000, 5001, 9100, 161, 8080, 8443, 21, 23, 548, 5353, 1900, 62078) static [pscustomobject[]]$HostnameKeywords = @( [pscustomobject]@{Pattern = "lgwebos"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "webostv"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "lgtv"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "roku"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "firetv"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "fire-tv"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "appletv"; Type = "Apple TV" }, [pscustomobject]@{Pattern = "apple-tv"; Type = "Apple TV" }, [pscustomobject]@{Pattern = "chromecast"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "smarttv"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "tizen"; Type = "Smart TV" }, [pscustomobject]@{Pattern = "wiim"; Type = "Media Streamer" }, [pscustomobject]@{Pattern = "linkplay"; Type = "Media Streamer" }, [pscustomobject]@{Pattern = "sonos"; Type = "Media Streamer" }, [pscustomobject]@{Pattern = "heos"; Type = "Media Streamer" }, [pscustomobject]@{Pattern = "homeassistant"; Type = "Home Assistant" }, [pscustomobject]@{Pattern = "home-assistant"; Type = "Home Assistant" }, [pscustomobject]@{Pattern = "pihole"; Type = "DNS Server" }, [pscustomobject]@{Pattern = "pi-hole"; Type = "DNS Server" }, [pscustomobject]@{Pattern = "proxmox"; Type = "Hypervisor" }, [pscustomobject]@{Pattern = "esxi"; Type = "Hypervisor" }, [pscustomobject]@{Pattern = "unifi"; Type = "Network" }, [pscustomobject]@{Pattern = "ubnt"; Type = "Network" }, [pscustomobject]@{Pattern = "synology"; Type = "NAS" }, [pscustomobject]@{Pattern = "diskstation"; Type = "NAS" }, [pscustomobject]@{Pattern = "freenas"; Type = "NAS" }, [pscustomobject]@{Pattern = "truenas"; Type = "NAS" } ) static [System.Collections.Generic.Dictionary[string, string]] $OuiBadMap = $null static [void] InitStatic() { if ($null -eq [NetworkScanner]::OuiBadMap) { [NetworkScanner]::OuiBadMap = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::OrdinalIgnoreCase) [NetworkScanner]::OuiBadMap["C4:F7:C1"] = "Linkplay" [NetworkScanner]::OuiBadMap["58:CF:79"] = "Linkplay" } } static [System.Collections.Generic.List[System.Net.IPAddress]] GetAddressesInSubnet([string]$cidr) { [NetworkScanner]::InitStatic() $parts = $cidr.Trim().Split('/') $ip = [System.Net.IPAddress]::Parse($parts[0]) $prefixLen = if ($parts.Length -gt 1) { [int]::Parse($parts[1]) } else { 24 } $ipBytes = $ip.GetAddressBytes() if ($ipBytes.Length -ne 4) { return [System.Collections.Generic.List[System.Net.IPAddress]]::new() } $reverseBytes = [System.Net.IPAddress]::NetworkToHostOrder([System.BitConverter]::ToInt32($ipBytes, 0)) $ipUint = [uint32]$reverseBytes $mask = if ($prefixLen -eq 0) { [uint32]0 } else { [uint32](0xFFFFFFFF -shl (32 - $prefixLen)) } $network = $ipUint -band $mask $broadcast = $network -bor (-bnot $mask) $addresses = [System.Collections.Generic.List[System.Net.IPAddress]]::new() for ($addr = $network + 1; $addr -lt $broadcast; $addr++) { $b = [System.BitConverter]::GetBytes([System.Net.IPAddress]::HostToNetworkOrder([int32]$addr)) $addresses.Add([System.Net.IPAddress]::new($b)) } return $addresses } static [string] GetMacAddress([System.Net.IPAddress]$addr) { $address = [string]::Empty try { $mac = New-Object byte[] 6 $ipInt = [System.BitConverter]::ToUInt32($addr.GetAddressBytes(), 0) $result = [DllUtils]::SendARP($ipInt, 0, $mac, 6) if ($result -eq 0) { $macStr = ($mac | ForEach-Object { $_.ToString("X2") }) -join ":" if ($macStr -ne "00:00:00:00:00:00") { $address = $macStr } } } catch { throw "Mac resolution failed" } return $address } static [System.Collections.Generic.Dictionary[string, string]] GetArpCache() { $cache = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::OrdinalIgnoreCase) try { $output = arp -a foreach ($line in $output) { $trimmed = $line.Trim() if ([string]::IsNullOrWhiteSpace($trimmed)) { continue } $parts = $trimmed -split '\s+' if ($parts.Length -ge 2) { $ip = $parts[0] $mac = $parts[1].Replace('-', ':').ToUpperInvariant() $testIp = $null if ([System.Net.IPAddress]::TryParse($ip, [ref]$testIp) -and $mac.Length -eq 17 -and $mac.Contains(':')) { $cache[$ip] = $mac } } } } catch { throw "ARP command failed" } return $cache } static [string] ClassifyDevice([NetworkDevice]$device) { [NetworkScanner]::InitStatic() $manual = [DeviceOverrides]::Get($device.MacAddress) if ($null -ne $manual) { return $manual } $ports = $device.OpenPorts $_host = [string]$device.Hostname.ToLowerInvariant() $title = [string]$device.HttpTitle.ToLowerInvariant() $server = [string]$device.HttpServer.ToLowerInvariant() $tls = [string]$device.TlsSubject.ToLowerInvariant() $ssh = [string]$device.SshBanner.ToLowerInvariant() $snmp = [string]$device.SnmpDescr.ToLowerInvariant() $nbName = [string]$device.NetbiosName.ToLowerInvariant() foreach ($kw in [NetworkScanner]::HostnameKeywords) { if ($_host.Contains($kw.Pattern.ToLowerInvariant())) { return $kw.Type } } $vendor = $device.Vendor.ToLowerInvariant() if (![string]::IsNullOrEmpty($device.MacAddress) -and $device.MacAddress.Length -ge 8) { $prefix = $device.MacAddress.Substring(0, 8).ToUpperInvariant() $corr = $null if ([NetworkScanner]::OuiBadMap.TryGetValue($prefix, [ref]$corr)) { $vendor = $corr.ToLowerInvariant() } } $scores = @{} $AddScore = { param([string]$type, [int]$s) $scores[$type] = ($scores[$type] -as [int]) + $s } $hasWorkstationPorts = $ports.Contains(3389) -or $ports.Contains(445) if ($ports.Contains(8006)) { &$AddScore "Hypervisor" 15 } if ($title.Contains("proxmox")) { &$AddScore "Hypervisor" 15 } if ($tls.Contains("vmware") -or $title.Contains("vmware esxi")) { &$AddScore "Hypervisor" 15 } if ($ports.Contains(902) -and $ports.Contains(443) -and !$hasWorkstationPorts) { &$AddScore "Hypervisor" 10 } if ($ports.Contains(2179) -and ($ports.Contains(5985) -or $ports.Contains(5986)) -and !$hasWorkstationPorts) { &$AddScore "Hypervisor" 8 } if ($tls.Contains("xenserver") -or $title.Contains("xenserver")) { &$AddScore "Hypervisor" 15 } if ($ports.Contains(3389) -and $ports.Contains(445)) { &$AddScore "Windows" 6 } if ($server.Contains("microsoft-iis")) { &$AddScore "Windows" 6 } if (![string]::IsNullOrEmpty($nbName) -and $ports.Contains(445)) { &$AddScore "Windows" 4 } if ($device.Ttl -ge 120 -and $device.Ttl -le 128 -and $ports.Contains(445)) { &$AddScore "Windows" 3 } if ($ports.Contains(3389) -and $ports.Contains(445) -and ($ports.Contains(80) -or $ports.Contains(443) -or $ports.Contains(53))) { &$AddScore "Windows Server" 6 } if ($title.Contains("exchange") -or $server.Contains("exchange")) { &$AddScore "Windows Server" 10 } if ($snmp.Contains("windows server")) { &$AddScore "Windows Server" 15 } $sshIsWindows = $ssh.Contains("for_windows") if ($sshIsWindows) { &$AddScore "Windows" 15 } if (!$sshIsWindows -and ($ssh.StartsWith("ssh-2.0-openssh") -or $ssh.StartsWith("ssh-1.99-openssh"))) { &$AddScore "Linux/SSH" 8 } if ($device.Ttl -ge 60 -and $device.Ttl -le 64 -and $ports.Contains(22) -and !$sshIsWindows) { &$AddScore "Linux/SSH" 3 } $isNetworkVendor = $vendor -match "cisco|ubiquiti|aruba|ruckus|meraki|netgear|tp-link|fortinet|juniper|mikrotik|gl technologies|gl.inet|draytek|zyxel|linksys|sonicwall|watchguard" if ($isNetworkVendor) { &$AddScore "Network" 8 } if ($ssh -match "cisco|routeros|mikrotik") { &$AddScore "Network" 15 } if ($title -match "unifi|fortigate|sonicwall|pfsense|opnsense|mikrotik") { &$AddScore "Network" 15 } if ($snmp -match "cisco ios|juniper|fortigate") { &$AddScore "Network" 12 } if ($isNetworkVendor -and $ports.Contains(53)) { &$AddScore "Router" 10 } if ($isNetworkVendor -and $ports.Contains(161) -and !$ports.Contains(53)) { &$AddScore "Switch/AP" 8 } $isPrinterVendor = $vendor -match "canon|epson|brother|xerox|lexmark|ricoh|konica|kyocera" if ($ports.Contains(9100) -or $ports.Contains(515) -or $ports.Contains(631)) { &$AddScore "Printer" 8 } if ($isPrinterVendor -and ($ports.Contains(9100) -or $ports.Contains(515) -or $ports.Contains(631))) { &$AddScore "Printer" 10 } if ($isPrinterVendor -and $ports.Count -le 3) { &$AddScore "Printer" 6 } if ($vendor.Contains("hewlett packard") -and $ports.Contains(9100)) { &$AddScore "Printer" 12 } if ($snmp -match "laserjet|officejet|printer") { &$AddScore "Printer" 15 } if ($title -match "embedded web server|web image monitor") { &$AddScore "Printer" 10 } if ($vendor -match "synology|qnap|asustor|drobo|buffalo|terramaster") { &$AddScore "NAS" 12 } if ($title -match "diskstation|synology|dsm ") { &$AddScore "NAS" 15 } if ($title -match "qts|qnap|truenas|freenas") { &$AddScore "NAS" 15 } if ($ports.Contains(548)) { &$AddScore "NAS" 4 } $isApple = $vendor.Contains("apple") if ($isApple -and $ports.Contains(62078)) { &$AddScore "Apple Device" 12 } if ($isApple -and $ports.Count -le 2) { &$AddScore "Apple Device" 6 } foreach ($svc in $device.MdnsServices) { if ($svc -match "_airplay|_raop|_airport") { &$AddScore "Apple Device" 10; break } } $isMobileVendor = $vendor -match "samsung|oneplus|xiaomi|huawei|motorola|oppo|vivo|zte|lg electronics" if ($isMobileVendor -and $ports.Count -le 2) { &$AddScore "Mobile" 8 } if ($vendor -match "hikvision|dahua|axis|amcrest|reolink|foscam") { &$AddScore "Camera" 12 } if ($title -match "hikvision|dahua|camera|dvr|nvr|ipcam") { &$AddScore "Camera" 12 } if ($ports.Contains(554)) { &$AddScore "Camera" 4 } if ($vendor -match "espressif|tuya|sonoff|shelly|nest|ecobee|signify|lutron|wemo|wyze|aqara|linkplay|wiim") { &$AddScore "IoT" 10 } if ($ports.Contains(8123)) { &$AddScore "Home Assistant" 12 } if ($title.Contains("home assistant") -or $_host.Contains("homeassistant") -or $_host.Contains("home-assistant")) { &$AddScore "Home Assistant" 15 } if ($title -match "pi-hole|pihole|adguard") { &$AddScore "DNS Server" 15 } if ($ports.Contains(53) -and $ports.Contains(80) -and !$isNetworkVendor) { &$AddScore "DNS Server" 6 } if ($ports.Contains(80) -or $ports.Contains(443) -or $ports.Contains(8080)) { &$AddScore "Web Device" 2 } if ($scores.Count -gt 0) { $sorted = $scores.GetEnumerator() | Sort-Object Value -Descending $winner = $sorted[0] if ($winner.Value -ge 6) { return $winner.Key } } $localAdminMac = $false if (![string]::IsNullOrEmpty($device.MacAddress) -and $device.MacAddress.Length -ge 2) { $firstByteStr = $device.MacAddress.Substring(0, 2) try { $firstByte = [System.Convert]::ToByte($firstByteStr, 16) $localAdminMac = ($firstByte -band 0x02) -ne 0 } catch { $null } } if ($localAdminMac -and $ports.Count -eq 0) { return "Mobile" } if ($ports.Contains(22)) { return "Linux/SSH" } if ($ports.Contains(445) -or $ports.Contains(3389)) { return "Windows" } if ($ports.Contains(80) -or $ports.Contains(443)) { return "Web Device" } if ($ports.Count -eq 0 -and ![string]::IsNullOrEmpty($device.MacAddress)) { return "IoT" } if ($ports.Count -eq 0) { return "Unknown" } return "Other" } static [void] ProbeHttp([NetworkDevice]$device, [System.Net.IPAddress]$addr) { $candidates = @( @{Port = 80; Https = $false }, @{Port = 8080; Https = $false }, @{Port = 5000; Https = $false }, @{Port = 8123; Https = $false }, @{Port = 443; Https = $true }, @{Port = 8443; Https = $true }, @{Port = 8006; Https = $true }, @{Port = 5001; Https = $true } ) $handler = [System.Net.Http.HttpClientHandler]::new() $handler.ServerCertificateCustomValidationCallback = { $true } $handler.AllowAutoRedirect = $true $handler.MaxAutomaticRedirections = 2 $client = [System.Net.Http.HttpClient]::new($handler) $client.Timeout = [System.TimeSpan]::FromMilliseconds(1500) $client.DefaultRequestHeaders.UserAgent.ParseAdd("KillerScan/1.2") foreach ($c in $candidates) { if (!$device.OpenPorts.Contains($c.Port)) { continue } try { $scheme = if ($c.Https) { "https" } else { "http" } $url = "{0}://{1}:{2}/" -f $scheme, $addr.ToString(), $c.Port $resp = $client.GetAsync($url).GetAwaiter().GetResult() $serverVals = $null if ($resp.Headers.TryGetValues("Server", [ref]$serverVals)) { $device.HttpServer = ($serverVals -join ", ").Trim() } $body = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult() if ($body -match '<title[^>]*>([^<]+)</title>') { $device.HttpTitle = [System.Net.WebUtility]::HtmlDecode($Matches[1]).Trim() } if (![string]::IsNullOrEmpty($device.HttpTitle) -or ![string]::IsNullOrEmpty($device.HttpServer)) { break } } catch { throw "HTTP probe failed for this candidate" } } $client.Dispose() $handler.Dispose() } static [void] ProbeSshBanner([NetworkDevice]$device, [System.Net.IPAddress]$addr) { try { $tcp = [System.Net.Sockets.TcpClient]::new() $connect = $tcp.ConnectAsync($addr, 22) if ($connect.Wait(1000) -and $tcp.Connected) { $stream = $tcp.GetStream() $stream.ReadTimeout = 1500 $buf = New-Object byte[] 256 $n = $stream.Read($buf, 0, $buf.Length) if ($n -gt 0) { $banner = [System.Text.Encoding]::ASCII.GetString($buf, 0, $n).Trim() if ($banner.StartsWith("SSH-")) { $nl = $banner.IndexOfAny(@("`r", "`n")) $device.SshBanner = if ($nl -gt 0) { $banner.Substring(0, $nl) } else { $banner } } } } $tcp.Dispose() } catch { throw "SSH banner probe failed" } } static [void] ProbeTlsCert([NetworkDevice]$device, [System.Net.IPAddress]$addr) { $tlsPorts = @(443, 8443, 8006, 902, 5001) foreach ($port in $tlsPorts) { if (!$device.OpenPorts.Contains($port) -and $port -ne 443) { continue } try { $tcp = [System.Net.Sockets.TcpClient]::new() $connect = $tcp.ConnectAsync($addr, $port) if ($connect.Wait(1500) -and $tcp.Connected) { $ssl = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, { $true }) $auth = $ssl.AuthenticateAsClientAsync($addr.ToString()) if ($auth.Wait(1500)) { if ($null -ne $ssl.RemoteCertificate) { $device.TlsSubject = $ssl.RemoteCertificate.Subject $tcp.Dispose() return } } } $tcp.Dispose() } catch { $null } } } static [void] ProbeNetbios([NetworkDevice]$device, [System.Net.IPAddress]$addr) { $query = @( 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01 ) try { $udp = [System.Net.Sockets.UdpClient]::new() $udp.Client.SendTimeout = 500 $udp.Client.ReceiveTimeout = 800 $ep = [System.Net.IPEndPoint]::new($addr, 137) $null = $udp.Send($query, $query.Length, $ep) $remoteEP = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) $resp = $udp.Receive([ref]$remoteEP) if ($resp.Length -ge 57) { $numNames = $resp[56] for ($i = 0; $i -lt $numNames -and 57 + ($i * 18) + 18 -le $resp.Length; $i++) { $off = 57 + ($i * 18) $suffix = $resp[$off + 15] if ($suffix -eq 0x00 -or $suffix -eq 0x20) { $name = [System.Text.Encoding]::ASCII.GetString($resp, $off, 15).Trim() if (![string]::IsNullOrWhiteSpace($name) -and $name -notmatch '[\x01\x02]') { $device.NetbiosName = $name break } } } } $udp.Dispose() } catch { $null } } static [void] ProbeSnmp([NetworkDevice]$device, [System.Net.IPAddress]$addr) { $query = @( 0x30, 0x29, 0x02, 0x01, 0x00, 0x04, 0x06, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0xA0, 0x1C, 0x02, 0x04, 0x7F, 0x8B, 0x2C, 0x1D, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x0E, 0x30, 0x0C, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00, 0x05, 0x00 ) try { $udp = [System.Net.Sockets.UdpClient]::new() $udp.Client.SendTimeout = 500 $udp.Client.ReceiveTimeout = 1000 $ep = [System.Net.IPEndPoint]::new($addr, 161) $null = $udp.Send($query, $query.Length, $ep) $remoteEP = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) $resp = $udp.Receive([ref]$remoteEP) if ($resp.Length -ge 30) { for ($i = $resp.Length - 2; $i -ge 0; $i--) { if ($resp[$i] -eq 0x04) { $len = $resp[$i + 1] if ($len -ge 5 -and $i + 2 + $len -le $resp.Length) { $s = [System.Text.Encoding]::UTF8.GetString($resp, $i + 2, $len) if ($s -ne "public" -and $s -notmatch '[\x00-\x1F]') { $device.SnmpDescr = $s.Trim() break } } } } } $udp.Dispose() } catch { $null } } static [NetworkDevice] ProbeHost([System.Net.IPAddress]$addr, [string]$cachedMac) { $device = [NetworkDevice]::new() $device.IpAddress = $addr.ToString() $device.MacAddress = $cachedMac try { $entry = [System.Net.Dns]::GetHostEntry($addr) $device.Hostname = $entry.HostName } catch { $null } try { $ping = [System.Net.NetworkInformation.Ping]::new() $reply = $ping.Send($addr, 400) if ($reply.Status -eq 'Success' -and $null -ne $reply.Options) { $device.Ttl = $reply.Options.Ttl } } catch { $null } [int[]]$portsFound = [NetworkScanner]::ProbePorts | ForEach-Object -Parallel { $tcp = [System.Net.Sockets.TcpClient]::new() try { $c = $tcp.ConnectAsync($using:addr, $_) if ($c.Wait(200) -and $tcp.Connected) { $_ } } catch { $null } $tcp.Dispose() } -ThrottleLimit 24 if ($null -ne $portsFound) { foreach ($p in $portsFound) { $device.OpenPorts.Add($p) } } if (![string]::IsNullOrEmpty($device.MacAddress)) { $device.Vendor = [OuiLookup]::GetVendor($device.MacAddress) } if ($device.OpenPorts.Count -gt 0) { if ($device.OpenPorts | Where-Object { $_ -match '^(80|443|8006|8080|8443|5000|5001|8123)$' }) { [NetworkScanner]::ProbeHttp($device, $addr) } if ($device.OpenPorts -contains 22) { [NetworkScanner]::ProbeSshBanner($device, $addr) } if ($device.OpenPorts | Where-Object { $_ -match '^(443|8443|8006|902)$' }) { [NetworkScanner]::ProbeTlsCert($device, $addr) } } [NetworkScanner]::ProbeNetbios($device, $addr) [NetworkScanner]::ProbeSnmp($device, $addr) $device.DeviceType = [NetworkScanner]::ClassifyDevice($device) return $device } static [NetworkDevice[]] ScanSubnet([string]$cidr, [bool]$fullScan = $true) { [NetworkScanner]::InitStatic() $addresses = [NetworkScanner]::GetAddressesInSubnet($cidr) $discoveredHosts = @{} Write-Host "Discovering hosts on $cidr..." -ForegroundColor Blue $arpCache = [NetworkScanner]::GetArpCache() foreach ($addr in $addresses) { $ipStr = $addr.ToString() if ($arpCache.ContainsKey($ipStr)) { $discoveredHosts[$ipStr] = $arpCache[$ipStr] } } $aliveIps = $addresses | ForEach-Object -Parallel { $p = [System.Net.NetworkInformation.Ping]::new() try { $r = $p.Send($_.ToString(), 500) if ($r.Status -eq 'Success') { $_.ToString() } } catch { $null } } -ThrottleLimit 100 if ($null -ne $aliveIps) { foreach ($ip in $aliveIps) { if (!$discoveredHosts.ContainsKey($ip)) { $discoveredHosts[$ip] = "" } } } Write-Host "Resolving $($discoveredHosts.Count) MAC addresses..." -ForegroundColor Blue $results = [System.Collections.Generic.List[NetworkDevice]]::new() $hostsToProbe = foreach ($key in $discoveredHosts.Keys) { [pscustomobject]@{IP = $key; Mac = $discoveredHosts[$key] } } if ($fullScan) { Write-Host "Probing $($hostsToProbe.Count) alive hosts..." -ForegroundColor Blue $resultsArr = $hostsToProbe | ForEach-Object -Parallel { $addr = [System.Net.IPAddress]::Parse($_.IP) $mac = $_.Mac if ([string]::IsNullOrEmpty($mac)) { $mac = [NetworkScanner]::GetMacAddress($addr) } return [NetworkScanner]::ProbeHost($addr, $mac) } -ThrottleLimit 10 if ($null -ne $resultsArr) { $results.AddRange($resultsArr) } } else { foreach ($h in $hostsToProbe) { $addr = [System.Net.IPAddress]::Parse($h.IP) $mac = $h.Mac if ([string]::IsNullOrEmpty($mac)) { $mac = [NetworkScanner]::GetMacAddress($addr) } $dev = [NetworkDevice]::new() $dev.IpAddress = $h.IP $dev.MacAddress = $mac try { $entry = [System.Net.Dns]::GetHostEntry($addr) $dev.Hostname = $entry.HostName } catch { throw "Hostname resolution failed" } if (![string]::IsNullOrEmpty($mac)) { $dev.Vendor = [OuiLookup]::GetVendor($mac) } $dev.DeviceType = [NetworkScanner]::ClassifyDevice($dev) $results.Add($dev) } } return $results.ToArray() | Sort-Object { $_.get_IpSortKey() } } } |