Insights-runtime-extractor: insights-runtime-extractor: container escape via privileged binary execution
What fixes it
No source in this snapshot names a fixed version. The vendor advisories under References are the place to check.
Description
The insights-runtime-extractor runs as a privileged DaemonSet (hostPID: true) that scans container processes for runtime fingerprinting. When scanning a container, the coordinator:
- fork()s a child process
- setns() into the target container's PID and mount namespaces
- fchdir() back to an orphaned FD pointing at the extractor's own root
filesystem (where /crio.sock is hostPath-mounted)
- 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
- 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
- The
VersionExecutablefingerprint invokesfpr_kind_executablewith
process.command_line[0] — the target's argv[0], read from /proc/<pid>/cmdline and fully controlled by the workload
fpr_kind_executablecallsexec.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
// 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);// Lines 182-185 — only effective UID changed; ruid/suid stay 0
fn switch_user(uid: u32) -> Result<(), ScannerError> {
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
// 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
// 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:
- Attacker creates a pod whose process name matches a configured
versioned-executable pattern (node, python, python3, MainThread, or contains java)
- The pod's main binary (at the path matching argv[0]) is a malicious
executable that calls setuid(0) to restore root
- 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
- The attacker's code regains full root with
CAP_SYS_ADMINin the host user
namespace
- The inherited cwd contains the host CRI-O socket → full node compromise
Text from CVE List
Affected versions 1
- Red Hat OpenShift Container Platform 4.21unaffected: 1792008935 and laterCVE List