Como envia datos a un sitio web HTTPS via socket? | Servidor Proxy python3

Iniciado por Drakaris, 20 Julio 2020, 19:21 PM

0 Miembros y 2 Visitantes están viendo este tema.

Drakaris

Buenas, estoy haciendo un servidor proxy con Python3. La idea es la siguiente:

1 . Primero, capturo el puerto especificado en el primer argumento:


if len(sys.argv) == 2:
        pass
else:
        sys.exit()


# CONSTANTS GLOBALS
global PORT_LISTEN, BUFFER_RECV, CONNECTIONS
PORT_LISTEN = int(sys.argv[1])
BUFFER_RECV = 8192
CONNECTIONS = 5


2. Después con el puerto especificado, creo una conexion socket servidor bind(), y obtengo los datos recibidos, con ello, ejecuto un hilo, donde paso los datos de la conexion y datos a la funcion get_data.

print("[*] Listening Port "+str(PORT_LISTEN)+"....")
lister = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
lister.bind(("",PORT_LISTEN))
lister.listen(CONNECTIONS)
while True:
        try:
                conn, addr = lister.accept()
                data = conn.recv(BUFFER_RECV)
                thread_data = threading.Thread(target=get_data, name="get_data", args=(conn, addr, data))
                thread_data.start()
        except KeyboardInterrupt:
                conn.close()
                lister.close()
                print("\n[\033[1;31m!\033[0m] Exiting Proxy Server....")
                sys.exit()


3. En la funcion get_data, obtengo la url, donde cogeré dominio donde estoy visitando

        #get url
        url = str(data).split('\\n')[0]
        url = str(url).split(' ')[1]

        #get FQDN
        website = url.replace("http://","")
        website = website.split("/")[0]


4. Una vez sé en que dominio estoy haciendo la petición, si el dominio contiene ":" cogeré el puerto especificado. Por defecto el puerto es 80, si no se detecto ":" en el dominio.

        port = 80
        if website.count(":") == 1:
                list = website.split(":")
                website = list[0]
                port = list[1]
                port = int(port)
        print("\n\033[1;32mwebsite\033[0m: "+str(website)+" \033[1;32mport\033[0m: "+str(port))


5. Finalmente ejecuto la funcion send_data con sus argumentos, para que me haga una conexion socket cliente al servidor y envie los datos, con un bucle.

send_data(website, int(port), conn, data, addr, True)

def send_data(site, port, conn, data, addr):
    sender = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sender.connect((site,port))
    sender.send(data)
    while True:
            reply = sender.recv(8192)
            if len(reply) > 0:
                conn.send(reply)
            else:
                break
    sender.close()
    conn.close()


Esto funciona correctamente en sitios web HTTP, pero como cabe esperar, el HTTPS no, por los certificados ssl.

Así que modifiqué el apartado 5. Y con una condicional, si el puerto es 443, ejecutará lo siguiente:

        if port == 443:
                send_data(website, int(port), conn, data, addr, True)
        else:
                send_data(website, int(port), conn, data, addr, False)

def send_data(site, port, conn, data, addr, SSL):
        sender = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sender.connect((site,port))
        sender.send(data)
        while True:
                reply = sender.recv(BUFFER_RECV)
                if len(reply) > 0:
                        conn.send(reply)
                else:
                        break
        sender.close()
        conn.close()



En send_data añadí un argumento booleano llamada SSL, que antes de hacer nada, si este es True, sería https, lo que no sé muy bien, es que hacer aquí.


Me he mirado el funcionamiento de los sitios HTTPS en youtube.
[youtube=640,360]https://www.youtube.com/watch?v=uNrh0zoItGI[/youtube]

En el minuto 2:16, explica con más profundidad como trabaja este protocolo. He pensado, si hay alguna forma de obtener el certificado que me da el servidor para obtener la llave publica.... pero no se muy bien.

Miré este enlace: https://soursop-dev.blogspot.com/2020/05/creacion-de-un-servidor-web-proxy-en.html

Donde te explica como hacer una conexion ssl con el modulo ssl.

Así que modifique la funcion send_data:

