Skip to content

Instantly share code, notes, and snippets.

@JohnLBevan
Created September 2, 2021 10:55
Show Gist options
  • Save JohnLBevan/1d59845a30fed5c700c03dc31bab3ca9 to your computer and use it in GitHub Desktop.
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
# 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