A PowerShell Port Scan

I recently guest-hosted Episode 69 the PowerScripting Podcast. One of the items we discussed was a port scanning script from TheAdminGuy blog. Here’s the original script: http://theadminguy.wordpress.com/2009/04/30/portscan-with-powershell/. As written, there’s nothing technically wrong with it. The script gets the job done and tells the user which ports are open. However, I thought this was an opportunity to demonstrate an even more PowerShell-like approach. Even the author admits there are limitations to his script.

Whenever I see a script like this, the first thing I try to do is separate the functionality. In the VBScript days we would write monolithic scripts to do everything. Sure, we might write a function or subroutine, but based on my forum experiences, even that was not a common occurrence. In PowerShell, we want to use smaller, cmdlet-like tools that leverage the pipeline.

In the original script, each computer is first pinged and only if reachable is a port scan conducted. This is a great purpose for a function, and you’ll find many test ping variations. Here’s another.

Function Try-Ping {
    Param([string]$computername=$(Throw "You must enter a computername to ping"))
    $ping = New-Object System.Net.NetworkInformation.Ping
    if ($ping.Send($computername).status.value__ -eq 0) {
        write $TRUE
     }
     else {
        write $FALSE
     }
} #end Try-Ping

The function takes a computername as a parameter and returns $True if the computername can be pinged. The rest of the original script was primarily the port scanning code, which again can be neatly modularized. One main change I introduced was to create a custom object with computer and port information. The existing script was merely checking in the port was open, but there’s other useful information so why not use it? You’ll see at the end how this all comes together, but here’s my port scanning function.

Function Scan-Port {
    Param([string]$computername=$env:computername,
          [array]$ports=@("21","22","23","25","80","443","3389")
         )
  #turn off error pipeline  
  $ErrorActionPreference = "SilentlyContinue"
  
  #set values for Write-Progress
  $activity="Port Scan"
  $status="Scanning $computername"
  $i=0
    
       foreach ($port in $ports){ 
       $i++
        Write-Progress -Activity $activity -status $status `
        -currentoperation "port $port" -percentcomplete (($i/$ports.count)*100)
       
        #create empty custom object
        $obj=New-Object PSObject
        $obj | Add-Member -MemberType Noteproperty -name "Computername" -value $computername.ToUpper()
        $obj | Add-Member -MemberType Noteproperty -name "Port" -value $port
        
        $tcp=New-Object System.Net.Sockets.TcpClient($computername, $port)
        
        if ($tcp.client.connected) {
 
            $obj | Add-Member -MemberType Noteproperty -name "PortOpen" -value $True
            $obj | Add-Member -MemberType Noteproperty -name "TTL" -value $($tcp.client.ttl)
            
            [string]$rep=$tcp.client.RemoteEndPoint
            [string]$ip=$rep.substring(0,$rep.indexof(":"))
            
            $obj | Add-Member -MemberType Noteproperty -name "RemoteIP" -value $ip
 
            }
        else {
#            Write-Warning "$computername not open on port: $port"
            $obj | Add-Member -MemberType Noteproperty -name "PortOpen" -value $False
            $obj | Add-Member -MemberType Noteproperty -name "TTL" -value -1
            $obj | Add-Member -MemberType Noteproperty -name "RemoteIP" -value $Null
         }  #end Else
         
            write $obj
            #disconnect the socket connection
            $tcp.client.disconnect($False)
 
        } #end foreach
        
        #dispose and disconnect
        $tcp.close()
 
        Write-Progress -Activity $activity -status "Complete" -Completed
        
} #end function

The function takes a computername and an array of ports as parameters. I’ve provided a set of defaults. Most of the code is the same as the original script except I create a custom object and populate with some properties. I also use the Write-Progress cmdlet to provide feedback about what the script is doing. Here’s how I might use this function once it’s been loaded into my PowerShell session.

PS C:\test> scan-port blog.sapien.com -ports @(25,80)

Computername : BLOG.SAPIEN.COM
Port         : 25
PortOpen     : False
TTL          : -1
RemoteIP     :

Computername : BLOG.SAPIEN.COM
Port         : 80
PortOpen     : True
TTL          : 128
RemoteIP     : 63.131.159.243

Or how about including the ping test? Now that I have objects, I can sort, filter, format, export or whatever.

$ports=get-content ports.txt

get-content computers.txt | foreach {
if (Try-Ping $_) {
   Scan-Port -computername $_ -ports $ports}
   } | sort Computername | Format-Table -group computername -auto

This will scan the list of ports from ports.txt. Finally, what about TheAdminGuy’s original goal? Here’s my slightly modified version of what I think he is trying to accomplish.

$ports=get-content ports.txt
 
get-content computers.txt | foreach {
 if (Try-Ping $_) {
   $results=Scan-port -computer $_ -ports $ports
   write-host "$_ Ports " -foreground "GREEN" -nonewline
   foreach ($port in $results) {
    if ($port.Portopen) {
        write-host "$($port.Port) " -foreground "GREEN" -nonewline
    }
    else {
        write-host "$($port.Port) " -foreground "RED" -nonewline
    }
   }
   
 } #end if try-ping
 else {
    write-host "$_ not found" -foreground "RED" -nonewline
 }
Write-Host `r
} #end foreach

These lines could be entered interactively in the shell, or put into a PS1 file, which is what I would do. I would dot source the scan port script at the beginning and I’m good to go! Now I have a port scanning function and I can use the output anyway I want.  I also now have a ping test function that I can re-use often.

These are the concepts I try to stress in forums I frequent and when teaching PowerShell. Break your work down into re-usable components centered around objects and leverage the pipeline to pull everything together.

You can download a zip file with my script samples here.