Automating a Local Portscan

Overview

In this post, I am going to demonstrate a script for automating a local TCP port scan in PowerShell. This was a seemingly simple task that I end up doing in almost all of my engagements, and I get tired of having to manually identify connected networks and built a command to port scan the internal network of an organization. I just want to do a basic TCP full connect scan all of the networks the current device is connected to (within reason). I don’t want to have to provide any parameters. Just port scan and go.

Background Knowledge

What is a TCP Connect Scan?

A full TCP connect scan (T1046) is a network reconnaissance technique used identify services running on remote endpoints within a network. The scan attempts to establish a full TCP connection with each specified port on the target to determine its status (open, closed, or filtered). This is a fairly basic technique, but is useful for an initial survey. I like to have a basic full TCP connect scanner built into my implants with the ability to conduct a more thorough scan, typically using a SOCKS proxy.

Employment Considerations

Full TCP Connect scans are fairly basic in the information that is returned and are defeated by several defense-in-depth techniques at the network layer.

  • This technique is defeated by TCP Brokering Firewalls. This type of network appliance interjects itself in the middle of the connection and establishes two TCP connections: (1) between you and the firewall and (2) between the firewall and the original destination. This results in a full TCP three-way handshake being established for every IP address you try to connect to through the firewall. It’s fairly easy to detect in practice. Normally, only a subset of IP addresses will be assigned to actual devices with listening ports, so you scan of a CIDR /24 should come back with less than 254 results with “Open” ports. If you get back “Open” ports for every IP address, then there is likely a TCP Brokering firewall in between you and the destination.
  • Full TCP Connect scans only tell you a service is listening on the port, but it doesn’t tell you what service is listening nor what version.

Building the Script

After I started building this script, I quickly found out that there are a lot of additional considerations to port scanning a network than I had originally assumed.

To start off, we first need to survey the local system for network interfaces. I figured this would be a simple for loop over the interfaces, but I quickly realized that there can be multiple interfaces, and each interface can have multiple IP addresses and network masks, so I need to keep track of the networks I see so that I don’t duplicate my scans. For example, one machine may have two interfaces on the same network and I only want to scan that network one time. For this task, I use a simple dictionary to ensure I get a unique list of networks. As a side note, I could have used a HashSet[string] that would make more sense, but this script wouldn’t be backwards compatible to .NET 2.0, and I’m always trying to maximize compatibility.

$networks = New-Object 'System.Collections.Generic.Dictionary[string,string]'

Next, I get a list of all of the interfaces using the interfaces cmdlet, which is an alias for Get-Interfaces cmdlet that is built-in to SpecterInsight. It returns an array of interfaces with various properties of the interface.

$interfaces = interfaces;

Next, I loop over each interface in the list of interfaces.

foreach($interface in $interfaces) {
	#Lines omitted for brevity
}

Within the loop shown above, I filter out interfaces that I don’t want to scan such as interfaces that are not in an “Up” state, interfaces with less than or equal to 2 valid ARP entries. This also generally eliminates loopback interfaces. The ‘continue’ keyword skips to the next interface and ensures that no networks on the current interface will be scanned.

#Network must be Up to scan
if($interface.Status -ne 'Up') {
	continue;
}

#Must have 3 or more valid arp entries
if($interface.Entries.Count -le 2) {
	continue;
}

Next, I loop through each network assigned to the interface and parse out an IPNetwork object. I don’t want to waste time nor generate too much noise by scanning subnets larger than 512 valid IP addresses, so I filter out any subnets where the CIDR is less than 23. Finally, anything that makes it to this point in the script is a valid network to scan, so I add it to the dictionary.

#Each interface can have multiple IP addresses
foreach($network in $interface.InterfaceIPs) {
	try {
		$subnet = [common.Networking.IPNetwork]::Parse($network.IP, $network.Netmask);
	} catch {
		continue;
	}
	
	#We don't want to scan too many addresses
	if($subnet.Cidr -lt 23) {
		continue;
	}
	
	#Ignore duplicate networks
	$subnetstr = $subnet.ToString();
	if(!$networks.ContainsKey($subnetstr)) {
		$networks.Add($subnetstr, $subnetstr);
	}
}

Now that we have a unique list of CIDR networks, we need to actually scan them. SpecterInsight comes with a built-in, multi-threaded TCP scanner in the recon module. First, we will need to import the recon module into the deployed Specter with the Load-ReflectiveModule command below that we will insert at the top of our script. The module will be requested and downloaded over the C2 channel to the Specter and reflectively loaded into memory.

load recon;

Finally, I loop through each CIDR network and initiate the scan. The operator may want to specify a set of ports to scan, so I’m going to pass in a variable $Ports that we can fill in later with default values in the parameter block for this script. By default, the result objects do not include a field for the CIDR network, so I add that with the Add-Member method. Lastly, we write the scan results to the pipeline using the Select statement. The results will then be serialized back to the C2 server and converted to JSON.

