I'm trying to create a local windows MITM proxy with C# to deal with a now unsupported application from a company that no longer exists.
The Proxy has to only service one HTTPS domain which is being done by creating a proxy that listens on the local address:port 127.0.0.1:443.
An entry in the host file is then created i.e. 127.0.0.1 my.single.domain.com.
When adding an entry for the domain directly into my hosts file, I do not get the normal "CONNECT" type of HTTP request, but rather on the socket I receive a direct client hello and I can see that the next step is initiate a handshake.
However, I'm unsure how to handle this using a C# SslStream
. Most examples to be found, including in places like MSDN, are for "CONNECT" type proxys.
Do I need to create two SslStreams to handle this.
Answering my own question but maybe it will give others some direction. This is not production standard code but it works.
public sealed class SslTcpProxy
{
static void Main(String[] args)
{
// Create a TCP/IP (IPv4) socket and listen for incoming connections.
TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 443);
tcpListener.Start();
Console.WriteLine("Server listening on 127.0.0.1:433 Press enter to exit.");
Console.WriteLine();
Console.WriteLine("Waiting for a client to connect...");
Console.WriteLine();
// Application blocks while waiting for an incoming connection.
TcpClient tcpClient = tcpListener.AcceptTcpClient();
AcceptConnection(tcpClient);
Console.ReadLine();
tcpListener.Stop();
}
private static void AcceptConnection(TcpClient client)
{
try
{
// Using a pre-created certificate.
String certFilePath = Environment.CurrentDirectory + @"\certificates\server-cert.pfx";
X509Certificate2 certificate;
try
{
certificate = new X509Certificate2(certFilePath, "[CER_PASSWORD]");
}
catch (Exception ex)
{
throw new Exception($"Could not create the certificate from file from {certFilePath}", ex);
}
SslStream clientSslStream = new SslStream(client.GetStream(), false);
clientSslStream.AuthenticateAsServer(certificate, false, SslProtocols.Default, false);
// Display the properties and settings for the authenticated as server stream.
Console.WriteLine("clientSslStream.AuthenticateAsServer");
Console.WriteLine("------------------------------------");
DisplaySecurityLevel(clientSslStream);
DisplaySecurityServices(clientSslStream);
DisplayCertificateInformation(clientSslStream);
DisplayStreamProperties(clientSslStream);
Console.WriteLine();
// The Ip address of the server we are trying to connect to.
// Dont use the URI as it will resolve from the host file.
TcpClient server = new TcpClient("[SERVER_IP]", 443);
SslStream serverSslStream = new SslStream(server.GetStream(), false, SslValidationCallback, null);
serverSslStream.AuthenticateAsClient("[SERVER_NAME]");
// Display the properties and settings for the authenticated as server stream.
Console.WriteLine("serverSslStream.AuthenticateAsClient");
Console.WriteLine("------------------------------------");
DisplaySecurityLevel(serverSslStream);
DisplaySecurityServices(serverSslStream);
DisplayCertificateInformation(serverSslStream);
DisplayStreamProperties(serverSslStream);
new Task(() => ReadFromClient(client, clientSslStream, serverSslStream)).Start();
new Task(() => ReadFromServer(serverSslStream, clientSslStream)).Start();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
private static Boolean SslValidationCallback(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslpolicyerrors)
{
return true;
}
private static void ReadFromServer(Stream serverStream, Stream clientStream)
{
Byte[] message = new Byte[4096];
Int32 serverBytes;
try
{
while ((serverBytes = serverStream.Read(message, 0, message.Length)) > 0)
{
clientStream.Write(message, 0, serverBytes);
}
}
catch
{
// Whatever
}
}
private static void ReadFromClient(TcpClient client, Stream clientStream, Stream serverStream)
{
Byte[] message = new Byte[4096];
FileInfo fileInfo = new FileInfo("client");
if (!fileInfo.Exists)
{
fileInfo.Create().Dispose();
}
using (FileStream stream = fileInfo.OpenWrite())
{
while (true)
{
Int32 clientBytes;
try
{
clientBytes = clientStream.Read(message, 0, message.Length);
}
catch
{
break;
}
if (clientBytes == 0)
{
break;
}
serverStream.Write(message, 0, clientBytes);
stream.Write(message, 0, clientBytes);
}
client.Close();
}
}
static void DisplaySecurityLevel(SslStream stream)
{
Console.WriteLine("Cipher: {0} strength {1}", stream.CipherAlgorithm, stream.CipherStrength);
Console.WriteLine("Hash: {0} strength {1}", stream.HashAlgorithm, stream.HashStrength);
Console.WriteLine("Key exchange: {0} strength {1}", stream.KeyExchangeAlgorithm, stream.KeyExchangeStrength);
Console.WriteLine("Protocol: {0}", stream.SslProtocol);
}
static void DisplaySecurityServices(SslStream stream)
{
Console.WriteLine("Is authenticated: {0} as server? {1}", stream.IsAuthenticated, stream.IsServer);
Console.WriteLine("IsSigned: {0}", stream.IsSigned);
Console.WriteLine("Is Encrypted: {0}", stream.IsEncrypted);
}
static void DisplayStreamProperties(SslStream stream)
{
Console.WriteLine($"Can read: {stream.CanRead}, write {stream.CanWrite}");
Console.WriteLine($"Can timeout: {stream.CanTimeout}");
}
static void DisplayCertificateInformation(SslStream stream)
{
Console.WriteLine($"Certificate revocation list checked: {stream.CheckCertRevocationStatus}");
X509Certificate localCertificate = stream.LocalCertificate;
if (stream.LocalCertificate != null)
{
Console.WriteLine("Local cert was issued to {0} and is valid from {1} until {2}.",
localCertificate.Subject,
localCertificate.GetEffectiveDateString(),
localCertificate.GetExpirationDateString());
}
else
{
Console.WriteLine("Local certificate is null.");
}
// Display the properties of the client's certificate.
X509Certificate remoteCertificate = stream.RemoteCertificate;
if (stream.RemoteCertificate != null)
{
if (remoteCertificate != null)
{
Console.WriteLine(
$"Remote cert was issued to {remoteCertificate.Subject} and is valid from {remoteCertificate.GetEffectiveDateString()} until {remoteCertificate.GetExpirationDateString()}.");
}
}
else
{
Console.WriteLine("Remote certificate is null.");
}
}
}