In the ever-evolving landscape of IT security, maintaining visibility over network connections is paramount. Whether you’re managing a large enterprise network or overseeing a small to medium-sized business, understanding which IP addresses are actively communicating with your systems is crucial.
This blog post delves into a specialized PowerShell script designed to alert administrators to specific IP addresses that are either listening or in an established state. This tool is invaluable for IT professionals, managed service providers (MSPs), and security teams aiming to bolster their network monitoring capabilities. The following content expounds on how to monitor TCP and UDP Connections on Windows using PowerShell.
Understanding the PowerShell Script
The script provided serves as an alerting mechanism for network connections, identifying specified IP addresses in a ‘Listening’ or ‘Established’ state. It goes beyond basic firewall checks, providing insights into active connections that may bypass traditional security filters. The script outputs essential details such as the address, process ID, state, protocol, local address, and process name. For those using NinjaOne, the results can be automatically saved to a custom field for further analysis.
Importance for IT Professionals and MSPs
In today’s digital environment, where cyber threats are ever-present, having granular control over network connections is essential. This script addresses the need for real-time monitoring of network activities, enabling IT professionals to respond quickly to potential security threats. For MSPs managing multiple client networks, this script offers a standardized method for monitoring and reporting on network activity, ensuring consistent and thorough oversight.
The Script:
#Requires -Version 5.1 <# .SYNOPSIS Alert on specified addresses that are Listening or Established and optionally save the results to a custom field. .DESCRIPTION Will alert on addresses, regardless if a firewall is blocking them or not. Checks for addresses that are in a 'Listen' or 'Established' state. UDP is a stateless protocol and will not have a state. Outputs the addresses, process ID, state, protocol, local address, and process name. When a Custom Field is provided this will save the results to that custom field. PARAMETER: -IpAddress "192.168.11.1, 192.168.1.1/24" A comma separated list of IP Addresses to check. Can include IPv4 CIDR notation for ranges. IPv6 CIDR notation not supported. (e.g. 192.168.1.0/24, 10.0.10.12) .EXAMPLE -IpAddress "192.168.1.0/24, 10.0.10.12" ## EXAMPLE OUTPUT WITH IpAddress ## [Info] Valid IP Address: 192.168.11.1 [Info] Valid IP Network: 192.168.1.1/24 [Alert] Found Local Address: 192.168.1.18, Local Port: 139, Remote Address: 0.0.0.0, Remote Port: None, PID: 4, Protocol: TCP, State: Listen, Process: System [Alert] Found Local Address: 192.168.1.18, Local Port: 138, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System [Alert] Found Local Address: 192.168.1.18, Local Port: 137, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" Name of the custom field to save the results to. .EXAMPLE -IpAddress "192.168.11.1, 192.168.1.1/24" -CustomField "ReplaceMeWithAnyMultilineCustomField" ## EXAMPLE OUTPUT WITH CustomField ## [Info] Valid IP Address: 192.168.11.1 [Info] Valid IP Network: 192.168.1.1/24 [Alert] Found Local Address: 192.168.1.18, Local Port: 139, Remote Address: 0.0.0.0, Remote Port: None, PID: 4, Protocol: TCP, State: Listen, Process: System [Alert] Found Local Address: 192.168.1.18, Local Port: 138, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System [Alert] Found Local Address: 192.168.1.18, Local Port: 137, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System [Info] Saving results to custom field: ReplaceMeWithAnyMultilineCustomField [Info] Results saved to custom field: ReplaceMeWithAnyMultilineCustomField .OUTPUTS None .NOTES Supported Operating Systems: Windows 10/Windows Server 2016 or later with PowerShell 5.1 Release Notes: Initial Release By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). #> [CmdletBinding()] param ( [Parameter()] [String]$IpAddress, [String]$CustomFieldName ) begin { function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters if ($Characters -ge 10000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") } # If we're requested to set the field value for a Ninja document we'll specify it here. $DocumentationParams = @{} if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # The field below requires additional information to be set $NeedsOptions = "Dropdown" if ($DocumentName) { if ($NeedsOptions -contains $Type) { # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # If an error is received it will have an exception property, the function will exit with that error information. if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. switch ($Type) { "Checkbox" { # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID if (-not $Selection) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") } $NinjaValue = $Selection } default { # All the other types shouldn't require additional work on the input. $NinjaValue = $Value } } # We'll need to set the field differently depending on if its a field in a Ninja Document or not. if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 } if ($CustomField.Exception) { throw $CustomField } } function Test-IPNetwork { param([string]$Text) $Ip, $Prefix = $Text -split '/' $Ip -as [System.Net.IPAddress] -and $Prefix -as [int] -and $Prefix -ge 0 -and $Prefix -le 32 } function Get-IPNetwork { [CmdletBinding()] Param( [Parameter(Mandatory, Position = 0)] [ValidateScript({ $_ -eq ([IPAddress]$_).IPAddressToString })] [string]$IPAddress, [Parameter(Mandatory, Position = 1, ParameterSetName = "SubnetMask")] [ValidateScript({ $_ -eq ([IPAddress]$_).IPAddressToString })] [ValidateScript({ $SMReversed = [IPAddress]$_ $SMReversed = $SMReversed.GetAddressBytes() [array]::Reverse($SMReversed) [IPAddress]$SMReversed = $SMReversed [convert]::ToString($SMReversed.Address, 2) -match "^[1]*0{0,}$" })] [string]$SubnetMask, [Parameter(Mandatory, Position = 1, ParameterSetName = "CIDRNotation")] [ValidateRange(0, 32)] [int]$PrefixLength, [switch]$ReturnAllIPs ) [IPAddress]$IPAddress = $IPAddress if ($SubnetMask) { [IPAddress]$SubnetMask = $SubnetMask $SMReversed = $SubnetMask.GetAddressBytes() [array]::Reverse($SMReversed) [IPAddress]$SMReversed = $SMReversed [int]$PrefixLength = [convert]::ToString($SMReversed.Address, 2).replace(0, '').length } else { [IPAddress]$SubnetMask = ([Math]::Pow(2, $PrefixLength) - 1) * [Math]::Pow(2, (32 - $PrefixLength)) } $FullMask = [UInt32]'0xffffffff' $WildcardMask = [IPAddress]($SubnetMask.Address -bxor $FullMask) $NetworkId = [IPAddress]($IPAddress.Address -band $SubnetMask.Address) $Broadcast = [IPAddress](($FullMask - $NetworkId.Address) -bxor $SubnetMask.Address) # Used for determining first usable IP Address $FirstIPByteArray = $NetworkId.GetAddressBytes() [Array]::Reverse($FirstIPByteArray) # Used for determining last usable IP Address $LastIPByteArray = $Broadcast.GetAddressBytes() [Array]::Reverse($LastIPByteArray) # Handler for /31, /30 CIDR prefix values, and default for all others. switch ($PrefixLength) { 31 { $TotalIPs = 2 $UsableIPs = 2 $FirstIP = $NetworkId $LastIP = $Broadcast $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address $LastIPInt = ([IPAddress]$LastIPByteArray).Address break } 32 { $TotalIPs = 1 $UsableIPs = 1 $FirstIP = $IPAddress $LastIP = $IPAddress $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address $LastIPInt = ([IPAddress]$LastIPByteArray).Address break } default { # Usable Address Space $TotalIPs = [Math]::pow(2, (32 - $PrefixLength)) $UsableIPs = $TotalIPs - 2 # First usable IP $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address + 1 $FirstIP = [IPAddress]$FirstIPInt $FirstIP = ($FirstIP).GetAddressBytes() [Array]::Reverse($FirstIP) $FirstIP = [IPAddress]$FirstIP # Last usable IP $LastIPInt = ([IPAddress]$LastIPByteArray).Address - 1 $LastIP = [IPAddress]$LastIPInt $LastIP = ($LastIP).GetAddressBytes() [Array]::Reverse($LastIP) $LastIP = [IPAddress]$LastIP } } $AllIPs = if ($ReturnAllIPs) { if ($UsableIPs -ge 500000) { Write-Host ('[Warn] Generating an array containing {0:N0} IPs, this may take a little while' -f $UsableIPs) } $CurrentIPInt = $FirstIPInt Do { $IP = [IPAddress]$CurrentIPInt $IP = ($IP).GetAddressBytes() [Array]::Reverse($IP) | Out-Null $IP = ([IPAddress]$IP).IPAddressToString $IP $CurrentIPInt++ } While ($CurrentIPInt -le $LastIPInt) } $obj = [PSCustomObject]@{ NetworkId = ($NetworkId).IPAddressToString Broadcast = ($Broadcast).IPAddressToString SubnetMask = ($SubnetMask).IPAddressToString PrefixLength = $PrefixLength WildcardMask = ($WildcardMask).IPAddressToString FirstIP = ($FirstIP).IPAddressToString LastIP = ($LastIP).IPAddressToString TotalIPs = $TotalIPs UsableIPs = $UsableIPs AllIPs = $AllIPs } Write-Output $obj } } process { if (-not (Test-IsElevated)) { Write-Error -Message "Access Denied. Please run with Administrator privileges." exit 1 } if ($env:ipAddress -and $env:ipAddress -ne 'null') { $IpAddress = $env:ipAddress } if ($env:customFieldName -and $env:customFieldName -ne 'null') { $CustomFieldName = $env:customFieldName } # Parse the Addresses to check $Addresses = if ($IpAddress) { # Validate the IP Address $IpAddress -split ',' | ForEach-Object { "$_".Trim() } | ForEach-Object { if (($_ -as [System.Net.IPAddress])) { Write-Host "[Info] Valid IP Address: $_" [System.Net.IPAddress]::Parse($_) } elseif ($(Test-IPNetwork $_)) { Write-Host "[Info] Valid IP Network: $_" $Address, $PrefixLength = $_ -split '/' try { Get-IPNetwork -IPAddress $Address -PrefixLength $PrefixLength -ReturnAllIPs | Select-Object -ExpandProperty AllIPs } catch { Write-Host "[Error] Invalid IP CIDR: $_" exit 1 } } else { Write-Host "[Error] Invalid IP Address: $_" exit 1 } } } else { $null } # Get the open ports $FoundAddresses = $( Get-NetTCPConnection | Select-Object @( 'LocalAddress' 'LocalPort' @{Name = "RemoteAddress"; Expression = { if ($_.RemoteAddress) { $_.RemoteAddress }else { "None" } } } @{Name = "RemotePort"; Expression = { if ($_.RemotePort) { $_.RemotePort }else { "None" } } } 'State' @{Name = "Protocol"; Expression = { "TCP" } } 'OwningProcess' @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } ) Get-NetUDPEndpoint | Select-Object @( 'LocalAddress' 'LocalPort' @{Name = "RemoteAddress"; Expression = { "None" } } @{Name = "RemotePort"; Expression = { "None" } } @{Name = "State"; Expression = { "None" } } @{Name = "Protocol"; Expression = { "UDP" } } 'OwningProcess' @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } ) ) | Where-Object { $( <# When Addresses are specified select just those addresses. #> if ($Addresses) { $_.LocalAddress -in $Addresses -or $_.RemoteAddress -in $Addresses } else { $true } ) -and ( <# Filter out anything that isn't listening or established. #> $( $_.Protocol -eq "TCP" -and $( $_.State -eq "Listen" -or $_.State -eq "Established" ) ) -or <# UDP is stateless, return all UDP connections. #> $_.Protocol -eq "UDP" ) } | Sort-Object LocalAddress, RemoteAddress | Select-Object * -Unique if (-not $FoundAddresses -or $FoundAddresses.Count -eq 0) { Write-Host "[Info] No Addresses were found listening or established with the specified network or address" } # Output the found Addresses $FoundAddresses | ForEach-Object { Write-Host "[Alert] Found Local Address: $($_.LocalAddress), Local Port: $($_.LocalPort), Remote Address: $($_.RemoteAddress), Remote Port: $($_.RemotePort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Process: $($_.Process)" } # Save the results to a custom field if one was provided if ($CustomFieldName -and $CustomFieldName -ne 'null') { try { Write-Host "[Info] Saving results to custom field: $CustomFieldName" Set-NinjaProperty -Name $CustomFieldName -Value $( $FoundAddresses | ForEach-Object { "Local Address: $($_.LocalAddress), Local Port: $($_.LocalPort), Remote Address: $($_.RemoteAddress), Remote Port: $($_.RemotePort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Process: $($_.Process)" } | Out-String ) Write-Host "[Info] Results saved to custom field: $CustomFieldName" } catch { Write-Host $_.Exception.Message Write-Host "[Warn] Failed to save results to custom field: $CustomFieldName" exit 1 } } } end { }
Access over 300+ scripts in the NinjaOne Dojo
How the Script Works
1. Initial Setup
The script requires PowerShell version 5.1 or later and is intended for use on Windows 10 or Windows Server 2016 and newer. It begins by checking if the script is running with administrator privileges, which is necessary to access network connection information.
2. Parameter Parsing
The script accepts two primary parameters: -IpAddress and -CustomField. The -IpAddress parameter allows you to specify a list of IP addresses or CIDR-notated IP ranges to monitor. The -CustomField parameter is optional and is used to save the results to a NinjaOne custom field.
3. Address Validation
Once the parameters are provided, the script validates each IP address or network range. If a valid address or network is detected, it proceeds to retrieve all corresponding IPs within that range.
4. Monitoring Network Connections
The script uses Get-NetTCPConnection and Get-NetUDPEndpoint cmdlets to capture all current TCP and UDP connections on the system. It filters these connections to identify those in a ‘Listening’ or ‘Established’ state for TCP, and captures all UDP connections due to their stateless nature.
5. Output and Optional Saving
The identified connections are then displayed in a structured format, showing details such as local and remote addresses, ports, process IDs, protocols, and states. If a custom field is specified, the script saves these results for later analysis within NinjaOne.
Use Case: Real-World Application
Consider a scenario where an MSP manages the IT infrastructure of a financial services company. The company has strict security requirements, needing to monitor all incoming and outgoing connections for unauthorized access. The MSP deploys this script across all servers and endpoints, specifying the company’s internal IP ranges to ensure that only sanctioned devices are communicating with the network. If the script detects a connection from an unapproved IP, it alerts the administrator, who can then take immediate action to investigate and mitigate the risk.
Comparison with Other Methods
While other tools like Wireshark or Netstat can provide detailed insights into network traffic, this PowerShell script offers a streamlined and automated approach. Unlike Wireshark, which requires manual packet analysis, this script automatically alerts on specific connections, making it more accessible for ongoing monitoring. Compared to Netstat, the script adds value by filtering and reporting connections in a more structured and actionable format.
Frequently Asked Questions
Q: Can this script monitor IPv6 addresses?
A: Currently, the script only supports IPv4 addresses and networks. IPv6 support would require modifications to the script to handle the different address format.
Q: What happens if an invalid IP address is entered?
A: The script includes validation checks and will halt execution if an invalid IP address or network is detected, providing an error message to the user.
Q: How are the results saved in NinjaOne?
A: If the -CustomField parameter is provided, the script uses NinjaOne’s CLI to save the results into the specified custom field, ensuring the data is accessible for future reference or reporting.
Implications of the Results
The output from this script can have significant implications for IT security. By identifying and alerting on active connections, IT professionals can quickly detect unauthorized access, potential malware communications, or misconfigured services. Regular use of this script enhances visibility into network activities, contributing to a more secure and resilient IT environment.
Best Practices for Using the Script
- Run Regularly: Schedule the script to run at regular intervals, ensuring continuous monitoring of network connections.
- Integrate with NinjaOne: Leverage NinjaOne’s capabilities to store and analyze the results, enabling long-term monitoring and trend analysis.
- Customize for Your Environment: Modify the script as needed to accommodate specific IP ranges or additional logging requirements.
- Test in a Safe Environment: Before deploying in production, test the script in a controlled environment to ensure it operates as expected.
Final Thoughts
This PowerShell script offers a robust solution for monitoring TCP and UDP connections on Windows systems. By integrating it into your network monitoring toolkit, particularly within NinjaOne, you can achieve greater visibility and control over your IT environment. For IT professionals and MSPs, this script provides a practical approach to safeguarding network integrity, allowing for proactive responses to potential threats.