#Scan each network
foreach($network in $networks.Keys) {
	$results = scan $network -Ports $Ports;
	$results | Add-Member -MemberType NoteProperty -Name "Network" -Value $network;
	foreach($result in $results) {
		$result | Add-Member -MemberType NoteProperty -Name "IP" -Value $result.IPAddress;
	}
	$results | select * -Exclude IPAddress;
}

The last thing we need to do is define a parameter block for operator input. The options we are going to expose are for port selection, the number of concurrent threads to use for scanning, and timeouts. By default, the OS will wait up to 120 seconds before closing a TCP connection. We won’t want to wait that long, we set a much lower option. Additionally, we are going to set some reasonable defaults so that an operator can just queue this task without any additional configuration 9 out of 10 times.

param(
    [Parameter(Mandatory = $true, HelpMessage = "A comma-separated list of ports to scan.")]
    [int[]]$Ports = @(21, 22, 23, 25, 80, 135, 161, 389, 443, 445, 3389),
    
    [Parameter(Mandatory = $true, HelpMessage = "The number of concurrent threads to use for scanning.")]
    [int]$ThreadCount = 128,

    [Parameter(Mandatory = $true, HelpMessage = "The amount of time in milliseconds to wait for a response to the initial TCP SYN packet.")]
    [int]$Timeout = 1250
)

The parameter block above results in the UI shown below:

Looking good! Now lets run it!

Results

Interactive Shell Output

Now when we run the script, we get output similar to the JSON shown below. The IP and CIDR Network are also defined as fields to enable aggregations in ELK dashboards. Each port can have one of three different results:

  • Filtered: Received no response nor packet from destination IP address.
  • Firewalled: Received a TCP RST packet in response.
  • Open: Established a full TCP three-way handshake.
[
  {
    "21": "Filtered",
    "22": "Filtered",
    "23": "Filtered",
    "25": "Filtered",
    "80": "Filtered",
    "135": "Open",
    "161": "Filtered",
    "389": "Filtered",
    "443": "Open",
    "445": "Open",
    "3389": "Open",
    "Network": "192.168.1.0/24",
    "IP": "192.168.1.101"
  }
]

When converted to text by the client, we get a nicely formatted table of open ports per IP address. This script took about 56 seconds to run against two CIDR /24 networks and results in a concise list of common open ports for systems running in the environment.

Demo

Kibana Visualizations

SpecterInsight automatically ships our TCP Scan off to ELK using the JSON data shown previously. Now we want to visualize the results in Kibana, all we have to do is build a few aggregation based visualizations.

Complete Script

The complete script is shown below so you can see it all together.

param(
    [Parameter(Mandatory = $true, HelpMessage = "A comma-separated list of ports to scan.")]
    [int[]]$Ports = @(21, 22, 23, 25, 80, 135, 161, 389, 443, 445, 3389),
    
    [Parameter(Mandatory = $true, HelpMessage = "The number of concurrent threads to use for scanning.")]
    [int]$ThreadCount = 128,

    [Parameter(Mandatory = $true, HelpMessage = "The amount of time in milliseconds to wait for a response to the initial TCP SYN packet.")]
    [int]$Timeout = 1250
)

load recon;

$interfaces = interfaces;

$networks = New-Object 'System.Collections.Generic.Dictionary[string,string]'
foreach($interface in $interfaces) {
	#Network must be Up to scan
	if($interface.Status -ne 'Up') {
		continue;
	}
	
	#Must have 3 or more valid arp entries
	if($interface.Entries.Count -le 2) {
		continue;
	}
	
	#Each interface can have multiple IP addresses
	foreach($network in $interface.InterfaceIPs) {
		try {
			$subnet = [common.Networking.IPNetwork]::Parse($network.IP, $network.Netmask);
		} catch {
			continue;
		}
		
		#We don't want to scan too many addresses
		if($subnet.Cidr -lt 23) {
			continue;
		}
		
		#Ignore duplicate networks
		$subnetstr = $subnet.ToString();
		if(!$networks.ContainsKey($subnetstr)) {
			$networks.Add($subnetstr, $subnetstr);
		}
	}
}

#Scan each network
foreach($network in $networks.Keys) {
	$results = scan $network -Ports $Ports -ThreadCount $ThreadCount -Timeout $Timeout;
	$results | Add-Member -MemberType NoteProperty -Name "Network" -Value $network;
	foreach($result in $results) {
		$result | Add-Member -MemberType NoteProperty -Name "IP" -Value $result.IPAddress;
	}
	$results | select * -Exclude IPAddress;
}

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top