HTTP(S) interception for my IoT lab

In my last post, I documented how my I set up my Linksys router to serve as a firewall for my home IoT lab. This was one of two outstanding issues that I described in my first post about my lab. The remaining unaddressed issue was to set up HTTPS interception.

With a proper firewall in place, my initial lab setup is almost complete. The only thing remaining on my initial goals is setting up TLS/HTTPS interception, which will involve generating a CA, configuring the lab tablet to trust it, configuring sslsplit on the Kali laptop and possibly setting up iptables rules on the router to intercept outbound traffic on port 443. I don’t think that will take too long to configure, so I might end up doing it when I first encounter a device that requires that capability.

In this post I’ll go into how I set up HTTP(S) interception in a way that supports both listening and tampering MITM.

Architecture

My lab has two primary infrastructure components, a WiFi router that handles NAT, firewalling and DHCP, and a laptop that runs a DNS server for tampering with DNS. In order to be able to read and modify HTTP and HTTPS connections, I needed to add servers for both protocols. In fact I landed on running two servers, Apache and sslsplit, both listening on port 80 and port 443, on separate IP addresses in order to get a more feature rich interception.

The traffic flow is that either the DNS server (via a spoofed record) or the router (via an iptables rule) redirects a client to sslsplit. sslsplit forges a certificate on a local CA if the request is HTTPS, and then forwards the connection to Apache. Apache then desides to either tamper with the request or transparently proxy it to the original destination.

Apache

Apache runs on 192.168.2.3, ports 80 and 443. The configuration is basically the default Apache/PHP Ubuntu configuration, with a few global options changed to enable the right modules:

RewriteEngine On
SSLProxyEngine On
AllowOverride all

ProxyVia On

In /var/www/html, there’s a .htaccess file that contains the actual logic for how Apache will handle the request. It’s convenient to put this in a .htaccess file, because these get automatically reloaded when modified without needing to restart Apache.

The .htaccess decides to either proxy the request to the original host or pass the request to a PHP script mitm.php based on a series of RewriteRules.

To perform the proxying, is actually quite simple:

# Prevent loops
RewriteCond "%{HTTP:Via}" =""

# To MITM a host:
# RewriteCond "%{HTTP_HOST}" "!=example.com"
RewriteCond "%{HTTP_HOST}" "!^192.168.\d+.\d+$"
RewriteRule ^(.*)$ %{REQUEST_SCHEME}://%{HTTP_HOST}/$1 [P,NS,L,QSA]

The RewriteRule at the bottom matches all requests, proxies them (P), doesn’t match subrequests (NS), prevents any other redirect rules from running (L), and passes through the query string (QSA).

There are two exceptions that are not proxied. The first is any request that already has the Via header set. Because ProxyVia is on, this will prevent Apache from entering infinite loops if it ever tries to go proxy a request to itself.

The second exception is disallowing any requests to 192.168.0.0/16. This is just to prevent any other weird request patterns, or cases where this allow requests to bypass some of the router’s firewall rules because they’re getting made by the laptop.

It’s also easy to add new exceptions for specific hosts to prevent them from getting proxied, and instead be served a tampered response.

For requests that don’t get proxied, they get matched by this rule:

RewriteCond "%{REQUEST_URI}" "!=/mitm.php"
RewriteRule ^(.*)$ /mitm.php [L,QSA]

This causes any non-proxied requests to get handled by mitm.php:

<?php

# Note:
# $_SERVER['SERVER_NAME'] contains the host header
# $_SERVER['REDIRECT_URL'] contains the original request path (no query string)

if ($_SERVER['SERVER_NAME'] == "example.com") {
  // do something cool
} else {
  print_r(array($_SERVER, $_REQUEST));
}

?>

So, to return a different result for a request all that needs to happen is to add a new RewriteCond for the target, and then add the relevant logic to mitm.php for handling the site. I think this is pretty flexible, because the PHP script could then chose to further proxy the request and modify it or do anything else it wants.

sslsplit

With Apache configured to be a transparent forward proxy that could tamper with requests, the other piece of the puzzle was to configure an SSL CA and a server to generate leaf certificates on-the-fly:

Creating a CA can be a real pain with the openssl command line tools, so I decided to use minica instead. Creating a new CA was as easy as:

minica -ca-common-name "" -org "IOT Testnet" example.com

This spits out a private key and a certificate for both the CA and for the example.com leaf. In this case, we only need the cacert.crt and cakey.key files for passing to sslsplit.

sslsplit is a lot like Moxie’s sslstrip, but with a lot more features. Basically, it supports generating certificates signed by the CA in response to new connections, and also supports a logging the connection contents as PCAP files.

Strangely, the sslsplit included in Kali had some sort of issue with openssl, a simple Dockerfile to build it from source instead. The invocation I settled on to actually run the tool is:

DATE="$(date +%Y-%m-%dT%H:%M)"

docker run --rm \
        --network host \
        -v "$(pwd):/data" \
        -it sslsplit \
        sslsplit \
                -c /data/cacert.crt \
                -k /data/cakey.key \
                -M /data/ssl_keylog \
                -X /data/data/$DATE.pcap \
                https 192.168.2.4 443 192.168.2.3 443 \
                http 192.168.2.4 80 192.168.2.3 80

These flags run sslsplit in docker with access to the host network. The sslsplit daemon listens on port 80 and 443 on 192.168.2.4 and proxies both to 192.168.2.3, which is where the Apache proxy is running. It also dumps a pcap file of the proxied content, and saves a log of the TLS session keys which can be used by Wireshark too.

For HTTP traffic, sslsplit does some simple processing like removing HSTS headers and also disables a bunch of features like compression and chunked encoding to make the log more readable. For HTTPS it does the same processing for inner request, and also handles the TLS handshake. In this case, it clones the snake-oil cert from Apache, and adds any name from the TLS SNI extension into the SAN field of the cert to make sure that it matches the requested name. For requests that don’t have SNI, they only get the default cert names, unfortunately, but hopefully this is relatively rare.

Conclusion

With sslsplit and Apache up and running, the only remaining task is to direct traffic into the MITM server. There are two ways that I’ve been doing this – either configuring the DNS server to return the IP address for sslsplit directly, or set up iptables rules on the router to slurp up all traffic for port 80 and 443. I’m not sure which is more useful yet, but having both options is nice. This makes the full flow:

  1. Direct target traffic to 192.168.2.4 using DNS or iptables.
  2. Optionally, stop if the goal is just to decrypt the traffic.
  3. Add a RewriteCond to /var/www/html/.htaccess to not proxy the traffic.
  4. Add relevant logic to /var/www/html/mitm.php to return desired results.

I also was able to load the generated CA into the Android table that I’m using for testing, which means that it always trusts the MITM, which is pretty convenient. I’m hoping that most IoT devices don’t do much certificate verification or can be configured to trust other certificates too.

In the end this ended up not being less than a hundred lines of configuration, but connecting all the pipes together ended up being pretty difficult and required some iteration. I’m pretty happy with the flexibility of the resulting system and the relatively fine grained control it provides over the traffic flows.