Privileges relinquishing order in C
Explains a subtle Linux privilege-dropping bug: calling setuid() before setgid() in a SUID/SGID-root binary leaves the saved set-group-ID privileged, letting a later setgid(0) call silently regain root group privileges due to POSIX semantics. The author provides compiled C PoC programs (drop_bad.c / drop_good.c) showing the wrong order failing to fully drop privileges versus the correct order (setgid then setuid) properly relinquishing them, and notes this becomes exploitable for privilege escalation when combined with a code-execution bug later in the program.
What it is
A technical write-up on the correct ordering of setuid()/setgid() when a SUID/SGID-root C program permanently drops privileges on Linux. It demonstrates, with compilable PoC binaries and their runtime UID/GID output, that dropping the user before the group leaves group privileges recoverable.
How it works
Per POSIX, setgid() sets the real GID, effective GID, and saved set-group-ID only if the process has appropriate privileges. If it does not, but gid matches the real GID or saved set-group-ID, it sets the effective GID and leaves the other two unchanged. So if setuid(getuid()) runs first, the effective UID becomes unprivileged before setgid() is called — the group IDs are never reset. A later setgid(0) then succeeds against the still-privileged saved set-group-ID, restoring EGID 0. The PoC makes this concrete: the bad-order binary prints EGID 0 after a supposed drop; the correct-order binary makes the same setgid(0) fail.
Preconditions / Affected
- A binary that is both SUID and SGID root (
chmod +s,-rwsr-sr-x). - It drops privileges with
setuid()called beforesetgid(). - Exploitation to actually regain root group requires a separate code-execution primitive (the article names buffer overflows,
printf()bugs) to callsetgid(0)after the flawed drop. Alone, wrong ordering is a latent flaw, not an exploit.
What to do
Drop the group before the user, and check every return value:
if (setgid(getgid()) == -1)
printf("setgid failed\n");
if (setuid(getuid()) == -1)
printf("setuid failed\n");With this order, the saved set-group-ID is cleared while still privileged, so a later setgid(0) fails and EGID stays at 1000. The article also notes that initgroups() or setgroups() should be called as part of a correct drop (it does not cover them further), and stresses that unchecked return values from these calls are themselves a security issue.
flowchart TD
A[SUID+SGID root process] --> B{Drop order?}
B -->|setuid then setgid| C[EGID dropped but saved set-group-ID still 0]
B -->|setgid then setuid| D[saved set-group-ID cleared while privileged]
C --> E[later setgid 0 succeeds -> EGID 0]
D --> F[later setgid 0 fails]Caveats
The article frames this as a privilege-escalation enabler, not a standalone vulnerability: without a follow-on code-execution bug the attacker has no way to issue the second setgid(). It explicitly sets initgroups()/setgroups() aside as out of scope while noting they matter.
Techniques named here
- Incorrect privilege relinquishing order
- Privilege escalation via combined vulnerability
Related in Offensive Security