Using socat for TLS interception

1 August 2022

socat is one of my favorite Linux commands. It's basically a super-powered netcat, giving you options way beyond TCP and stdin/out. If you need to pipe one thing into another thing, socat can do it. It'll speak TCP, UDP, UNIX domain sockets, open/exec, and even raw IP/ethernet packets. While it has a lot of features, the interface is relatively simple - you supply two addresses, and socat will create a bidirectional (or unidirectional if you choose) link between them. For example, here's a simple TCP echo server:

socat tcp-listen:5555,fork,reuseaddr pipe

tcp-listen is the "address type keyword", which decides what type of connection we want to work with. In this case, we're listening for a TCP connection. :5555 is an "address parameter" - what exactly these mean is specific to the address type, but in this case it's the port we're listening on. fork and reuseaddr are "options" - sometimes these are specific to a particular address type, but usually they come from larger "option groups" that are shared between similar address types. fork, from the CHILD option group, causes socat to fork when a connection is established and then continue to wait for connections. Without this option, socat will only accept a single connection and terminate when it closes. reuseaddr is part of the SOCKET option group, and allows multiple processes to listen on the same address. It isn't strictly necessary, but I've found that sometimes it can take a second for the port to become available again after socat closes, and this reduces your turnaround time.

pipe creates an unnamed pipe, where the read and write halves are connected together. In total, this will accept a TCP connection, fork, create a pipe, then attach the read and write ends of the two streams, creating our echo server. You can verify this behavior with netcat, but socat can do the same thing:

socat tcp-connect:localhost:5555 stdio

We'll come back to socat in a bit.

TLS Interception

TLS is ubiquitous these days, and it makes it a bit annoying to inspect the traffic going in and out of your computer. There are commercial tools that can help you strip it, but socat can do it for us provided we can get in the middle of the communication. You'll also need to disable certificate validation in whatever TLS client you're using.

We'll need to trick the client into connecting to us instead of whatever server it wants to talk to. The simplest way to do this is with the /etc/hosts file, which allows us to force most DNS resolvers to use a particular IP address for a domain. If your client isn't fooled by this, you can use iptables rules to redirect the traffic itself instead of the domain. Set up a rule in your hosts file:

# Redirect example.com to localhost
127.0.0.1 example.com

Now anybody wanting to talk to example.com will talk to you instead:

$ ping example.com
PING example.com (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.207 ms

As-is, whatever client you're trying to intercept won't work correctly - there's nothing listening for connections on the port it expects. We'll use socat to handle that.

Set up socat

There's two address types that will be useful here: openssl-listen and openssl. These listen for or establish a TLS-encrypted TCP connection, respectively. We'll have socat listen for a connection, then redirect traffic back to the actual origin:

socat openssl-listen:443,fork,reuseaddr openssl:93.184.216.34:443

Note the use of example.com's IP address instead of its domain - if we'd used the domain, it would've just pointed back to 127.0.0.1 again.

That's the principle, but it won't work as is. We need to give the listener half a certificate to use, and the client half will fail to connect because the certificate covers the domain, not the IP. We can use openssl to generate a certificate and a private key. If you don't care about the specifics, here's a working command:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=localhost'

Once we have these, we'll provide them to socat and turn off certificate verification:

socat -v openssl-listen:443,fork,reuseaddr,cert=cert.pem,key=key.pem,verify=0 openssl:93.184.216.34:443,verify=0

Now we can make our request (note the -k option, which disables certificate validation):

curl -k https://example.com

Because we used the -v switch on socat, it'll copy the data that passes through the sockets to stderr, allowing us to see the request. You can pipe this to a file and use a hex editor to inspect the data more thoroughly. There's a way to take this to the next level though - split the pipe in half and take a trip through the loopback interface:

socat openssl-listen:443,fork,reuseaddr,cert=cert.pem,key=key.pem,verify=0 tcp-connect:localhost:8888
socat tcp-listen:8888,fork,reuseaddr openssl:93.184.216.34:443,verify=0

Now that the request is travelling over the network in cleartext, tools like Wireshark can parse it and give you way more detail.