def send_data(site, port, conn, data, addr, SSL):
        if SSL == True:
                server_cert = "./ssl/sechome.csr"
                client_cert = "./ssl/sechome.crt"
                client_key = "./ssl/sechome.key"
                context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=server_cert)
                context.load_cert_chain(certfile=client_cert, keyfile=client_key)
                s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
                sender = context.wrap_socket(s, server_side=False, server_hostname=site)
        else:
                sender = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sender.connect((site,port))
        sender.send(data)
        while True:
                reply = sender.recv(BUFFER_RECV)
                if len(reply) > 0:
                        conn.send(reply)
                else:
                        break
        sender.close()
        conn.close()
:

Creé los certificados autofirmados:

$ cd ssl
ssl/$ openssl req -newkey rsa:2048 -nodes -keyout sechome.key -out sechome.csr -sha256
ssl/$ openssl x509 -signkey sechome.key -in sechome.csr -req -days 365 -out sechome.crt


Como bien me imaginaba esto no funciona, ya que tendría que coger el certificado que me da el dominio al hacer la peticion. No acabo de entender muy bien en las variables de crt y key, que llaves tengo que poner...


                server_cert = "./ssl/sechome.csr"
                client_cert = "./ssl/sechome.crt"
                client_key = "./ssl/sechome.key"



CODE COMPLETE:

#!/usr/bin/python3
import socket, threading, re, sys, os

if len(sys.argv) == 2:
pass
else:
sys.exit()


# CONSTANTS GLOBALS
global PORT_LISTEN, BUFFER_RECV, CONNECTIONS
PORT_LISTEN = int(sys.argv[1])
BUFFER_RECV = 8192
CONNECTIONS = 5

def send_data(site, port, conn, data, addr, SSL):
if SSL == True:
server_cert = "./ssl/sechome.csr"
client_cert = "./ssl/sechome.crt"
client_key = "./ssl/sechome.key"
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=server_cert)
context.load_cert_chain(certfile=client_cert, keyfile=client_key)
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sender = context.wrap_socket(s, server_side=False, server_hostname=site)
else:
sender = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sender.connect((site,port))
sender.send(data)
while True:
reply = sender.recv(BUFFER_RECV)
if len(reply) > 0:
conn.send(reply)
else:
break
sender.close()
conn.close()


def get_data(conn, addr, data):
#get url
url = str(data).split('\\n')[0]
url = str(url).split(' ')[1]

#get FQDN
website = url.replace("http://","")
website = website.split("/")[0]
port = 80
if website.count(":") == 1:
list = website.split(":")
website = list[0]
port = list[1]
port = int(port)
print("\n\033[1;32mwebsite\033[0m: "+str(website)+" \033[1;32mport\033[0m: "+str(port))
if port == 443:
send_data(website, int(port), conn, data, addr, True)
else:
send_data(website, int(port), conn, data, addr, False)


print("[*] Listening Port "+str(PORT_LISTEN)+"....")
lister = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
lister.bind(("",PORT_LISTEN))
lister.listen(CONNECTIONS)
while True:
try:
conn, addr = lister.accept()
data = conn.recv(BUFFER_RECV)
thread_data = threading.Thread(target=get_data, name="get_data", args=(conn, addr, data))
thread_data.start()
except KeyboardInterrupt:
conn.close()
lister.close()
print("\n[\033[1;31m!\033[0m] Exiting Proxy Server....")
sys.exit()
Lo increible, no es lo que ves, sino como es

el-brujo

No tienes que crear ningún certificado para conectarte a una página web que usa https, ni crt, ni key, ni csr, ni nada. Eso es para el servidor web si fuera tuyo, pues para configurar el certificado SSL.

Para navegar o en tu caso, para enviar una petición con datos sólo tienes que usar la librería SSL y ya:

import ssl

https://docs.python.org/3/library/ssl.html


@XSStringManolo

Si quieres interceptar tráfico te recomiendo usar mitmproxy.
Yo uso el cli, pero tienen lib para python. Aquí te dejo una implementación:
https://gist.github.com/carpedm20/27f5157c3ff8f2a63a37

Drakaris

Buenas! He modificado el código y al detectar que el puerto es 443 he creado la funcion send_data_tls() donde le envio los mismos argumentos.

if port == 443:
send_data_tls(website, int(port), data, conn)
else:
send_data(website, int(port), conn, data, addr)


funcion send_data_tls

