How to scan SSL/TLS sites.
The other day, I hit a conundrum.
We couldn't make LDAPS connections to a couple of domain controllers. A quick "TS" over to the systems in question indicated that we had a correct certificate in place, and that it was valid, but when we connected using "LDP" over port 636, we would be told that the certificate exchange wasn't allowed to finish.
A look at the event viewer showed us that the certificate had expired. That's ludicrous, though because the certificates show in the certificate manager as valid.
The only thing we could figure out is that we must have been running into the problem specified in KB 321051 - "How to enable LDAP over SSL..." - or at least, the last issue on "Pre-SP3 SSL certificate caching issue". Sure, we're on SP4, but that's the only thing we could figure out.
To test it further, we had to view the certificate, and the brutal method we chose was to open up Internet Explorer and open https://192.168.0.1:636 - where "192.168.0.1" was the address of the DC whose certificate we wanted to view.
An error comes up on screen when you do this, because the IP address doesn't match the name. You want this error, because it allows you to press "View Certificate".
Sure enough, the certificate that came up was old, and expired. And, just as the article assured us we shouldn't have to do on a Windows 2000 SP4 box, a re-boot fixed the server to using the right certificate.
If you're thinking like we were, your next though will be "gee, I wonder how many of our other servers have this problem?" Obviously, we would be hard-pressed to scan all of our servers using an Internet Explorer error dialog, so it was time to write a program to do it for us.
Less than an hour later, I had the program I like to call "SSLScan".
Sure, it's in C# .NET 2.0, but it's probably the shortest piece of SSL code I've written that wasn't completely densely obfuscated. using System;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.Collections;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace SSLScan
{
class Program
{
static int port = 443; // We choose to default to HTTPS.
// Other examples - LDAPS is 636.
static void Main(string[] args)
{
//
// SSLScan - scan a list of SSL servers, connecting to each
// in turn, and asking for their certificate.
// Then display certificate information.
//
// We assume that each server responds to an SSL ClientHello
// immediately upon connection - this will not work with some
// SSL servers, for instance FTPS servers, that require some
// sort of command(s) to be issued prior to sending the SSL
// negotiation.
//
// For now, the only argument is the name of the file used to list
// the hosts - or you can feed it as stdin.
//
switch (args.Length)
{
case 0:
Usage();
Console.Error.WriteLine("Reading from keyboard");
TestConnectionsFromStream(new System.IO.StreamReader(
System.Console.OpenStandardInput()));
break;
case 1:
Console.Error.WriteLine("Reading from file {0}", args[0]);
TestConnectionsFromStream(new System.IO.StreamReader(args[0]));
break;
default:
Usage();
break;
}
}
private static void TestConnectionsFromStream(
System.IO.StreamReader streamReader)
{
string server;
bool blank = false;
bool interactive = !streamReader.BaseStream.CanSeek;
char[] delimiters = { ':' };
string[] elements;
do {
if (blank)
Console.WriteLine();
if (interactive)
Console.Error.Write("\nServerName[:PortNumber] > ");
server = streamReader.ReadLine();
if (server == null || // end of file - no more.
(interactive && server.Length==0)) // Interactive - blank line ends.
break;
if (server.StartsWith("//"))
continue; // ignore comments - go to next server.
if (server.Contains(":"))
{
elements = server.Split(delimiters);
server = elements[0];
port = Convert.ToInt32(elements[1]);
}
try
{
TestConnectionToSite(server,port);
Console.WriteLine();
}
catch (Exception e)
{
Console.Error.WriteLine("Exception on site {0}", server);
Console.Error.WriteLine(e.Message);
}
blank = true;
} while (true); // Forever - or until "break" hit.
}
public static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
Console.WriteLine("Subject: {0}",certificate.Subject);
Console.WriteLine("Issuer : {0}",certificate.Issuer);
Console.WriteLine("Serial : {0}",certificate.GetSerialNumberString());
Console.WriteLine("Expires: {0}", certificate.GetExpirationDateString());
X509Certificate2 cert2 = new X509Certificate2(certificate);
if (cert2.NotAfter < DateTime.Now)
Console.WriteLine("*** Certificate has expired!!!");
else if (cert2.NotAfter.AddDays(-30.0) < DateTime.Now)
Console.WriteLine("*** Certificate expires in thirty days or less!!!");
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
Console.WriteLine("Certificate error: {0}", sslPolicyErrors);
// Reject the SSL connection
return false;
}
private static void TestConnectionToSite(string serverName, int port)
{
Console.Error.WriteLine("Connecting to server: " + serverName + ":" + port.ToString());
TcpClient client = new TcpClient(serverName, port);
Console.Error.WriteLine("Client connected.");
// Create an SSL stream that will enclose the client's stream.
SslStream sslStream = new SslStream(
client.GetStream(),
false,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
null
);
// The server name must match the name on the server certificate.
try
{
sslStream.AuthenticateAsClient(serverName);
}
catch (AuthenticationException e)
{
// Uncomment this lot of code if you want to see the exceptions.
// Console.WriteLine("Exception: {0}", e.Message);
// if (e.InnerException != null)
// {
// Console.WriteLine("Inner exception: {0}", e.InnerException.Message);
// }
// Console.WriteLine("Authentication failed - closing the connection.");
client.Close();
return;
}
client.Close();
}
static void Usage()
{
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(
"SSLScan [filename] - if filename is blank, reads from stdin.");
Console.Error.WriteLine(
"Scans multiple systems with an SSL connection," +
" listing the certificates.");
Console.Error.WriteLine(
"Sites are specified as host:port - the ':port' part" +
" is optional, and if not");
Console.Error.WriteLine(
"specified, will default to the previous port value," +
" or the default of " +
port.ToString());
}
}
}