Vulnerability/Get-ExposedServices.ps1
|
function Get-ExposedServices { <# .SYNOPSIS Identifies internet-facing services and exposed network ports .DESCRIPTION Scans for services listening on public interfaces, checks firewall rules, identifies high-risk exposed services, and provides security recommendations. .PARAMETER IncludeAllPorts Scan all common ports (1-1024) instead of just high-risk ports .PARAMETER CustomPorts Additional custom ports to scan .PARAMETER CheckFirewall Analyze firewall rules for exposed services .PARAMETER OutputPath Path to save exposed services report .PARAMETER Format Report format: HTML, JSON, CSV, or All .EXAMPLE Get-ExposedServices Get-ExposedServices -IncludeAllPorts -CheckFirewall -OutputPath "C:\Reports" .OUTPUTS PSCustomObject with exposed services findings #> [CmdletBinding()] param( [Parameter()] [switch]$IncludeAllPorts, [Parameter()] [int[]]$CustomPorts, [Parameter()] [switch]$CheckFirewall, [Parameter()] [string]$OutputPath, [Parameter()] [ValidateSet('HTML', 'JSON', 'CSV', 'All')] [string]$Format = 'HTML' ) begin { Write-Host "=== EXPOSED SERVICES SCAN ===" -ForegroundColor Cyan Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow Write-Host "Scan Mode: $(if($IncludeAllPorts){'Full Port Scan'}else{'High-Risk Ports Only'})" -ForegroundColor Yellow Write-Host "Check Firewall: $(if($CheckFirewall){'Yes'}else{'No'})" -ForegroundColor Yellow Write-Host "Custom Ports: $(if($CustomPorts){$CustomPorts -join ', '}else{'None'})" -ForegroundColor Yellow $findings = [PSCustomObject]@{ ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME PublicIP = $null LocalIPs = @() ListeningPorts = @() ExposedServices = @() FirewallRules = @() Summary = @{ TotalListening = 0 HighRisk = 0 MediumRisk = 0 LowRisk = 0 FirewallAllowRules = 0 } Recommendations = @() } # High-risk ports database $highRiskPorts = @{ 21 = @{Service='FTP'; Risk='High'; Description='Unencrypted file transfer'} 22 = @{Service='SSH'; Risk='Medium'; Description='Secure shell - requires strong auth'} 23 = @{Service='Telnet'; Risk='Critical'; Description='Unencrypted remote access'} 25 = @{Service='SMTP'; Risk='Medium'; Description='Mail server - spam relay risk'} 53 = @{Service='DNS'; Risk='Medium'; Description='DNS server - DDoS amplification'} 80 = @{Service='HTTP'; Risk='Medium'; Description='Web server - unencrypted'} 110 = @{Service='POP3'; Risk='Medium'; Description='Mail retrieval - unencrypted'} 135 = @{Service='RPC'; Risk='High'; Description='Windows RPC - attack vector'} 139 = @{Service='NetBIOS'; Risk='High'; Description='NetBIOS - information disclosure'} 143 = @{Service='IMAP'; Risk='Medium'; Description='Mail access - unencrypted'} 443 = @{Service='HTTPS'; Risk='Low'; Description='Secure web server'} 445 = @{Service='SMB'; Risk='Critical'; Description='File sharing - ransomware vector'} 1433 = @{Service='SQL Server'; Risk='High'; Description='Database - data breach risk'} 1434 = @{Service='SQL Monitor'; Risk='High'; Description='SQL Server Discovery'} 3306 = @{Service='MySQL'; Risk='High'; Description='Database - data breach risk'} 3389 = @{Service='RDP'; Risk='Critical'; Description='Remote Desktop - brute-force target'} 5432 = @{Service='PostgreSQL'; Risk='High'; Description='Database - data breach risk'} 5900 = @{Service='VNC'; Risk='High'; Description='Remote desktop - weak auth'} 8080 = @{Service='HTTP-Alt'; Risk='Medium'; Description='Alternative web server'} 8443 = @{Service='HTTPS-Alt'; Risk='Low'; Description='Alternative secure web'} } # Merge user-specified custom ports into the assessed port database if ($CustomPorts) { foreach ($customPort in $CustomPorts) { if (-not $highRiskPorts.ContainsKey($customPort)) { $highRiskPorts[$customPort] = @{Service='Custom'; Risk='Medium'; Description='User-specified custom port'} } } } } process { try { # 1. GET NETWORK INFORMATION Write-Host "`n[1/4] Gathering network information..." -ForegroundColor Cyan # Get local IP addresses try { $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceAlias -notlike '*Loopback*' } foreach ($adapter in $adapters) { $findings.LocalIPs += [PSCustomObject]@{ IPAddress = $adapter.IPAddress Interface = $adapter.InterfaceAlias PrefixLength = $adapter.PrefixLength } Write-Host " Local IP: $($adapter.IPAddress) ($($adapter.InterfaceAlias))" -ForegroundColor Gray } } catch { Write-Warning "Could not retrieve network adapters" } # Ensure TLS 1.2 on Windows PowerShell 5.1 (idempotent, only if the type exists) try { if ($null -ne ([Net.SecurityProtocolType] -as [type])) { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } } catch { Write-Verbose "Could not enforce TLS 1.2: $_" } # Try to get public IP try { Write-Host " Detecting public IP..." -ForegroundColor Gray $publicIP = (Invoke-WebRequest -Uri "https://api.ipify.org" -UseBasicParsing -TimeoutSec 5).Content $findings.PublicIP = $publicIP Write-Host " Public IP: $publicIP" -ForegroundColor Green } catch { Write-Host " Could not detect public IP (offline or blocked)" -ForegroundColor Gray } # 2. SCAN LISTENING PORTS Write-Host "`n[2/4] Scanning listening ports..." -ForegroundColor Cyan # Get listening TCP connections try { $connections = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue foreach ($conn in $connections) { $localPort = $conn.LocalPort # Get process info $process = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue $processName = if ($process) { $process.ProcessName } else { 'Unknown' } $processPath = if ($process) { $process.Path } else { 'Unknown' } # Check if it's a high-risk port $portInfo = $highRiskPorts[$localPort] $serviceName = if ($portInfo) { $portInfo.Service } else { 'Unknown' } $riskLevel = if ($portInfo) { $portInfo.Risk } else { 'Low' } $description = if ($portInfo) { $portInfo.Description } else { 'Custom service' } # Determine if exposed to internet $isExposed = ($conn.LocalAddress -eq '0.0.0.0' -or $conn.LocalAddress -eq '::') $portEntry = [PSCustomObject]@{ Port = $localPort Service = $serviceName Process = $processName ProcessPath = $processPath PID = $conn.OwningProcess LocalAddress = $conn.LocalAddress IsExposed = $isExposed Risk = $riskLevel Description = $description } $findings.ListeningPorts += $portEntry # If exposed and high/critical risk, add to exposed services if ($isExposed -and $riskLevel -in @('Critical', 'High', 'Medium')) { $findings.ExposedServices += $portEntry $color = switch ($riskLevel) { 'Critical' { 'Red' } 'High' { 'Yellow' } 'Medium' { 'Gray' } default { 'Gray' } } Write-Host " [!] $riskLevel - Port $localPort ($serviceName) - $processName" -ForegroundColor $color } } $findings.Summary.TotalListening = $findings.ListeningPorts.Count Write-Host " Found $($findings.Summary.TotalListening) listening ports" -ForegroundColor Green } catch { Write-Warning "Could not scan TCP connections - $_" } # FIXED: Count risk levels with safe array wrapping $findings.Summary.HighRisk = @($findings.ExposedServices | Where-Object { $_.Risk -in @('Critical', 'High') }).Count $findings.Summary.MediumRisk = @($findings.ExposedServices | Where-Object { $_.Risk -eq 'Medium' }).Count $findings.Summary.LowRisk = @($findings.ExposedServices | Where-Object { $_.Risk -eq 'Low' }).Count # 3. CHECK FIREWALL RULES if ($CheckFirewall) { Write-Host "`n[3/4] Analyzing firewall rules..." -ForegroundColor Cyan try { $fwRules = Get-NetFirewallRule -Enabled True -Direction Inbound -ErrorAction SilentlyContinue | Where-Object { $_.Action -eq 'Allow' } foreach ($rule in $fwRules) { # Get port filter $portFilter = Get-NetFirewallPortFilter -AssociatedNetFirewallRule $rule -ErrorAction SilentlyContinue $addressFilter = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $rule -ErrorAction SilentlyContinue if ($portFilter -and $portFilter.LocalPort) { $localPorts = $portFilter.LocalPort $protocol = $portFilter.Protocol $remoteAddress = if ($addressFilter) { $addressFilter.RemoteAddress } else { 'Any' } # Check if allows from internet $allowsInternet = ($remoteAddress -eq 'Any' -or $remoteAddress -contains 'Internet') if ($allowsInternet) { $ruleEntry = [PSCustomObject]@{ Name = $rule.Name DisplayName = $rule.DisplayName Port = $localPorts Protocol = $protocol RemoteAddress = $remoteAddress Profile = $rule.Profile } $findings.FirewallRules += $ruleEntry $findings.Summary.FirewallAllowRules++ Write-Host " [*] Firewall allows: $($rule.DisplayName) - Port $localPorts" -ForegroundColor Gray } } } Write-Host " Found $($findings.Summary.FirewallAllowRules) allow rules from internet" -ForegroundColor $( if ($findings.Summary.FirewallAllowRules -gt 10) { 'Yellow' } else { 'Green' } ) } catch { Write-Warning "Could not analyze firewall rules - $_" } } else { Write-Host "`n[3/4] Skipping firewall analysis (disabled)" -ForegroundColor Gray } # 4. GENERATE RECOMMENDATIONS Write-Host "`n[4/4] Generating recommendations..." -ForegroundColor Cyan # Check for critical services $criticalServices = @($findings.ExposedServices | Where-Object { $_.Risk -eq 'Critical' }) if ($criticalServices.Count -gt 0) { foreach ($svc in $criticalServices) { $findings.Recommendations += "CRITICAL: Port $($svc.Port) ($($svc.Service)) is exposed - block immediately or use VPN" } } # Check for RDP $rdp = $findings.ExposedServices | Where-Object { $_.Port -eq 3389 } if ($rdp) { $findings.Recommendations += "CRITICAL: Remote Desktop (3389) exposed - use VPN or change to non-standard port with strong auth" } # Check for SMB $smb = $findings.ExposedServices | Where-Object { $_.Port -eq 445 } if ($smb) { $findings.Recommendations += "CRITICAL: SMB (445) exposed - major ransomware risk, block immediately" } # Check for databases $databases = @($findings.ExposedServices | Where-Object { $_.Port -in @(1433, 3306, 5432) }) if ($databases.Count -gt 0) { $findings.Recommendations += "HIGH: Database port(s) exposed - restrict to application servers only" } # Check for management ports $mgmt = @($findings.ExposedServices | Where-Object { $_.Port -in @(22, 23, 135, 5900) }) if ($mgmt.Count -gt 0) { $findings.Recommendations += "HIGH: Management protocol exposed - use VPN or IP whitelisting" } # General recommendations if ($findings.ExposedServices.Count -eq 0) { $findings.Recommendations += "No high-risk services exposed - maintain current security posture" } $findings.Recommendations += "Use VPN for remote access instead of direct port exposure" $findings.Recommendations += "Implement IP whitelisting for necessary exposed services" $findings.Recommendations += "Enable Network Level Authentication for RDP if required" $findings.Recommendations += "Use reverse proxy (nginx/Cloudflare) for web services" $findings.Recommendations += "Monitor exposed services with IDS/IPS" $findings.Recommendations += "Conduct quarterly port scans from external perspective" $findings.Recommendations += "Use fail2ban or similar for brute-force protection" # 5. EXPORT REPORTS if ($OutputPath) { Write-Host "`n[*] Exporting exposed services reports..." -ForegroundColor Cyan if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $reportName = "ExposedServices_$timestamp" # Export JSON if ($Format -in @('JSON', 'All')) { $jsonPath = Join-Path $OutputPath "$reportName.json" $findings | ConvertTo-Json -Depth 10 | Out-File $jsonPath -Encoding UTF8 Write-Host " JSON report saved: $jsonPath" -ForegroundColor Green } # Export CSV if ($Format -in @('CSV', 'All')) { $csvPath = Join-Path $OutputPath "$reportName.csv" $findings.ExposedServices | Export-Csv $csvPath -NoTypeInformation Write-Host " CSV report saved: $csvPath" -ForegroundColor Green } # Export HTML if ($Format -in @('HTML', 'All')) { $htmlPath = Join-Path $OutputPath "$reportName.html" $html = Generate-ExposedServicesHTML -Findings $findings $html | Out-File $htmlPath -Encoding UTF8 Write-Host " HTML report saved: $htmlPath" -ForegroundColor Green } } } catch { Write-Error "Error during exposed services scan - $_" throw } } end { Write-Host "`n=== EXPOSED SERVICES SCAN COMPLETE ===" -ForegroundColor $( if ($findings.Summary.HighRisk -gt 0) { 'Red' } elseif ($findings.Summary.MediumRisk -gt 0) { 'Yellow' } else { 'Green' } ) Write-Host "`nSummary:" -ForegroundColor Cyan Write-Host " Total Listening Ports: $($findings.Summary.TotalListening)" -ForegroundColor White Write-Host " Exposed Services: $($findings.ExposedServices.Count)" -ForegroundColor $( if ($findings.ExposedServices.Count -gt 0) { 'Yellow' } else { 'Green' } ) Write-Host " Critical/High Risk: $($findings.Summary.HighRisk)" -ForegroundColor $( if ($findings.Summary.HighRisk -gt 0) { 'Red' } else { 'Gray' } ) Write-Host " Medium Risk: $($findings.Summary.MediumRisk)" -ForegroundColor Gray Write-Host " Low Risk: $($findings.Summary.LowRisk)" -ForegroundColor Gray if ($CheckFirewall) { Write-Host " Firewall Allow Rules: $($findings.Summary.FirewallAllowRules)" -ForegroundColor Gray } if ($findings.ExposedServices.Count -gt 0) { Write-Host "`nExposed High-Risk Services:" -ForegroundColor Cyan $highRiskExposed = @($findings.ExposedServices | Where-Object { $_.Risk -in @('Critical', 'High') }) if ($highRiskExposed.Count -gt 0) { $highRiskExposed | Sort-Object { switch ($_.Risk) { 'Critical' { 0 } 'High' { 1 } default { 2 } } } | Select-Object -First 10 | ForEach-Object { $color = switch ($_.Risk) { 'Critical' { 'Red' } 'High' { 'Yellow' } default { 'Gray' } } Write-Host " [$($_.Risk)] Port $($_.Port) - $($_.Service) ($($_.Process))" -ForegroundColor $color } } } if ($findings.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Cyan $findings.Recommendations | Select-Object -First 10 | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } } return $findings } } # Helper function to generate HTML report function Generate-ExposedServicesHTML { param($Findings) $riskColor = if($Findings.Summary.HighRisk -gt 0){'critical'} elseif($Findings.Summary.MediumRisk -gt 0){'medium'} else{'safe'} $html = @" <!DOCTYPE html> <html> <head> <title>Exposed Services Report - $($Findings.ComputerName)</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; } .container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } h1 { color: #2c3e50; border-bottom: 3px solid #e74c3c; padding-bottom: 10px; } h2 { color: #34495e; margin-top: 30px; } .summary { background: #ecf0f1; padding: 20px; border-radius: 5px; margin: 20px 0; } .metric { display: inline-block; margin: 10px 20px 10px 0; } .metric-label { font-weight: bold; color: #7f8c8d; } .metric-value { font-size: 24px; font-weight: bold; } .critical { color: #e74c3c; } .high { color: #e67e22; } .medium { color: #f39c12; } .low { color: #95a5a6; } .safe { color: #27ae60; } table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 14px; } th { background: #e74c3c; color: white; padding: 12px; text-align: left; } td { padding: 10px; border-bottom: 1px solid #ddd; vertical-align: top; } tr:hover { background: #f8f9fa; } .risk-badge { padding: 4px 8px; border-radius: 3px; color: white; font-weight: bold; font-size: 12px; } .risk-critical { background: #e74c3c; } .risk-high { background: #e67e22; } .risk-medium { background: #f39c12; } .risk-low { background: #95a5a6; } .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #7f8c8d; font-size: 12px; } .no-exposure { color: #27ae60; font-weight: bold; font-size: 16px; padding: 20px; text-align: center; } .network-info { background: #e8f4f8; padding: 15px; border-radius: 5px; margin: 10px 0; } </style> </head> <body> <div class="container"> <h1>Exposed Services Security Report</h1> <p><strong>Computer:</strong> $($Findings.ComputerName) | <strong>Scan Date:</strong> $($Findings.ScanDate)</p> <div class="network-info"> <h3>Network Information</h3> <p><strong>Public IP:</strong> $(if($Findings.PublicIP){$Findings.PublicIP}else{'Not detected'})</p> <p><strong>Local IPs:</strong> $(($Findings.LocalIPs | ForEach-Object { "$($_.IPAddress) ($($_.Interface))" }) -join ', ')</p> </div> <div class="summary"> <h2>Summary</h2> <div class="metric"> <div class="metric-label">Listening Ports</div> <div class="metric-value">$($Findings.Summary.TotalListening)</div> </div> <div class="metric"> <div class="metric-label">Exposed Services</div> <div class="metric-value $(if($Findings.ExposedServices.Count -gt 0){'critical'}else{'safe'})"> $($Findings.ExposedServices.Count) </div> </div> <div class="metric"> <div class="metric-label">Critical/High Risk</div> <div class="metric-value critical">$($Findings.Summary.HighRisk)</div> </div> <div class="metric"> <div class="metric-label">Medium Risk</div> <div class="metric-value medium">$($Findings.Summary.MediumRisk)</div> </div> </div> <h2>Exposed Services</h2> "@ if ($Findings.ExposedServices.Count -gt 0) { $html += @" <table> <tr> <th style="width: 80px;">Risk</th> <th style="width: 80px;">Port</th> <th style="width: 120px;">Service</th> <th style="width: 150px;">Process</th> <th>Description</th> <th style="width: 150px;">Local Address</th> </tr> "@ foreach ($service in ($Findings.ExposedServices | Sort-Object { switch ($_.Risk) { 'Critical' { 0 } 'High' { 1 } 'Medium' { 2 } 'Low' { 3 } } })) { $riskClass = "risk-$($service.Risk.ToLower())" $desc = $service.Description -replace '<', '<' -replace '>', '>' $html += @" <tr> <td><span class="risk-badge $riskClass">$($service.Risk)</span></td> <td><strong>$($service.Port)</strong></td> <td>$($service.Service)</td> <td>$($service.Process)</td> <td>$desc</td> <td>$($service.LocalAddress)</td> </tr> "@ } $html += " </table>`n" } else { $html += " <p class=`"no-exposure`">No high-risk services exposed to internet</p>`n" } $html += " `n <h2>Recommendations</h2>`n <ul>`n" foreach ($rec in $Findings.Recommendations) { $recEscaped = $rec -replace '<', '<' -replace '>', '>' $html += " <li>$recEscaped</li>`n" } $currentDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $html += @" </ul> <div class="footer"> Generated by WindowsSecurityAudit Module | Exposed Services Scan | $currentDate </div> </div> </body> </html> "@ return $html } |