def send_data_tls(site, port, data, conn):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sender = ssl.wrap_socket(s, do_handshake_on_connect=False, ssl_version=ssl.PROTOCOL_TLS)
sender.connect((site,port))
print("\033[1;32m data sent\033[0m: "+str(data))
sender.send(data)
while True:
reply = sender.recv(BUFFER_RECV)
sender.send(reply)
print("\033[1;32m data sent X2\033[0m: "+str(reply))



Cuando lo ejecuto me imprime lo siguiente en la pantalla:
imagenes:https://drive.google.com/drive/folders/1S4GZBjBUziGXvo_wNpVon8EkyoEs1tue?usp=sharing

Como se puede ver en las imagenes, la salida me imprime bién los datos a enviar:
Citarb'CONNECT platzi.com:443 HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: platzi.com:443\r\n\r\n'

Pero cuando recibo la respuesta, recibo un erro 400 Bad Request:
Citar
b'HTTP/1.1 400 Bad Request\r\nServer: cloudflare\r\nDate: Wed, 22 Jul 2020 09:30:08 GMT\r\nContent-Type: text/html\r\nContent-Length: 155\r\nConnection: close\r\nCF-RAY: -\r\n\r\n<html>\r\n<head><title>400 Bad Request</title></head>\r\n<body>\r\n<center><h1>400 Bad Request</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>\r\n'

Además me sale el siguiente error:
Citarssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:2162)

El error 400 indica que la petición que fue enviada no a podido ser procesada por algún tipo de error. Que puede estar causando este error?

En realidad no acabo de entender porque, yo me conecto a un sitio web por el puerto 443, y envio los datos envidos con data() a otro servidor, en este caso a Cloudflare. Es aquí que yo no entiendo que datos tengo que enviar o que debo de hacer. ¿Este es el paso donde se hace el handshake antes de conectarse al sitio web?
Lo increible, no es lo que ves, sino como es

Drakaris

Buenas!! Cuando he hecho la parte del proxy HTTP entendí su funcionamiento y todo, pero ahora en la parte de HTTPS. no acabo de entender muy bien.... Modifiqué el código, para ver mejor lo que esta pasando...


#CODE
class Send_data_tls:
def send_data(self, site, port, conn, data, addr):
context = ssl.create_default_context()
sender = context.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_hostname=site)
sender.connect((site,port))
HEAD = "HEAD / HTTP 1.1\r\nHost: "+str(site)+"\r\n\r\n"
sender.send(HEAD.encode())
print("\033[1;32m data sent\033[0m: "+str(data))
receive = sender.recv(BUFFER_RECV)
print("\n\n"+str(receive))

def get_data(conn, addr, data):
#CODE
if port == 443:
SSL = Send_data_tls()
SSL.send_data(website, int(port), conn, data, addr)
else:
send_data(website, int(port), conn, data, addr)
#CODE
#CODE


Hice varias pruebas y ninguna me surgió nada interesante... donde pueda partir de alla. Cree la clase Send_data_tls con la funcion send_data (creo una clase por que me imagino que tengo que hacer algo antes de enviar los datos a la web HTTPS).

En la funcion get_data en la condicional cargo la clase en la variable SSL y ejecuto la funcion send_data().

Cuando lo ejecuto, me encuentro con algo MUY lógico...
[IMAGE] =>  https://drive.google.com/file/d/1rdieWydRAgjcHvhZ7qdK6hrk583-V2Hl/view?usp=sharing

Lo que pasa, y cuya cosa ya me dí cuenta es que, al conectarme a la web por 443
Citar
data sent: b'CONNECT platzi.com:443 HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: platzi.com:443\r\n\r\n'

Este se conecta antes con el servidor Cloudflare (creo que la CA para autenticar el certificado), y este por alguna razon me recibe el 400 Bad Request.

El problema en que no entiendo que esta pasando y que debo hacer.... El cloudflare me esta rechazando.

Puedo hacer lo que quiero hacer con unicamente el modulo ssl?

Segun @el-brujo me dijo que con ese modulo me bastava. Hay algun post o sitio donde expliquen como funciona este modulo?
Cita de: el-brujo en 20 Julio 2020, 22:45 PM
Para navegar o en tu caso, para enviar una petición con datos sólo tienes que usar la librería SSL y ya:

Código:

import ssl


https://docs.python.org/3/library/ssl.html


Gracias
Lo increible, no es lo que ves, sino como es