¿Qué es Netcat?
La llamada navaja suiza de la red, util para depuración, transferencia de archivos, prueba de puertos, banner grabbing, proxies simples y más. Debido a sus diversos y riesgosos usos es desactivada de los sistemas por seguridad.
Sin embargo es común que en estos sistemas existe python instalado, y acontece que, con python se puede realizar una copia de Netcat, conociendo eso ahora muestro su realización.
Escribiendo Netcat.py
import argparse
import socket
import shlex
import subprocess
import sys
import textwrap
import threading
def execute(cmd):
cmd = cmd.strip()
if not cmd:
return
output = subprocess.check_output(shlex.split(cmd),
stderr = subprocess.STDOUT)
return output.decode()En este primer código tenemos los módulos necesarios para la construccion de nuestro Netcat, además, se establece una función execute(cmd), en donde se normaliza cmd con cmd.strip() eliminando caracteres de espacio Unicode al principio y final de la cadena, si cmd resulta estar vacio retorna inmediatamente, caso contrario lo ejecuta en la maquina local con subprocess.checkoutput, tokeniza el comando con shlex.split para manejar correctamente inputs con espacios o comillas y nos aseguramos de capturar stderr con stderr = subprocess.STDOUT, esto para poder observar tambien mensajes de error, finalmente convertimos la cadena de bytes --Devuelta por subprocess.check_output-- almacenada en la variable output a un string mas legible con decode().
Ahora procedemos a la función main, encargada de manejar los comandos y de llamar las funciones restantes del código.
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description = 'BHP Net Tool',
formatter_class = argparse.RawDescriptionHelpFormatter,
epilog = textwrap.dedent('''Example:
netcat.py -t 192.168.1.108 -p 5555 -l -c #command shell
netcat.py -t 192.168.1.108 -p 5555 -u=mytest.txt #upload to file
netcat.py -t 192.168.1.108 -p 5555 -l -e=\"cat /etc/passwd\" #execute command
echo 'ABC' | ./netcapt.py -t 192.168.1.108 -p 135 #echo text to server port 135
netcat.py -t 192.168.1.108 -p 5555 #connect to server
'''))
parser.add_argument('-c', '--command', action='store_true', help='command shell')
parser.add_argument('-e', '--execute', help='execute specified command')
parser.add_argument('-l', '--listen', action='store_true', help='listen')
parser.add_argument('-p', '--port', type=int, default=5555, help='specified port')
parser.add_argument('-t', '--target', default='192.168.1.203', help='specified IP')
parser.add_argument('-u', '--upload', help='upload file')
args = parser.parse_args()
if args.listen:
buffer = ''
else:
buffer = sys.stdin.read()
nc = NetCat(args, buffer.encode())
nc.run()Se hace uso argparse.ArgumentParser(...) para crear una interfaz para lineas de comando, además de proveer comandos para subir archivos, ejecutar comandos o iniciar una shell. También se proveen ejemplos de uso para cuando el usuario use --help.
El argumento -c inicia una shell, -e ejecuta un comando especificado por el cliente, -l inicia el modo de escucha, -t para especificar la IP a donde se quiere conectar, -u especifica el nombre del archivo a subir. El script puede ser usado desde el cliente o servidor ya que puede ser usado para ser cualquiera de los dos.
Si eligimos ser el cliente iniciamos la funcion main con un buffer string vacío, caso contrario enviamos el contenido de buffer desde stdin, finalmente mandamos a llamar el metodo run para iniciarlo.
Una vez terminada la interfaz llega la hora de implementar todas sus características en código.
Se debe añadir lo siguiente arriba de la función main:
class NetCat:
def __init__(self, args, buffer=None):
self.args = args
self.buffer = buffer
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def run(self):
if self.args.listen:
self.listen()
else:
self.send()Se define el constructor con los argumentos de la linea de comandos y el buffer, despues creamos el socket ipv4 con protocolo TCP para la conexión con socket(socket.AF_INET, socket.SOCK_STREAM), ademas de especificar con setsockopt(SOL_SOCKET, socket.SO_REUSEADDR, 1) que la dirección (IP:Puerto) se puede usar de manera inmediata sin esperar el TIME_WAIT del SO para evitar un posible error de "address already in use". Finalmente establecemos la funcion run(self), donde establecemos el modo servidor o cliente segun args.
Ahora pasamos a escribir el metodo send()
def send(self):
self.socket.connect((self.args.target, self.args.port))
if self.buffer:
self.socket.send(self.buffer)
try:
while True:
recv_len = 1
response = ''
while recv_len:
data = self.socket.recv(4096)
recv_len = len(data)
response += data.decode()
if recv_len < 4096:
break
if response:
print(response)
buffer = input('> ')
buffer += '\n'
self.socket.send(buffer.encode())
except KeyboardInterrupt:
print('User terminated.')
self.socket.close()
sys.exit()La funcion send(self) se conecta al servidor con socket.connect((ip, port)), despues comprueba si se mando un payload o solo se conecto con la condicional if self.buffer, si existe, lo manda al servidor, caso contrario continua al try.
Dentro del try nos encontramos con un bucle infinito que solo puede ser interrumpido por el usuario, esto se observa en el except KeyboardInterrupt.
Dentro del bucle tenemos nuevamente otro bucle pero condicionado por recv_len con: while recv_len. Este bucle permite capturar la respuesta del servidor en chunks de 4096 bits para despues decodificarla y almacenarla en response con: response += data.decode(). La condicional: if recv_len < 4096. Comprueba que hemos recibido toda la respuesta del servidor y rompe el bucle, continua a otra condicional: if response. Si existe respuesta, la devuelve y nos permite enviar nuevamente información al servidor.
Ahora pasamos a escribir la parte que se comporta como servidor.
def listen(self):
self.socket.bind((self.args.target, self.args.port))
self.socket.listen(5)
while True:
client_socket, _ = self.socket.accept()
client_thread = threading.Thread(
target = self.handle, args=(client_socket,)
)
client_thread.start()Como se puede observar resulta bastante sencilla, esta funcion crea un nuevo socket para el servidor y lo relaciona al mismo tiempo con la linea self.socket.bind((self.args.target, self.args.port)) para despues habilitar una lista de espera de 5 conexiones entrantes.
El bucle infinito que le sigue consiste en escuchar de forma infitnita y aceptar la conexion entrante y almacenarla en client_socket, para despues iniciar un hilo por cada conexión entrante con threading.Thread(target = self.handle, args = (client_socket,))
def handle(self, client_socket):
if self.args.execute:
output = execute(self.args.execute)
client_socket.send(output.encode())
elif self.args.upload:
file_buffer = b''
while True:
data = client_socket.recv(4096)
if data:
file_buffer += data
else:
break
with open(self.args.upload, 'wb') as f:
f.write(file_buffer)
message = f'Saved file {self.args.upload}'
client_socket.send(message.encode())
elif self.args.command:
cmd_buffer = b''
while True:
try:
client_socket.send(b'BHP: #> ')
while '\n' not in cmd_buffer.decode():
cmd_buffer += client_socket.recv(64)
response = execute(cmd_buffer.decode())
if response:
client_socket.send(response.encode())
cmd_buffer = b''
except Exception as e:
print(f'Server killed {e}')
self.socket.close()
sys.exit()Continuamos con la parte final del código, dentro de la funcion handle, contamos con una condicional donde se revisa el argumento recibido por el cliente, si es el argumento "execute", ejecutamos el comando y guardamos el resultado con output = execute(self.args.execute) para despues regresar el resultado al cliente con client_socket.send(output.encode()).
Cuando un cliente manda un archivo el servidor prepara un buffer binario vacio. Posteriormente empieza a almacenar los datos recibidos en bloques de 4096 bytes. Terminada la transmision de datos lo guarda localmente con with open(self.args.upload, 'wb') as f: . Finalmente el servidor manda un mensaje de confirmacion al cliente.
Para el caso command, el servidor crea un buffer binario vacio y entra en bucle infinito donde proporciona un prompt al usuario y espera los datos, esperando un salto de linea para terminar de almacenar en el buffer. Posteriormente lo ejecuta y almacena en la variable response. Finalmente lo manda al cliente y limpia el buffer para continuar con una comunicacion continua.
