一线运维实战手记 · 云计算 · 数据库 · 故障排查
2026年10月16日

CVE-2026-104795 [HIGH] – The insights-runtime-extractor runs as a privileged DaemonSet (`hostPID: true`) that scans

未分类
📑 文章目录

The insights-runtime-extractor runs as a privileged DaemonSet (`hostPID: true`)
that scans container processes for runtime fingerprinting. When scanning a
container, the coordinator:

1. **fork()s** a child process
2. **setns()** into the target container’s PID and mount namespaces
3. **fchdir()** back to an orphaned FD pointing at the extractor’s own root
filesystem (where `/crio.sock` is hostPath-mounted)
4. Calls **only `seteuid(target_uid)`** — changing the effective UID but leaving
the **real UID and saved-set-UID at 0** and **retaining the permitted
capability set**
5. The `check_no_privileged_perms()` guard inspects only the Effective capability
set (so it passes), and is **skipped entirely when the target runs as uid 0**
6. The `VersionExecutable` fingerprint invokes `fpr_kind_executable` with
`process.command_line[0]` — the target’s argv[0], read from
`/proc//cmdline` and **fully controlled by the workload**
7. `fpr_kind_executable` calls `exec.Command(executable, “–version”).Output()`
— executing the **workload’s own binary** inside the workload’s mount
namespace while **ruid=0 in the host user namespace**

The executed process can call `setuid(0)` to restore full root with
`CAP_SYS_ADMIN`, and inherits the orphaned cwd which resolves into the extractor
container’s filesystem containing the host CRI-O socket.

## Affected Code

### 1. Insufficient privilege drop (Rust — extractor coordinator)

**File:** `extractor/src/insights_runtime_extractor.rs`

“`rust
// Lines 139-146 — seteuid-only drop, guard skipped for root containers
join_process_namespaces(process.pid)?;
change_dir(current_dir);
switch_user(*process.uid)?;
if *process.uid != 0 {
perms::check_no_privileged_perms()
.expect(“Must not have privileged permissions to run fingerprints”);
}
fingerprint::run_fingerprints(&config, out_dir, &process);
“`

“`rust
// Lines 182-185 — only effective UID changed; ruid/suid stay 0
fn switch_user(uid: u32) -> Result {
debug!(“… Becoming user: {uid}”);
seteuid(uid.into()).map_err(|e| ScannerError::Errno(e))
}
“`

### 2. Execution of untrusted binary (Go — fingerprint)

**File:** `fingerprints/pkg/utils/file.go`

“`go
// Lines 75-80 — executes a path taken verbatim from workload’s argv[0]
func GetExecutableVersionOutput(executable string) (string, error) {
out, err := exec.Command(executable, “–version”).Output()
if err != nil {
return “”, err
}
return string(out), nil
}
“`

### 3. Workload-controlled path forwarded (Rust — version_executable)

**File:** `version_executable.rs`

“`rust
// Lines 29-34 — workload-controlled command_line[0] forwarded to fingerprint
return Some(vec![
fpr_kind_executable,
out_dir.to_string(),
String::from(&process.command_line[0]),
String::from(&version_executable.runtime_kind_name),
]);
“`

## Attack Pattern

**Attacker:** Any tenant able to schedule a pod (unprivileged, any namespace)
**Victim:** The node running the insights-runtime-extractor DaemonSet
**Trigger:** Automatic — the DaemonSet scans all container processes on the node

**Attack sequence:**

1. Attacker creates a pod whose process name matches a configured
versioned-executable pattern (`node`, `python`, `python3`, `MainThread`,
or contains `java`)
2. The pod’s main binary (at the path matching argv[0]) is a malicious
executable that calls `setuid(0)` to restore root
3. When the insights operator triggers a scan, the DaemonSet forks, enters the
attacker’s mount namespace, and executes the attacker’s binary with ruid=0
4. The attacker’s code regains full root with `CAP_SYS_ADMIN` in the host user
namespace
5. The inherited cwd contains the host CRI-O socket → full node compromise