CertC-POS37ΒΆ
Ensure that privilege relinquishment is successful
Required inputs: IR
The POSIX
setuid() function has complex semantics and platform-specific
behavior [
Open
Group 2004].
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
uidis 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 meaning of "appropriate privileges" varies from platform to platform. For
example, on Solaris, appropriate privileges for
setuid() means that the
PRIV_PROC_SETID privilege is in the effective privilege set of the
process. On BSD, it means that the effective user ID (EUID) is zero (that is,
the process is running as root) or that
uid=geteuid(). On Linux, it means that the process has
CAP_SETUID capability and that
setuid(geteuid()) will fail if the EUID is not equal to 0,
the real user ID (RUID), or the saved set-user ID (SSUID).
Because of this complex behavior, desired privilege drops sometimes may fail.
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.
Noncompliant Code Example
This noncompliant code example compiles cleanly on most POSIX systems, but no explicit checks are 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,
* but if privilege relinquishment failed,
* attacker can regain elevated privileges!
*/
If the program is run as a setuid root program, over time, the state of the UIDs might look like the following:
| Description | Code | EUID | RUID | SSUID |
|---|---|---|---|---|
| Program startup | 0 | User | 0 | |
| Temporary drop | seteuid(getuid()) |
User | User | 0 |
| Restore | seteuid(0) |
0 | User | 0 |
| Permanent drop | setuid(getuid()) |
User | User | User |
| Restore (attacker) | setuid(0) (fails) |
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 | seteuid(getuid()) |
User | User | 0 |
| Restore | seteuid(0) |
User | User | 0 |
| Permanent drop | setuid(getuid()) |
User | User | 0 |
| Restore (attacker) | setuid(0) |
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
2003]. If the
setuid() call succeeds after (supposedly) dropping privileges
permanently, then the 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;
* attacker cannot regain elevated privileges
*/
Compliant Solution
A better solution is to ensure that proper privileges exist before attempting to perform a permanent drop:
/* Store the privileged ID for later verification */
uid_t privid = geteuid();
/* 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(privid) != 0) {
/* Handle error */
}
/* Code intended to run with elevated privileges */
}
/* ... */
/* Restore privileges if needed */
if (geteuid() != privid) {
if (seteuid(privid) != 0) {
/* Handle error */
}
}
/* 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;
* attacker cannot regain elevated privileges
*/
Supplementary Group IDs
A process may have a number of supplementary group IDs, in addition to its
effective group ID, and the supplementary groups can allow privileged access to
files. The
getgroups() function returns an array that contains the
supplementary group IDs and can also contain the effective group ID. The
setgroups() function can set the supplementary group IDs and can
also set the effective group ID on some systems. Using
setgroups() usually requires privileges. Although POSIX
defines the
getgroups() function, it does not define
setgroups().
Under normal circumstances,
setuid() and related calls do not alter the supplementary group
IDs. However, a setuid-root program can alter its supplementary group IDs and
then relinquish root privileges, in which case, it maintains the supplementary
group IDs but lacks the privilege necessary to relinquish them. Consequently,
it is recommended that a program immediately relinquish supplementary group IDs
before relinquishing root privileges.
POS36-C.
Observe correct revocation order while relinquishing privileges discusses
how to drop supplementary group IDs. To ensure that supplementary group IDs are
indeed relinquished, you can use the following
eql_sups function:
/* Returns nonzero if the two group lists are equivalent (taking into
account that the lists may differ wrt the egid */
int eql_sups(const int cursups_size, const gid_t* const cursups_list,
const int targetsups_size, const gid_t* const targetsups_list) {
int i;
int j;
const int n = targetsups_size;
const int diff = cursups_size - targetsups_size;
const gid_t egid = getegid();
if (diff > 1 || diff < 0 ) {
return 0;
}
for (i=0, j=0; i < n; i++, j++) {
if (cursups_list[j] != targetsups_list[i]) {
if (cursups_list[j] == egid) {
i--; /* skipping j */
} else {
return 0;
}
}
}
/* If reached here, we're sure i==targetsups_size. Now, either
j==cursups_size (skipped the egid or it wasn't there), or we didn't
get to the egid yet because it's the last entry in cursups */
return j == cursups_size ||
(j+1 == cursups_size && cursups_list[j] == egid);
}
System-Specific Capabilities
Many systems have nonportable privilege capabilities that, if unchecked, can yield privilege escalation vulnerabilities. The following section describes one such capability.
File System Access Privileges (Linux)
Processes on Linux have two additional values called
fsuid and
fsgid. These values indicate the privileges used when accessing
files on the file system. They normally shadow the effective user ID and
effective group ID, but the
setfsuid() and
setfsgid() functions allow them to be changed. Because changes to
the
euid and
egid normally also apply to
fsuid and
fsgid, a program relinquishing root privileges need not be
concerned with setting
fsuid or
fsgid to safe values. However, there has been at least one kernel
bug that violated this invariant ([
Chen
2002] and [
Tsafrir
2008]). Consequently, a prudent program checks that
fsuid and
fsgid have harmless values after relinquishing privileges.
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 Guidelines
| Taxonomy | Taxonomy item | Relationship |
|---|---|---|
| ISO/IEC TR 24772 | Privilege Sandbox Issues [XYO] | Prior to 2018-01-12: CERT: Unspecified Relationship |
| CWE 2.11 | CWE-273, Failure to check whether privileges were dropped successfully | 2017-07-07: CERT: Exact |
Bibliography
| [ Chen 2002] | "Setuid Demystified" |
| [ Dowd 2006] | Chapter 9, "Unix I: Privileges and Files" |
| [ Open Group 2004] |
setuid()
getuid()
seteuid()
|
| [ Tsafrir 2008] | "The Murky Issue of Changing Process Identity: Revising 'Setuid Demystified'" |
| [ Wheeler 2003] | Section 7.4, "Minimize Privileges" |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
check_privilege_drop |
setuid(getuid()) call must be followed by a setuid(0) != -1 check. |
None |
False |
Options
This rule shares the following common options: exclude_in_macros, exclude_messages_in_system_headers, excludes, extend_exclude_to_macro_invocations, includes, justification_checker, languages, post_processing, provider, report_at, severity
The following places define options that affect this rule: Stylechecks, Analysis-GlobalOptions
This rule has no individual options.