Safe Penetration Testing Labs · advanced · ~25 min
Run a defensive checklist over any setuid binary.
A setuid program runs with the privileges of the file's owner, not the user who launched it. If the owner is root, even a tiny bug can hand an attacker full control of the machine.
Auditing a setuid binary is a checklist. You walk it item by item. Anything that is not handled correctly counts as a finding.
During an authorised security assessment, you will often find setuid binaries on a target system.
The process is direct:
Each gap you find is something an attacker could use to gain higher privileges.
1. Is the environment scrubbed?
The environment is the set of variables a process inherits (for example $PATH). A setuid binary that trusts variables like $PATH, $LD_PRELOAD, $LD_LIBRARY_PATH, $IFS, or $ENV can be compromised. An attacker controls these values before launching the program.
Always call clearenv first, then set only known-safe values.
2. Are file descriptors 0, 1, and 2 open?
These are standard input, output, and error. They should always be open. If one is closed, the next open call reuses that low number. A later printf (which writes to descriptor 1) could then write into one of your own data files instead of the screen.
3. Does the code use getuid() vs geteuid() correctly?
Both matter:
geteuid returns the effective user id — the elevated identity the program runs as.getuid returns the real user id — the caller who launched it.Making an authorisation decision against the wrong one is a classic security bug (CVE).
4. Are there any file races?
This follows the TOCTOU (time-of-check to time-of-use) and symlink lessons. Every path operation must use either openat with O_NOFOLLOW, or realpath followed by a prefix check.
5. Does it run external commands?
system(3) and popen(3) pass the current environment to the child process. If you must spawn a program, use execve with a sanitised envp.
Walk the list explicitly. Each "yes, this is a problem" is a finding worth its own separate write-up.
uid_t getuid(void); /* real uid — the caller */
uid_t geteuid(void); /* effective uid — privileges in force */
int setresuid(uid_t r, uid_t e, uid_t s); /* drop privileges for good */
int clearenv(void); /* remove all environment variables */
A setuid binary runs with the file owner's privileges, not the caller's.
When the owner is root, these binaries become small doorways into root access. Auditing one is a focused checklist:
PATH inheritancegetuid() / geteuid() distinction/* Defensive setuid program start-up: */
if (clearenv() != 0) exit(1); /* strip ALL env first */
setenv("PATH", "/usr/bin:/bin", 1); /* known-safe PATH */
if (setresuid(geteuid(), geteuid(), geteuid()) < 0) exit(1);
/* Drop privs entirely the moment the elevated work is done */
uid_t real = getuid();
if (setresuid(real, real, real) < 0) {
perror("setresuid"); exit(1);
}
/* Now we run as the original caller — much less attack surface */
argv[0] for the program name. An attacker can set it to anything.Run ls -l /usr/bin/sudo. You will see -rwsr-xr-x. The s in that permission string is the setuid bit.
To list every setuid file on a system:
find / -perm -4000 -type f 2>/dev/null
The checklist is not the whole story. The usual memory-safety rules still apply: use bounded copies, validate all input, and so on.
Common setuid programs include sudo, su, ping (historically), mount (historically), and passwd.
Each one ships with this kind of audit already built in. Review their source to see the patterns in real code.
/usr/bin/sudo (it is open source).clearenv matters by exploiting a program through LD_PRELOAD.geteuid for the elevated id and getuid for the caller; never confuse them in auth decisions.