Created
September 2, 2021 10:55
-
-
Save JohnLBevan/1d59845a30fed5c700c03dc31bab3ca9 to your computer and use it in GitHub Desktop.
Certificate discovery script; basically a port scanner which checks for an ssl connection and pulls back the certificate info where available
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# See also https://gist.github.com/JohnLBevan/a6c819ba5d825f29d465fb0433b54082 -- useful for fetching a range of IPs to be scanned | |
Function Get-TlsCertificate { | |
# Note: This uses PS7's parallel foreach feature; so requires PS7. If we wanted to write a version compatible with older versions we could use workflows (e.g. https://codereview.stackexchange.com/questions/97726/powershell-to-quickly-ping-a-number-of-machines) or add custom job/runspace logic (though that's much less tidy) | |
[CmdletBinding(DefaultParameterSetName = 'ByComputerAndPort')] | |
Param ( | |
[Parameter(ParameterSetName = 'ByComputerAndPort', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] | |
[string[]]$ComputerName | |
, | |
[Parameter(ParameterSetName = 'ByComputerAndPort', ValueFromPipelineByPropertyName)] | |
[int[]]$Port = @(443, 465, 990) # HTTPS, SMTPS (implicit), FTPS (implicit)... Note; explicit SSL connections won't work with the current approach, since those require negotiation between making the connection and getting the sslstream | |
, | |
# This takes a computername (ip/fqdn/etc) and port as above, but also accepts an SNI property, which may differ from the computername, allowing multisite listners to perform hostname resolution | |
# Could improve by using a bespoke type here, or adding validation | |
[Parameter(ParameterSetName = 'BySniObject', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] | |
[PSCustomObject[]]$SniObject | |
, | |
[Parameter()] | |
[int]$ThrottleLimit = 30 | |
) | |
Begin { | |
# The begin and process blocks just build up a list of all the resources to be scanned; the end block does the heavy lifting (implemented this way to make most efficient use of `-parallel` | |
[System.Collections.Generic.List[PSCustomObject]]$scanTargets = [System.Collections.Generic.List[PSCustomObject]]::new() | |
} | |
Process { | |
if ($PSCmdlet.ParameterSetName -eq 'ByComputerAndPort') { | |
foreach ($cn in $ComputerName) { | |
foreach ($p in $Port) { | |
$scanTargets.Add(([PSCustomObject]@{ComputerName = $cn; Port = $p; Sni = $cn})) | |
} | |
} | |
} else { | |
$scanTargets.AddRange($SniObject) | |
} | |
} | |
End { | |
[System.Security.Authentication.SslProtocols]$allSslProtocols = [System.Security.Authentication.SslProtocols]::GetValues([System.Security.Authentication.SslProtocols]) | |
$ScanTargets | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { | |
Write-Verbose "Processing $_" | |
$target = $_ | |
[PSCustomObject]$result = ([PSCustomObject][Ordered]@{ | |
ComputerName = $target.ComputerName | |
RemotePort = $target.Port | |
SniName = $target.Sni | |
TcpTestSucceeded = $false | |
Certificate = $null | |
Exception = $null | |
}) | |
[ScriptBlock]$callback = { param($sender, $certificate, $chain, $sslPolicyErrors) $true } # https://docs.microsoft.com/en-us/dotnet/api/system.net.security.remotecertificatevalidationcallback?view=net-5.0 | |
try { | |
[System.Net.Sockets.TcpClient]$client = [System.Net.Sockets.TcpClient]::new() | |
$client.Connect($target.ComputerName, $target.Port) | |
$result.TcpTestSucceeded = $client.Connected | |
[System.Net.Security.SslStream]$stream = [System.Net.Security.SslStream]::new($client.GetStream(), $false, $callback) # $false here means the inner stream is disposed of along with our ssl wrapper stream | |
$stream.AuthenticateAsClient($target.Sni.ToString(), $null, $using:allSslProtocols, $false) # $null is our client certs list. $false is whether to check for certificate revocation. Calling ToString just to ensure any user passed content is of the correct type | |
if ($null -ne $stream.RemoteCertificate) { | |
$result.Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $stream.RemoteCertificate ) | |
} | |
} catch { # may want to improve by only catching certain types of error here... | |
$result.Exception = $_.Exception | |
} finally { | |
$result | |
if ($null -ne $stream) {$stream.Dispose(); $stream = $null;} | |
if ($null -ne $client) {$client.Dispose(); $client = $null;} # note: older versions of .net don't support Dispose... Hopefully we don't have any of these anymore; but if you get an error, check if the dispose method exists on your version & consider switching to Close if Dispose is unnavailable | |
Write-Verbose "Processed $_" | |
} | |
} | |
} | |
} | |
[string[]]$ips = '172.24.72.0/24' | Covert-CidrToIpV4List | % IPAddressToString | |
[PSCustomObject[]]$results = Get-TlsCertificate -ComputerName $ips | |
$results | ? TcpTestSucceeded | Select-Object ComputerName, RemotePort, TcpTestSucceeded, @{N='CertThumb';E={$_.Certificate.Thumbprint}}, @{N='CertSubj';E={$_.Certificate.Subject}}, @{N='CertExpires';E={$_.Certificate.NotAfter}}, @{N='Exception';E={$_.Exception.Message}} | ft -AutoSize | |
# potential improvements | |
# consider writing something to do reverse dns lookups on any ips/computer names so we can get a list of hostnames (where ptrs exist) to try with the sni piece | |
# work out how to discover sites under a domain querying dns for a list of names to check (maybe we can pull from DNS where we have access to AXFR; don't know enough about this area, otherwise https://geekflare.com/find-subdomains/ gives some workaround solutions; but there's nothing perfect / guaranteed to give all subdomains) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment