PDQ give you a good inventory of the assets the agent is installed on, but did you know you can use PDQ to see other assets? Play around finding devices on your network without an agent using PDQ and Bogdan’s “poor man’s nmap” script.
If you want to check what’s on your network beyond the devices enrolled in PDQ, Bogdan wrote a PowerShell scanner he calls “poor man’s nmap.”
https://github.com/pdqcom/Bonus-Content/blob/master/Connect-Scanners/PoorMansNMAP.ps1
It runs from a machine with the PDQ agent and checks nearby hosts for common open ports. You don’t need an agent on each device being checked, which makes it useful for finding things like cameras, printers, and other equipment missing from your endpoint inventory.
Here’s the walkthrough, starting at 16:12.
How to use it
- Pick one managed machine per network segment. That’s where the script runs. Start with one segment so you can review the results before expanding.
- Review the script’s port list. Bogdan defines common ports and their likely services. You can adjust that list for what you want to check in your environment.
- Add the script to a PowerShell scanner in PDQ. Go to Scanners → Create scanner → PowerShell, import or paste the script, then select your test machine and run it. Validate the returned columns before saving. Keep the scan scoped to your intended machines. You can now use the live shell too.
- Compare the results with your inventory. Look for unfamiliar IPs and unexpected open ports. A missing hostname doesn’t mean there’s nothing there. In the demo, a device returned no hostname but had port 23 open. It turned out to be an NDI camera.
- Investigate and keep a baseline. Confirm what the device is, who owns it, and whether it needs the service you found. Save the results so you have something to compare with the next scan.
A couple of things to know
The service names are based on port numbers. Port 23 being open suggests Telnet, but you need to verify what’s actually listening. This helps with discovery; it doesn’t establish that a device has a particular vulnerability. A device that doesn’t respond may also be offline, filtered, or outside the scan’s reach.
Give whoever handles your security alerts a heads-up. Your MDR or XDR may flag the scanning activity.
And take it easy with unfamiliar equipment. In the webcast, follow-up poking at that camera caused it to stop working.
This is a fun script to run. If you’re already doing this, how are you comparing scan results over time to catch new devices or newly opened ports?
# Poor man's nmap - PDQ Connect PowerShell scanner edition. # Auto-detects the local subnet, parallel ICMP sweep, then TCP-connect probes # common ports on each live host. Emits ONE OBJECT PER LISTENING HOST so the # scanner maps the properties to columns (IPAddress, Hostname, MAC, OpenPorts). # No params, no Format-Table, no ConvertTo-Csv - those produce strings, and the # scanner would just reflect String.Length into a bogus "Length" column. # --- Config (edit here instead of params) ------------------------------------ $Ports = @(21,22,23,25,53,80,110,135,139,143,389,443,445,587,993,995,1433,3306,3389,5432,5900,8080,8443) $TimeoutMs = 500 $ServiceNames = @{ 21='ftp'; 22='ssh'; 23='telnet'; 25='smtp'; 53='dns'; 80='http'; 110='pop3' 135='msrpc'; 139='netbios'; 143='imap'; 389='ldap'; 443='https'; 445='smb' 587='submission'; 993='imaps'; 995='pop3s'; 1433='mssql'; 3306='mysql' 3389='rdp'; 5432='postgres'; 5900='vnc'; 8080='http-alt'; 8443='https-alt' } function Get-IPRange { param([Parameter(Mandatory)][string]$CIDR) $parts = $CIDR -split '/' $base = [System.Net.IPAddress]::Parse($parts[0]) $prefix = [int]$parts[1] $bytes = $base.GetAddressBytes() [Array]::Reverse($bytes) $ipInt = [System.BitConverter]::ToUInt32($bytes, 0) if ($prefix -eq 0) { $mask = [uint32]0 } else { $mask = [uint32](([math]::Pow(2, $prefix) - 1)) -shl (32 - $prefix) } $network = [uint32]($ipInt -band $mask) $hostBits = [uint32]((-bnot $mask) -band [uint32]::MaxValue) $broadcast = [uint32]($network -bor $hostBits) if ($prefix -ge 31) { $lo = [int64]$network; $hi = [int64]$broadcast } else { $lo = [int64]$network + 1; $hi = [int64]$broadcast - 1 } for ($i = $lo; $i -le $hi; $i++) { $b = [System.BitConverter]::GetBytes([uint32]$i) [Array]::Reverse($b) ([System.Net.IPAddress]::new($b)).IPAddressToString } } # --- Auto-detect the local subnet -------------------------------------------- $cfg = Get-NetIPConfiguration -ErrorAction SilentlyContinue | Where-Object { $_.IPv4DefaultGateway -and $_.NetAdapter.Status -eq 'Up' } | Select-Object -First 1 if (-not $cfg) { throw "Could not auto-detect an active IPv4 network." } $Cidr = "$($cfg.IPv4Address.IPAddress)/$($cfg.IPv4Address.PrefixLength)" $targets = @(Get-IPRange -CIDR $Cidr) # --- Host discovery (parallel ICMP, batched) --------------------------------- $live = New-Object System.Collections.Generic.List[string] $batch = 256 for ($i = 0; $i -lt $targets.Count; $i += $batch) { $slice = $targets[$i..([math]::Min($i + $batch - 1, $targets.Count - 1))] $pingers = foreach ($ip in $slice) { $p = [System.Net.NetworkInformation.Ping]::new() [PSCustomObject]@{ IP = $ip; Pinger = $p; Task = $p.SendPingAsync($ip, $TimeoutMs) } } try { [void][System.Threading.Tasks.Task]::WaitAll([System.Threading.Tasks.Task[]]($pingers.Task), $TimeoutMs + 1000) } catch {} foreach ($pg in $pingers) { if ($pg.Task.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion -and $pg.Task.Result.Status -eq 'Success') { $live.Add($pg.IP) } $pg.Pinger.Dispose() } } # --- Port scan + enrichment (emit only LISTENING hosts) ---------------------- $results = foreach ($ip in $live) { $conns = foreach ($port in $Ports) { $c = [System.Net.Sockets.TcpClient]::new() [PSCustomObject]@{ Port = $port; Client = $c; Task = $c.ConnectAsync($ip, $port) } } try { [void][System.Threading.Tasks.Task]::WaitAll([System.Threading.Tasks.Task[]]($conns.Task), $TimeoutMs) } catch {} $open = foreach ($cn in $conns) { $isOpen = ($cn.Task.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion -and $cn.Client.Connected) $cn.Client.Close() if ($isOpen) { $name = $ServiceNames[[int]$cn.Port] if ($name) { "$($cn.Port)/$name" } else { "$($cn.Port)" } } } $open = @($open) if ($open.Count -eq 0) { continue } # skip hosts with nothing listening $hostname = try { [System.Net.Dns]::GetHostEntry($ip).HostName } catch { '' } $mac = '' try { $n = Get-NetNeighbor -IPAddress $ip -ErrorAction Stop | Where-Object { $_.LinkLayerAddress } | Select-Object -First 1 if ($n) { $mac = $n.LinkLayerAddress } } catch { $arpLine = (arp -a $ip 2>$null | Select-String $ip) if ($arpLine -match '([0-9a-fA-F]{2}[-:]){5}[0-9a-fA-F]{2}') { $mac = $Matches[0] } } [PSCustomObject]@{ IPAddress = $ip Hostname = $hostname MAC = $mac OpenPorts = ($open -join ', ') } } # Emit objects to the pipeline -> scanner builds one column per property, # one row per listening host. $results | Sort-Object { [System.Version]$_.IPAddress }
Source: r/pdq · by /u/Amanda_PDQ