Skip to content

Back to the catalog

bencteux.frwriteup

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.

TL;DR Calling setuid() before setgid() in SUID/SGID-root C programs leaves group privileges regainable — the fix is to drop setgid() first, then setuid().
Document

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 before setgid().
  • Exploitation to actually regain root group requires a separate code-execution primitive (the article names buffer overflows, printf() bugs) to call setgid(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.

Technologies named here

Techniques named here

  • Incorrect privilege relinquishing order
  • Privilege escalation via combined vulnerability