Search code examples
pythonsocketssslproxypyopenssl

MITM proxy over SSL hangs on wrap_socket with client


I’m in quite a predicament and you are my last hope. I started doing a project making a MITM proxy with ssl. My program uses the HTTP CONNECT method. To keep this concise my program follows the following diagram…

      Client          Proxy             Server
           +              +             +
  CONNECT  |              |             |
           +------------> |             |
           |              |             |
           |    200 Established         |
           |  <-----------+             |
           |              |             |
           | <----------> |             |
           |  Handshake   |             |
           |              | <---------> |
           |              |  Handshake  |
           |              |             |
           |              |             |
           | <----------> | <---------> |
           | Data Exchange| Data Exchange
           |              |             |

My proxy shares a certificate with the client however my client seems to be unable to connect and my proxy hangs on do_handshake() when I call wrap_socket() on the accepted socket. Here’s the section of python where a client connects to the proxy…

import ssl, socket
from _thread import *
port = 8000
def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('localhost', port))
    sock.listen(10)
    while True:
        connection, addr = sock.accept()
        data = connection.recv(8192)
        connection.send(b"HTTP/1.0 200 OK")
        connection = ssl.wrap_socket(connection, keyfile='private.pem', certfile='cacert.pem', server_side=True)
        start_new_thread(connection_string, (connection, data, addr))  # start thread to handle CONNECT

I simulate a client with Openssl

openssl s_client -proxy 127.0.0.1:8000 -connect www.google.com:443 -state -verify 1 -CAfile cacert.pem -verify_return_error

When the client connects to the proxy they both hang. If I kill the client I receive

Traceback (most recent call last):
  File "/mitm/mitm_proxy.py", line 100, in <module>
    run()
  File “/mitm/mitm_proxy.py", line 92, in run
    connection = ssl.wrap_socket(connection, keyfile='private.pem', certfile='cacert.pem', server_side=True)
  File "/usr/lib/python3.5/ssl.py", line 1069, in wrap_socket
    ciphers=ciphers)
  File "/usr/lib/python3.5/ssl.py", line 752, in __init__
    self.do_handshake()
  File "/usr/lib/python3.5/ssl.py", line 988, in do_handshake
    self._sslobj.do_handshake()
  File "/usr/lib/python3.5/ssl.py", line 633, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:645)

if I kill my proxy my client says

CONNECTED(00000003)
s_client: HTTP CONNECT failed
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 140709212149056 bytes and written 39 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
---

I’ve compared my code with others that work and they are essentially the same which is why I’m here. I can’t figure out whats going wrong.


Solution

  • I'm not sure if this the main cause of your problem but this one is definitely wrong:

        connection.send(b"HTTP/1.0 200 OK")
    

    The correct response of the server should be the status line ending with \r\n, optional key:value\r\n pairs and then the \r\n to mark the end of the HTTP header, i.e. minimally:

        connection.send(b"HTTP/1.0 200 OK\r\n\r\n")
    

    Since the client is not receiving the missing \r\n\r\n it will probably wait for it and not start the TLS handshake (i.e. send ClientHello). The server instead will wait for the client to start the handshake and this way both will wait for each other forever.

    Apart from you not following the standard openssl s_client -proxy ... seems to not follow the standard too. It actually needs the response to contain the word "established" as the following excerpt from the lazy HTTP parsing code in apps/s_client.c in OpenSSL 1.1.0c suggests:

    case PROTO_CONNECT:
            ...
            BIO_printf(fbio, "CONNECT %s HTTP/1.0\r\n\r\n", connectstr);
            (void)BIO_flush(fbio);
            /* wait for multi-line response to end CONNECT response */
            do {
                mbuf_len = BIO_gets(fbio, mbuf, BUFSIZZ);
                if (strstr(mbuf, "200") != NULL
                    && strstr(mbuf, "established") != NULL)
                    foundit++;
            } while (mbuf_len > 3 && foundit == 0);
    

    So try with the following line:

        connection.send(b"HTTP/1.0 200 established\r\n\r\n")