En el panorama en constante evolución de la seguridad informática, mantener la visibilidad de las conexiones de red es primordial. Tanto si gestionas una gran red empresarial como si supervisas una pequeña o mediana empresa, es crucial saber qué direcciones IP se comunican activamente con tus sistemas.
Este post profundiza en un script PowerShell especializado diseñado para alertar a los administradores de direcciones IP específicas que están escuchando o en un estado establecido. Esta herramienta tiene un valor incalculable para los profesionales de TI, los proveedores de servicios gestionados (MSP) y los equipos de seguridad que deseen reforzar sus capacidades de supervisión de la red. El siguiente contenido explica cómo supervisar conexiones TCP y UDP en Windows usando PowerShell.
Comprender el script PowerShell
El script proporcionado sirve como mecanismo de alerta para conexiones de red, identificando direcciones IP especificadas en estado ‘Escuchando’ o ‘Establecido’. Va más allá de las comprobaciones básicas del firewall y ofrece información sobre las conexiones activas que pueden eludir los filtros de seguridad tradicionales. El script muestra detalles esenciales como la dirección, el ID del proceso, el estado, el protocolo, la dirección local y el nombre del proceso. Para quienes utilicen NinjaOne, los resultados pueden guardarse automáticamente en un campo personalizado para su posterior análisis.
Importancia para los profesionales de TI y los MSP
En el entorno digital actual, en el que las ciberamenazas están siempre presentes, es esencial tener un control granular de las conexiones de red. Este script responde a la necesidad de supervisar en tiempo real las actividades de la red, lo que permite a los profesionales de TI responder rápidamente a posibles amenazas para la seguridad. Para los MSP que gestionan redes de varios clientes, este script ofrece un método estandarizado para supervisar e informar sobre la actividad de la red, lo que garantiza una supervisión coherente y exhaustiva.
El script para supervisar conexiones TCP y UDP
#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://ninjastage2.wpengine.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 { }
Cómo funciona el script
1. Configuración inicial
El script requiere la versión 5.1 o posterior de PowerShell y está pensado para su uso en Windows 10 o Windows Server 2016 y versiones posteriores. Comienza comprobando si el script se está ejecutando con privilegios de administrador, necesarios para acceder a la información de la conexión de red.
2. Análisis sintáctico de parámetros
El script acepta dos parámetros principales: -IpAddress y -CustomField. El parámetro -IpAddress permite especificar una lista de direcciones IP o rangos de IP anotados con CIDR para monitorizar. El parámetro -CustomField es opcional y se utiliza para guardar los resultados en un campo personalizado de NinjaOne.
3. Validación de direcciones
Una vez proporcionados los parámetros, el script valida cada dirección IP o rango de red. Si detecta una dirección o red válida, procede a recuperar todas las IP correspondientes dentro de ese rango.
4. Supervisión de las conexiones de red
El script utiliza los cmdlets Get-NetTCPConnection y Get-NetUDPEndpoint para capturar todas las conexiones TCP y UDP actuales del sistema. Filtra estas conexiones para identificar aquellas en estado «Escuchando» o «Establecido» para TCP, y captura todas las conexiones UDP debido a su naturaleza sin estado.
5. Salida y ahorro opcional
Las conexiones identificadas se muestran entonces en un formato estructurado, mostrando detalles como direcciones locales y remotas, puertos, ID de procesos, protocolos y estados. Si se especifica un campo personalizado, el script guarda estos resultados para su posterior análisis dentro de NinjaOne.
Caso práctico: aplicaciones reales
Consideremos un escenario en el que un MSP gestiona la infraestructura de TI de una empresa de servicios financieros. La empresa tiene estrictos requisitos de seguridad y necesita supervisar todas las conexiones entrantes y salientes para evitar accesos no autorizados. El MSP despliega este script en todos los servidores y endpoints, especificando los rangos de IP internos de la empresa para garantizar que sólo los dispositivos autorizados se comunican con la red. Si el script detecta una conexión desde una IP no autorizada, alerta al administrador, que puede tomar medidas inmediatas para investigar y mitigar el riesgo.
Comparación con otros métodos
Mientras que otras herramientas como Wireshark o Netstat pueden proporcionar información detallada sobre el tráfico de red, este script de PowerShell ofrece un enfoque simplificado y automatizado. A diferencia de Wireshark, que requiere un análisis manual de los paquetes, este script alerta automáticamente sobre conexiones específicas, lo que lo hace más accesible para una supervisión continua. Comparado con Netstat, el script añade valor filtrando e informando de las conexiones en un formato más estructurado y procesable.
Preguntas frecuentes
P: ¿Este script monitorizar direcciones IPv6?
R: Actualmente, el script sólo admite direcciones y redes IPv4. La compatibilidad con IPv6 requeriría modificaciones en el script para gestionar el diferente formato de las direcciones.
P: ¿Qué ocurre si se introduce una dirección IP inválida?
R: El script incluye comprobaciones de validación y detendrá la ejecución si se detecta una dirección IP o una red no válidas, proporcionando un mensaje de error al usuario.
P: ¿Cómo se guardan los resultados en NinjaOne?
R: Si se proporciona el parámetro -CustomField, el script utiliza la CLI de NinjaOne para guardar los resultados en el campo personalizado especificado, asegurando que los datos sean accesibles para futuras referencias o informes.
Implicaciones de los resultados
El resultado de este script puede tener implicaciones significativas para la seguridad informática. Al identificar y alertar sobre las conexiones activas, los profesionales de TI pueden detectar rápidamente accesos no autorizados, posibles comunicaciones de malware o servicios mal configurados. El uso regular de este script mejora la visibilidad de las actividades de la red, contribuyendo a un entorno informático más seguro y resistente.
Buenas prácticas para utilizar el script
- Ejecuta el script con regularidad: programa el script para que se ejecute a intervalos regulares, garantizando así la supervisión continua de las conexiones de red.
- Intégralo con NinjaOne: aprovecha las capacidades de NinjaOne para almacenar y analizar los resultados, lo que permite la supervisión a largo plazo y el análisis de tendencias.
- Adáptalo a tu entorno: modifica el script según sea necesario para adaptarlo a rangos de IP específicos o a requisitos de registro adicionales.
- Prueba en un entorno seguro: antes de desplegarlo en producción, prueba el script en un entorno controlado para asegurarte de que funciona como debería.
Reflexiones finales
Este script PowerShell ofrece una solución robusta para supervisar conexiones TCP y UDP en sistemas Windows. Al integrarlo en tu conjunto de herramientas de monitorización de red, en particular dentro de NinjaOne, puedes lograr una mayor visibilidad y control sobre tu entorno de TI. Para los profesionales de TI y MSP, este script proporciona un enfoque práctico para salvaguardar la integridad de la red, permitiendo respuestas proactivas a las amenazas potenciales.