Created
February 14, 2026 00:31
-
-
Save mainframed/7f2fe5a1ccdbe8568233164d79a8d27a to your computer and use it in GitHub Desktop.
PSScan.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $FormatEnumerationLimit = -1 | |
| function Grab-Banner { | |
| Param( | |
| [string]$TargetIP, | |
| [int]$Port, | |
| [int]$TimeOut = 3000 | |
| ) | |
| $banner = "" | |
| $protocol = "unknown" | |
| try { | |
| $client = New-Object System.Net.Sockets.TcpClient | |
| $connect = $client.BeginConnect($TargetIP, $Port, $null, $null) | |
| $wait = $connect.AsyncWaitHandle.WaitOne($TimeOut, $false) | |
| if (!$wait -or !$client.Connected) { | |
| $client.Close() | |
| return $null | |
| } | |
| $stream = $client.GetStream() | |
| $stream.ReadTimeout = $TimeOut | |
| # --------------------------------------------------------------- | |
| # PHASE 1: Wait briefly and see if the server speaks first. | |
| # Protocols like FTP, SSH, and TN3270 send data on connect. | |
| # HTTP/HTTPS servers stay silent and wait for a request. | |
| # --------------------------------------------------------------- | |
| Start-Sleep -Milliseconds 800 | |
| $serverSpokeFirst = $stream.DataAvailable | |
| if ($serverSpokeFirst) { | |
| $buf = New-Object byte[] 4096 | |
| $read = $stream.Read($buf, 0, $buf.Length) | |
| if ($read -gt 0) { | |
| $rawBytes = $buf[0..($read - 1)] | |
| $rawText = [System.Text.Encoding]::ASCII.GetString($rawBytes).Trim() | |
| # --- Detect SSH: starts with "SSH-" --- | |
| if ($rawText.StartsWith("SSH-")) { | |
| $protocol = "SSH" | |
| $banner = ($rawText -split "`n")[0].Trim() | |
| } | |
| # --- Detect FTP: starts with 220 (but not SMTP) --- | |
| elseif ($rawText -match "^220[\s-]" -and $rawText -notmatch "SMTP|ESMTP") { | |
| $protocol = "FTP" | |
| $banner = $rawText | |
| if ($banner.Length -gt 300) { $banner = $banner.Substring(0, 300) + "..." } | |
| } | |
| # --- Detect SMTP: starts with 220 and contains SMTP --- | |
| elseif ($rawText -match "^220.*(?:SMTP|ESMTP)") { | |
| $protocol = "SMTP" | |
| $banner = ($rawText -split "`n")[0].Trim() | |
| } | |
| # --- Detect Telnet/TN3270: starts with IAC (0xFF) sequences --- | |
| elseif ($rawBytes[0] -eq 0xFF) { | |
| $isTN3270E = $false | |
| $hasEOR = $false | |
| $hasBinary = $false | |
| $telnetOptions = @() | |
| for ($i = 0; $i -lt $rawBytes.Length - 2; $i++) { | |
| if ($rawBytes[$i] -eq 0xFF) { | |
| $verb = $rawBytes[$i + 1] | |
| $opt = $rawBytes[$i + 2] | |
| # Skip IAC IAC (escaped 0xFF in data) | |
| if ($verb -eq 0xFF) { $i += 1; continue } | |
| $verbName = switch ($verb) { | |
| 0xFB { "WILL" } | |
| 0xFC { "WONT" } | |
| 0xFD { "DO" } | |
| 0xFE { "DONT" } | |
| 0xFA { "SB" } | |
| default { "0x{0:X2}" -f $verb } | |
| } | |
| $optName = switch ($opt) { | |
| 0x00 { "BINARY" } | |
| 0x01 { "ECHO" } | |
| 0x03 { "SUPPRESS-GO-AHEAD" } | |
| 0x05 { "STATUS" } | |
| 0x18 { "TERMINAL-TYPE" } | |
| 0x19 { "EOR" } | |
| 0x28 { "TN3270E" } | |
| default { "OPT-0x{0:X2}" -f $opt } | |
| } | |
| if ($verb -ne 0xFA) { | |
| $telnetOptions += "$verbName $optName" | |
| } | |
| if ($opt -eq 0x28) { $isTN3270E = $true } | |
| if ($opt -eq 0x19) { $hasEOR = $true } | |
| if ($opt -eq 0x00) { $hasBinary = $true } | |
| $i += 2 | |
| } | |
| } | |
| if ($isTN3270E) { | |
| $protocol = "TN3270E" | |
| $banner = "TN3270E | Negotiation: $($telnetOptions -join ', ')" | |
| } | |
| elseif ($hasEOR -and $hasBinary) { | |
| # EOR + BINARY without explicit TN3270E is often plain TN3270 | |
| $protocol = "TN3270 (probable)" | |
| $banner = "TN3270 (EOR+BINARY, no TN3270E option) | Negotiation: $($telnetOptions -join ', ')" | |
| } | |
| elseif ($telnetOptions.Count -gt 0) { | |
| $protocol = "Telnet" | |
| $banner = "Telnet | Negotiation: $($telnetOptions -join ', ')" | |
| } | |
| else { | |
| $protocol = "Telnet (minimal)" | |
| $banner = "Telnet (IAC data but no parseable options)" | |
| } | |
| } | |
| # --- Unknown server-speaks-first protocol --- | |
| else { | |
| $protocol = "unknown (server-speaks-first)" | |
| $banner = $rawText | |
| if ($banner.Length -gt 200) { $banner = $banner.Substring(0, 200) + "..." } | |
| } | |
| } | |
| } | |
| else { | |
| # --------------------------------------------------------------- | |
| # PHASE 2: Server did NOT speak first. | |
| # Try TLS handshake first, then fall back to plain HTTP. | |
| # --------------------------------------------------------------- | |
| $client.Close() | |
| # --- Attempt TLS --- | |
| $tlsSuccess = $false | |
| try { | |
| $client2 = New-Object System.Net.Sockets.TcpClient | |
| $connect2 = $client2.BeginConnect($TargetIP, $Port, $null, $null) | |
| $wait2 = $connect2.AsyncWaitHandle.WaitOne($TimeOut, $false) | |
| if ($wait2 -and $client2.Connected) { | |
| $stream2 = $client2.GetStream() | |
| $sslStream = New-Object System.Net.Security.SslStream($stream2, $false, {$true}) | |
| $sslStream.AuthenticateAsClient($TargetIP) | |
| $tlsSuccess = $true | |
| # TLS succeeded - send HTTP HEAD | |
| $request = "HEAD / HTTP/1.0`r`nHost: $TargetIP`r`nConnection: close`r`n`r`n" | |
| $bytes = [System.Text.Encoding]::ASCII.GetBytes($request) | |
| $sslStream.Write($bytes, 0, $bytes.Length) | |
| Start-Sleep -Milliseconds 800 | |
| $buf = New-Object byte[] 4096 | |
| $sslStream.ReadTimeout = $TimeOut | |
| $read = $sslStream.Read($buf, 0, $buf.Length) | |
| $proto = $sslStream.SslProtocol | |
| $protocol = "HTTPS" | |
| if ($read -gt 0) { | |
| $response = [System.Text.Encoding]::ASCII.GetString($buf, 0, $read) | |
| $statusLine = ($response -split "`r`n")[0] | |
| $serverHeader = "" | |
| foreach ($line in ($response -split "`r`n")) { | |
| if ($line -match "^Server:\s*(.+)$") { | |
| $serverHeader = $Matches[1].Trim() | |
| break | |
| } | |
| } | |
| if ($serverHeader) { | |
| $banner = "$statusLine | Server: $serverHeader | TLS: $proto" | |
| } else { | |
| $banner = "$statusLine | TLS: $proto" | |
| } | |
| } | |
| else { | |
| $banner = "TLS: $proto (no HTTP response to HEAD)" | |
| } | |
| $sslStream.Close() | |
| $client2.Close() | |
| } | |
| else { | |
| $client2.Close() | |
| } | |
| } | |
| catch { | |
| # TLS failed - not an HTTPS endpoint | |
| try { $client2.Close() } catch {} | |
| } | |
| # --- If TLS failed, try plain HTTP --- | |
| if (!$tlsSuccess) { | |
| try { | |
| $client3 = New-Object System.Net.Sockets.TcpClient | |
| $connect3 = $client3.BeginConnect($TargetIP, $Port, $null, $null) | |
| $wait3 = $connect3.AsyncWaitHandle.WaitOne($TimeOut, $false) | |
| if ($wait3 -and $client3.Connected) { | |
| $stream3 = $client3.GetStream() | |
| $stream3.ReadTimeout = $TimeOut | |
| $request = "HEAD / HTTP/1.0`r`nHost: $TargetIP`r`nConnection: close`r`n`r`n" | |
| $bytes = [System.Text.Encoding]::ASCII.GetBytes($request) | |
| $stream3.Write($bytes, 0, $bytes.Length) | |
| Start-Sleep -Milliseconds 800 | |
| $buf = New-Object byte[] 4096 | |
| $read = $stream3.Read($buf, 0, $buf.Length) | |
| if ($read -gt 0) { | |
| $response = [System.Text.Encoding]::ASCII.GetString($buf, 0, $read) | |
| if ($response -match "^HTTP/") { | |
| $protocol = "HTTP" | |
| $statusLine = ($response -split "`r`n")[0] | |
| $serverHeader = "" | |
| foreach ($line in ($response -split "`r`n")) { | |
| if ($line -match "^Server:\s*(.+)$") { | |
| $serverHeader = $Matches[1].Trim() | |
| break | |
| } | |
| } | |
| if ($serverHeader) { | |
| $banner = "$statusLine | Server: $serverHeader" | |
| } else { | |
| $banner = $statusLine | |
| } | |
| } | |
| else { | |
| $protocol = "unknown (client-speaks-first)" | |
| $banner = $response.Trim() | |
| if ($banner.Length -gt 200) { $banner = $banner.Substring(0, 200) + "..." } | |
| } | |
| } | |
| $client3.Close() | |
| } | |
| else { | |
| $client3.Close() | |
| } | |
| } | |
| catch { | |
| try { $client3.Close() } catch {} | |
| } | |
| } | |
| } | |
| try { $client.Close() } catch {} | |
| } | |
| catch { | |
| $banner = "" | |
| } | |
| if ($banner -eq "") { return "[$protocol] (open, no banner)" } | |
| return "[$protocol] $banner" | |
| } | |
| function scan { | |
| <# | |
| .SYNOPSIS | |
| Port scanner with protocol-detection-based banner grabbing. | |
| Does NOT assume services based on port number - detects the protocol | |
| from server behavior (server-speaks-first vs client-speaks-first, | |
| IAC negotiation parsing, TLS handshake, etc.) | |
| .PARAMETER IPStart | |
| Your starting IP | |
| .PARAMETER IPEnd | |
| Your ending IP | |
| .PARAMETER File | |
| Provide a file (one IP per line) instead of IPStart and IPEnd | |
| .PARAMETER DNS | |
| Try to get HostNames from IPs | |
| .PARAMETER forcedns | |
| Try to resolve DNS names regardless of ping result | |
| .PARAMETER PortScan | |
| Perform a PortScan | |
| .PARAMETER Ports | |
| Ports to scan. No defaults - you must specify what you need. | |
| .PARAMETER BannerGrab | |
| Detect protocol and grab banners from open ports | |
| .PARAMETER forceportscan | |
| Port scan regardless of ping result | |
| .PARAMETER outfile | |
| CSV output path | |
| .PARAMETER v | |
| Verbose per-host output during scan | |
| .PARAMETER collectall | |
| Include all hosts in results, not just those with hits | |
| .PARAMETER ExportCSV | |
| Export results as CSV | |
| .PARAMETER TimeOut | |
| Ping/connect timeout in ms. Default 100. | |
| .PARAMETER BannerTimeOut | |
| Banner grab timeout in ms. Default 3000. | |
| .EXAMPLE | |
| scan -IPStart 10.0.0.1 -IPEnd 10.0.0.1 -PortScan -BannerGrab -Ports 23,443,2023,10443,8080 -forceportscan -v | |
| .EXAMPLE | |
| scan -File .\lpars.txt -PortScan -BannerGrab -Ports 21,22,23,80,443,992,2023,2323,4035,8080,10443 -forceportscan -v -DNS -ExportCSV -outfile results.csv | |
| #> | |
| Param( | |
| [ValidatePattern("\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")] | |
| [string]$IPStart, | |
| [ValidatePattern("\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")] | |
| [string]$IPEnd, | |
| [string]$File, | |
| [string]$outfile, | |
| [switch]$DNS, | |
| [switch]$PortScan, | |
| [switch]$BannerGrab, | |
| [switch]$forcedns, | |
| [switch]$forceportscan, | |
| [switch]$ExportCSV, | |
| [switch]$collectall, | |
| [switch]$v, | |
| [int[]]$Ports, | |
| [int]$TimeOut = 100, | |
| [int]$BannerTimeOut = 3000 | |
| ) | |
| Write-Host -ForegroundColor DarkGray "" | |
| Write-Host -ForegroundColor Cyan "=== Port Scanner with Protocol Detection ===" | |
| Write-Host -ForegroundColor DarkGray "Detects: SSH, FTP, TN3270/TN3270E, Telnet, HTTP, HTTPS, SMTP" | |
| Write-Host -ForegroundColor DarkGray "No port-number assumptions - protocol identified from server behavior" | |
| Write-Host -ForegroundColor DarkGray "" | |
| if (!$Ports -or $Ports.Count -eq 0) { | |
| Write-Host -ForegroundColor Yellow "No ports specified. Use -Ports to specify which ports to scan." | |
| Write-Host -ForegroundColor Yellow "Example: -Ports 21,22,23,80,443,2023,8080,10443" | |
| return | |
| } | |
| $totalresults = @() | |
| if ($IPStart -and $IPEnd) { | |
| foreach($a in ($IPStart.Split(".")[0]..$IPEnd.Split(".")[0])) { | |
| foreach($b in ($IPStart.Split(".")[1]..$IPEnd.Split(".")[1])) { | |
| foreach($c in ($IPStart.Split(".")[2]..$IPEnd.Split(".")[2])) { | |
| foreach($d in ($IPStart.Split(".")[3]..$IPEnd.Split(".")[3])) { | |
| $ip = "$a.$b.$c.$d" | |
| dostuff | |
| if (($global:pingcheck -eq $TRUE) -or ($global:hostcheck -eq $TRUE) -or ($global:portcheck -eq $TRUE) -or ($collectall)) { | |
| $totalresults += $Global:obj | |
| } | |
| } | |
| } | |
| } | |
| } | |
| $totalresults | Format-Table -Property IP, DNS, PING, PORTS, BANNERS -AutoSize -Wrap | |
| if ($ExportCSV) { | |
| $totalresults | Select-Object IP, DNS, PING, @{Name='PORTS';Expression={$_.PORTS -join ';'}}, @{Name='BANNERS';Expression={$_.BANNERS -join ' | '}} | Export-Csv $outfile -NoTypeInformation | |
| } | |
| } | |
| elseif ($File) { | |
| foreach ($line in Get-Content $File) { | |
| $ip = $line.Trim() | |
| if ($ip -eq "") { continue } | |
| dostuff | |
| if (($global:pingcheck -eq $TRUE) -or ($global:hostcheck -eq $TRUE) -or ($global:portcheck -eq $TRUE) -or ($collectall)) { | |
| $totalresults += $Global:obj | |
| } | |
| } | |
| $totalresults | Format-Table -Property IP, DNS, PING, PORTS, BANNERS -AutoSize -Wrap | |
| if ($ExportCSV) { | |
| $totalresults | Select-Object IP, DNS, PING, @{Name='PORTS';Expression={$_.PORTS -join ';'}}, @{Name='BANNERS';Expression={$_.BANNERS -join ' | '}} | Export-Csv $outfile -NoTypeInformation | |
| } | |
| } | |
| } | |
| function dostuff { | |
| $ping = New-Object System.Net.NetworkInformation.Ping | |
| ### Ping | |
| try { | |
| $pingStatus = $ping.Send($ip, $TimeOut) | |
| $pingsuccess = $pingStatus.Status | |
| $Global:pingcheck = ($pingsuccess -eq "Success") | |
| } | |
| catch { | |
| $Global:pingcheck = $False | |
| $pingsuccess = "Failed" | |
| } | |
| ### DNS | |
| if ($DNS) { | |
| try { | |
| if ($forcedns) { | |
| $getHostEntry = [Net.DNS]::BeginGetHostEntry($ip, $null, $null) | |
| } else { | |
| $getHostEntry = [Net.DNS]::BeginGetHostEntry($pingStatus.Address, $null, $null) | |
| } | |
| $Global:hostcheck = $TRUE | |
| } | |
| catch { | |
| $hostname = "no DNS" | |
| $Global:hostcheck = $FALSE | |
| } | |
| } | |
| ### Port scan | |
| $openPorts = @() | |
| $banners = @() | |
| try { | |
| if ($PortScan) { | |
| for ($i = 0; $i -lt $Ports.Count; $i++) { | |
| $port = $Ports[$i] | |
| $client = New-Object System.Net.Sockets.TcpClient | |
| $targetAddr = if ($forceportscan) { $ip } else { $pingStatus.Address } | |
| $beginConnect = $client.BeginConnect($targetAddr, $port, $null, $null) | |
| $waitResult = $beginConnect.AsyncWaitHandle.WaitOne($TimeOut, $false) | |
| if ($waitResult -and $client.Connected) { | |
| $openPorts += $port | |
| } else { | |
| Start-Sleep -Milli $TimeOut | |
| if ($client.Connected) { | |
| $openPorts += $port | |
| } | |
| } | |
| $client.Close() | |
| } | |
| if ($openPorts.Count -eq 0) { | |
| $openPorts = "no open ports" | |
| $Global:portcheck = $FALSE | |
| } else { | |
| $Global:portcheck = $TRUE | |
| } | |
| } | |
| } | |
| catch { | |
| $openPorts = "no open ports" | |
| $Global:portcheck = $FALSE | |
| } | |
| ### Banner grab on open ports | |
| if ($BannerGrab -and $openPorts -is [array] -and $openPorts.Count -gt 0) { | |
| foreach ($p in $openPorts) { | |
| Write-Host -ForegroundColor DarkGray " Probing ${ip}:${p}..." | |
| $b = Grab-Banner -TargetIP $ip -Port $p -TimeOut $BannerTimeOut | |
| if ($b) { | |
| $banners += "${p}: $b" | |
| } else { | |
| $banners += "${p}: (open, could not identify)" | |
| } | |
| } | |
| } | |
| ### Resolve DNS | |
| if ($DNS) { | |
| try { | |
| $hostName = ([System.Net.DNS]::EndGetHostEntry([IAsyncResult]$getHostEntry)).HostName | |
| } | |
| catch { | |
| } | |
| } | |
| ### Verbose output | |
| if ($v) { | |
| Write-Host "`n--- $ip ---" -ForegroundColor White | |
| $color = if ($Global:pingcheck) { "Green" } else { "Red" } | |
| Write-Host " PING: $pingsuccess" -ForegroundColor $color | |
| if ($DNS) { | |
| $color = if ($hostname -and $hostname -ne "no DNS") { "Green" } else { "Red" } | |
| Write-Host " DNS: $hostname" -ForegroundColor $color | |
| } | |
| $color = if ($Global:portcheck) { "Green" } else { "Red" } | |
| Write-Host " PORTS: $($openPorts -join ', ')" -ForegroundColor $color | |
| if ($banners.Count -gt 0) { | |
| Write-Host " SERVICES:" -ForegroundColor Cyan | |
| foreach ($entry in $banners) { | |
| Write-Host " $entry" -ForegroundColor Cyan | |
| } | |
| } | |
| } | |
| ### Build result object | |
| $Global:obj = New-Object PSObject | |
| $Global:obj | Add-Member NoteProperty -Name IP -Value $ip | |
| $Global:obj | Add-Member NoteProperty -Name DNS -Value $hostname | |
| $Global:obj | Add-Member NoteProperty -Name PING -Value $pingsuccess | |
| $Global:obj | Add-Member NoteProperty -Name PORTS -Value $openPorts | |
| $Global:obj | Add-Member NoteProperty -Name BANNERS -Value $banners | |
| $openports = "" | |
| $hostname = "" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment