The POSIX setuid()
function has complex semantics and platform-specific behavior [[Open Group 04]]
If the process has appropriate privileges,
setuid()
shall set the real user ID, effective user ID, and the saved set-user-ID of the calling process touid
.If the process does not have appropriate privileges, but
uid
is equal to the real user ID or the saved set-user-ID,setuid()
shall set the effective user ID touid
; the real user ID and saved set-user-ID shall remain unchanged.
The phrase "appropriate privileges" varies from platform to platform. For example, on Solaris appropriate privileges means that EUID=0 (that is, the process is running as root). On BSD, it means that EUID=0 or that uid=geteuid()
. On Linux, it means that the process has CAP_SETUID
capability and that setuid(geteuid())
will fail if the effective EUID is not equal to 0, the RUID, or the SSUID.
Because of this complex behavior, there may be cases where the desired privilege drops are unsuccessful. For example, the range of Linux Kernel versions (2.2.0-2.2.15) is vulnerable to an insufficient privilege attack wherein setuid(getuid())
did not drop privileges as expected when the capability bits were set to zero. As a precautionary measure, subtle behavior and error conditions for the targeted implementation must be carefully noted.
Non-Compliant Code Example
This non-compliant code example compiles cleanly on most POSIX systems, however no explicit checks have been made to ensure that privilege relinquishment has succeeded. This may be dangerous depending on the sequence of the preceding privilege changes.
/* Code intended to run with elevated privileges */ /* Temporarily drop privileges */ if (seteuid(getuid()) != 0) { /* Handle error */ } /* Code intended to run with lower privileges */ if (need_more_privileges) { /* Restore privileges */ if (seteuid(0) != 0) { /* Handle Error */ } /* Code intended to run with elevated privileges */ } /* ... */ /* Permanently drop privileges */ if (setuid(getuid()) != 0) { /* Handle Error */ } /* Code intended to run with lower privileges */
If the program is run with as a setuid root program, the state of the UID
's over time might be:
Description |
Code |
EUID |
RUID |
SSUID |
---|---|---|---|---|
program startup |
|
0 |
user |
0 |
temporary drop |
|
user |
user |
0 |
restore |
|
0 |
user |
0 |
permanent drop |
|
user |
user |
user |
restore (attacker) |
|
user |
user |
user |
If the program fails to restore privileges, it will be unable to permanently drop them later:
Description |
Code |
EUID |
RUID |
SSUID |
---|---|---|---|---|
program startup |
|
0 |
user |
0 |
temporary drop |
|
user |
user |
0 |
restore |
|
user |
user |
0 |
permanent drop |
|
user |
user |
0 |
restore (attacker) |
|
0 |
0 |
0 |
Compliant Solution
This compliant solution was implemented in sendmail, a popular mail transfer agent, to determine if superuser privileges were successfully dropped [[Wheeler 03]]. If the setuid()
call succeeds after (supposedly) dropping privileges permanently , privileges were not dropped as intended.
/* Code intended to run with elevated privileges */ /* Temporarily drop privileges */ if (seteuid(getuid()) != 0) { /* Handle error */ } /* Code intended to run with lower privileges */ if (need_more_privileges) { /* Restore Privileges */ if (seteuid(0) != 0) { /* Handle error */ } /* Code intended to run with elevated privileges */ } /* ... */ /* Permanently drop privileges */ if (setuid(getuid()) != 0) { /* Handle error */ } if (setuid(0) != -1) { /* Privileges can be restored, handle error */ } /* Code intended to run with lower privileges */
Non-Compliant Code Example
The function in this non-compliant code example correctly follows the principle of least privilege, however, due to inconsistencies and implementation defined behavior of certain functions (such as setuid()
) across various operating systems, the final result may be unexpected. Here, when privileges are given up temporarily for the final time, the effective user ID of the process is set to the real user ID. Unexpectedly, the call to setuid(realuid)
that follows, does not affect the saved set-user-ID since effective UID is no longer 0 (Except on FreeBSD and NetBSD). If a seteuid(0)
gets executed maliciously after this statement, root privileges would be recovered from the saved set-user-ID.
void doSomething(void) { uid_t realuid = getuid(); seteuid(realuid); /* Give up privileges temporarily */ seteuid(0); /* Regain superuser privileges */ /* Carry out the privileged task */ seteuid(realuid); /* Give up privileges temporarily */ setuid(realuid); /* Failed attempt at giving up privileges permanently */ }
Compliant Solution
The following code shows how the effective UID should be obtained and compared against 0 (superuser's EUID) to make sure privileges can be successfully dropped permanently. This constitutes a more portable and safe solution.
void doSomething(void) { uid_t realuid = getuid(); seteuid(realuid); /* Give up privileges temporarily */ seteuid(0); /* Regain superuser privileges */ /* Carry out privileged task */ seteuid(realuid); /* Give up privileges temporarily */ if (!geteuid()) { /* Check if the effective uid is still that of the superuser */ setuid(realuid); /* Go ahead and give up privileges permanently */ } else { /* Handle the possible implementation defined behavior */ } }
Risk Assessment
If privilege relinquishment conditions are left unchecked, any flaw in the program may lead to unintended system compromise corresponding to the more privileged user or group account.
Rule |
Severity |
Likelihood |
Remediation Cost |
Priority |
Level |
---|---|---|---|---|---|
POS37-C |
high |
probable |
low |
P18 |
L1 |
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
References
[[CWE - 273]] Failure to Check Whether Privileges Were Dropped Successfully
[[Dowd 06]] Chapter 9, "Unix I: Privileges and Files"
[[Open Group 04]] setuid()
, getuid()
, seteuid()
[[Wheeler 03]] Section 7.4, "Minimize Privileges"