Privilege Dropping / Lowering Errors
Description
Privilege Dropping / Lowering Errors is a vulnerability class that occurs when a product fails to reduce privileges before transferring control of a resource to an actor that lacks those elevated permissions. When a system with elevated permissions hands off processes, files, or other resources to another process or user without first dropping its elevated privileges, those elevated privileges spread throughout the system. This creates vulnerability to privilege escalation attacks, as the receiving actor gains access to capabilities far beyond what was intended.
Risk
Failure to properly drop privileges before resource handoff creates severe security risks by allowing privilege escalation across trust boundaries. When setuid programs, daemons, or services retain elevated privileges longer than necessary, any vulnerability in the code (such as buffer overflows, command injection, or path traversal) can be exploited with those elevated privileges. The risk is particularly acute in Unix/Linux systems where root privileges provide complete system access. Child processes that inherit elevated privileges can be exploited to compromise the entire system. Additionally, resources created with elevated privileges (files, network connections, shared memory) may have incorrect permissions, allowing unauthorized access even after the creating process terminates.
Solution
Implement separation of privilege through system compartmentalization with clear trust boundaries. Drop privileges immediately after completing operations that require them, following the principle of least privilege. Use elevated permissions only when necessary, then relinquish them before performing any operations that could be influenced by untrusted input. When implementing privilege dropping, address all aspects: user ID, group ID, supplementary groups, capabilities, and any process-inheritable attributes. Verify that privilege-dropping operations succeeded by checking return values and confirming the new privilege state. Close or properly protect any resources (file handles, sockets, shared memory) created with elevated privileges before dropping to lower privilege levels. Design privilege separation architectures where only minimal code runs with elevated privileges, with the bulk of application logic running unprivileged.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Retained privileges prevent access rights from being properly restricted. Actors that should have limited permissions operate with elevated access, enabling unauthorized resource access, system modification, and privilege escalation through any subsequent vulnerability. |
| Non-Repudiation | Scope: Access Control, Non-Repudiation The system may attribute actions to the impersonated user rather than the actual actor, compromising audit trails and accountability. Actions performed with inherited privileges may be logged under incorrect identities. |
Example Code
Vulnerable Code (C)
The following examples demonstrate privilege dropping errors:
// Vulnerable: Continues running as root after chroot
#include <stdio.h>
#include <unistd.h>
int vulnerable_chroot_service(char *app_home, char *filename) {
// Restrict filesystem view
chroot(app_home);
chdir("/");
// Vulnerable: Still running as root!
// Any vulnerability here gives attacker root access
FILE* data = fopen(filename, "r+");
if (data != NULL) {
process_file(data); // Buffer overflow here = root compromise
fclose(data);
}
return 0;
}
// Vulnerable: Setuid program doesn't drop privileges before exec
int vulnerable_exec_program(char *program_path) {
// Running as root due to setuid bit
// Vulnerable: Executes external program with root privileges
// The PATH or program could be controlled by attacker
char *env_program = getenv("HELPER_PROGRAM");
if (env_program != NULL) {
execve(env_program, NULL, NULL); // Runs as root!
}
return 0;
}
// Vulnerable: Doesn't drop group privileges
int vulnerable_partial_drop(uid_t target_uid) {
// Drop user privilege
setuid(target_uid);
// Vulnerable: Supplementary groups not dropped!
// Process may still be member of privileged groups
// Vulnerable: Original group ID not changed!
// getgid() still returns root group
do_sensitive_operation();
return 0;
}
# Vulnerable: Python service retaining root privileges
import os
import subprocess
class VulnerableService:
def start_worker(self, task_config):
# Main process running as root
pid = os.fork()
if pid == 0:
# Child process - should be unprivileged worker
# Vulnerable: No privilege drop!
# Worker runs with root privileges
# User-controlled configuration
script_path = task_config.get('script')
subprocess.run(['/bin/bash', script_path]) # Runs as root!
def create_log_file(self, log_path):
# Vulnerable: Creates file as root, then drops privileges
# File is now owned by root with root permissions
with open(log_path, 'w') as f:
f.write("Log started\n")
# Now drop privileges
os.setuid(unprivileged_uid)
# Vulnerable: Log file still owned by root
# Process can no longer write to its own log!
# Or worse: log file writable by others due to umask
Fixed Code (C)
// Fixed: Proper privilege dropping
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <grp.h>
#include <pwd.h>
#include <stdlib.h>
int drop_privileges(const char *username) {
struct passwd *pw = getpwnam(username);
if (pw == NULL) {
return -1;
}
// Drop supplementary groups first
if (setgroups(0, NULL) != 0) {
return -1;
}
// Drop group privilege (must happen while still root)
if (setgid(pw->pw_gid) != 0) {
return -1;
}
// Drop user privilege last
if (setuid(pw->pw_uid) != 0) {
return -1;
}
// Verify the drop succeeded
if (getuid() != pw->pw_uid ||
geteuid() != pw->pw_uid ||
getgid() != pw->pw_gid ||
getegid() != pw->pw_gid) {
return -1;
}
// Verify we cannot regain root
if (setuid(0) != -1) {
// This should fail! If it succeeds, privilege drop failed
return -1;
}
return 0;
}
int secure_chroot_service(char *app_home, char *filename, char *run_as_user) {
// Open resources needed while still privileged
// (if any resources need root access)
// Restrict filesystem view
if (chroot(app_home) != 0) {
return -1;
}
if (chdir("/") != 0) {
return -1;
}
// NOW drop privileges
if (drop_privileges(run_as_user) != 0) {
fprintf(stderr, "Failed to drop privileges\n");
return -1;
}
// Safe: Now running as unprivileged user
FILE* data = fopen(filename, "r+");
if (data != NULL) {
process_file(data); // Any exploit limited to unprivileged user
fclose(data);
}
return 0;
}
int secure_exec_program(char *program_path, uid_t run_as_uid, gid_t run_as_gid) {
// Validate the program path - must be absolute and in allowed list
if (!is_allowed_program(program_path)) {
return -1;
}
// Drop privileges BEFORE exec
if (setgroups(0, NULL) != 0 ||
setgid(run_as_gid) != 0 ||
setuid(run_as_uid) != 0) {
return -1;
}
// Verify drop succeeded
if (getuid() != run_as_uid || geteuid() != run_as_uid) {
return -1;
}
// Clear environment for safety
char *safe_env[] = {"PATH=/usr/bin", NULL};
// Safe: External program runs unprivileged
execve(program_path, NULL, safe_env);
// If we get here, exec failed
return -1;
}
# Fixed: Python service with proper privilege dropping
import os
import subprocess
import pwd
import grp
class SecureService:
def drop_privileges(self, username):
"""Drop all root privileges"""
pw = pwd.getpwnam(username)
# Drop supplementary groups
os.setgroups([])
# Drop group then user privileges
os.setgid(pw.pw_gid)
os.setuid(pw.pw_uid)
# Verify
if os.getuid() != pw.pw_uid or os.getgid() != pw.pw_gid:
raise RuntimeError("Failed to drop privileges")
# Verify we can't regain root
try:
os.setuid(0)
raise RuntimeError("Privilege drop verification failed")
except PermissionError:
pass # Expected - we should not be able to setuid(0)
def start_worker(self, task_config, worker_user):
# Main process running as root
pid = os.fork()
if pid == 0:
# Child process
# Drop privileges FIRST
self.drop_privileges(worker_user)
# Validate script path
script_path = task_config.get('script')
if not self.is_allowed_script(script_path):
os._exit(1)
# Safe: Now running as unprivileged user
subprocess.run(['/bin/bash', script_path])
os._exit(0)
def create_log_file(self, log_path, owner_user):
"""Create log file with correct ownership"""
pw = pwd.getpwnam(owner_user)
# Create file with restrictive permissions
old_umask = os.umask(0o077)
try:
fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o640)
os.write(fd, b"Log started\n")
os.fchown(fd, pw.pw_uid, pw.pw_gid) # Change ownership
os.close(fd)
finally:
os.umask(old_umask)
# Now safe to drop privileges
self.drop_privileges(owner_user)
The fix ensures privileges are dropped before any untrusted input is processed, verifies privilege drops succeeded, and handles all aspects of privilege (user, group, supplementary groups).
Exploited in the Wild
Setuid Program Exploits (Unix/Linux Systems, Ongoing)
Numerous setuid programs have been exploited because they failed to drop privileges before processing user input or executing external programs. Attackers use environment variable manipulation, symlink attacks, and input injection to gain root access through these elevated processes.
Container Runtime Privilege Escalation (Container Platforms, 2019-Present)
Container runtimes have experienced privilege dropping errors where containers intended to run as unprivileged users retained root capabilities. Notable examples include CVE-2019-5736 in runc, where a malicious container could overwrite the host runc binary due to insufficient privilege isolation.
Daemon Privilege Retention (Various Services, Historical)
Network daemons that started as root for port binding but failed to drop privileges before handling client connections have been exploited. Vulnerabilities in the connection handling code provided attackers with root access due to retained privileges.
Tools to Test/Exploit
-
PEDA/GEF — GDB extensions for exploit development that help identify privilege state during debugging.
-
Lynis — Security auditing tool that identifies setuid programs and privilege configuration issues.
-
checksec — Script for checking security properties of binaries including setuid configuration.
CVE Examples
-
CVE-2004-2504 — Windows program running as SYSTEM executes other programs without dropping privileges.
-
CVE-2004-0806 — Setuid program fails to drop privileges before executing environment-specified program.
-
CVE-2000-1213 — Program doesn't drop privileges after acquiring raw socket.
-
CVE-2001-0787 — Failure to drop privileges in related groups during privilege lowering.
References
-
MITRE Corporation. "CWE-271: Privilege Dropping / Lowering Errors." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/271.html
-
Chen, H., Wagner, D., and Dean, D. "Setuid Demystified." USENIX Security Symposium. https://www.usenix.org/legacy/events/sec02/full_papers/chen/chen.pdf
-
CERT C Secure Coding Standard. "POS36-C. Observe correct revocation order while relinquishing privileges." https://wiki.sei.cmu.edu/confluence/display/c/POS36-C