Skip to content

Instantly share code, notes, and snippets.

@mainframed
Created February 14, 2026 00:31
Show Gist options
  • Select an option

  • Save mainframed/7f2fe5a1ccdbe8568233164d79a8d27a to your computer and use it in GitHub Desktop.

Select an option

Save mainframed/7f2fe5a1ccdbe8568233164d79a8d27a to your computer and use it in GitHub Desktop.
PSScan.ps1
$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