Linux hosts

Finding and removing setuid binaries, the secure way

Every setuid-root binary is a small door into root that the system leaves unlocked on purpose, trusting the program to check who is knocking. Most hosts have a dozen of these doors, and nobody remembers why half of them are there.

The short answer

Inventory with find / -xdev -type f \( -perm -4000 -o -perm -2000 \) and getcap -r /. Remove the setuid bit from programs no one on the host needs, such as chfn, chsh and newgrp on servers. On Debian and Ubuntu use dpkg-statoverride --update so package upgrades do not put the bit back. Review the list after every install.

Updated Houssam Hammoudi, CTOTested with Debian 12 (dpkg 1.21, passwd 4.13), Alpine 3.22

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

A program with the setuid bit runs with the rights of the file's owner, not the rights of the user who started it. When the owner is root, any bug in that program, a buffer overflow or a bad environment variable, is a path to root for every local user.

Some of these programs are needed: passwd has to write /etc/shadow, su has to switch users. Many are not needed on a server: chfn changes the "full name" field, chsh changes a login shell, newgrp switches groups for people who still do that by hand.

Two things make this worse over time. New packages add new setuid files quietly. And removing the bit with chmod u-s is undone at the next package upgrade, because the package puts its files back exactly as they shipped.

What the docs say

If the set-user-ID bit is set on the program file referred to by path, then the effective user ID of the calling process is changed to that of the owner of the program file.

Source: execve(2)

This can be used to force programs that are normally setuid to be install without a setuid flag, or only executable by a certain group.

Source: dpkg-statoverride(1)

The Debian tool is documented, but most hardening guides show only chmod. The chmod change looks right until the next security update reinstalls the package.

The secure configuration

Inventory first. Include setgid files and file capabilities, which give part of root's power without the setuid bit:

bash
# Setuid (4000) and setgid (2000) files on the root filesystem.
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -exec ls -l {} + 2>/dev/null
# Repeat -xdev per mounted filesystem you care about (/home, /var, /opt).
# File capabilities: e.g. cap_net_raw on ping.
getcap -r / 2>/dev/null
# Who owns each file (Debian, Ubuntu). After /usr merge, some are known as /bin/...
dpkg -S /usr/bin/chfn /bin/su

Remove the bit in a way that survives upgrades:

bash
# Debian, Ubuntu: record an override; --update applies it now.
dpkg-statoverride --update --add root root 0755 /usr/bin/chfn
dpkg-statoverride --update --add root root 0755 /usr/bin/chsh
dpkg-statoverride --update --add root root 0755 /usr/bin/newgrp
dpkg-statoverride --list
bash
# Or keep the bit but limit who can run it: only group "admins".
dpkg-statoverride --update --add root admins 4750 /usr/bin/su

On RPM systems (Fedora, RHEL, Rocky) there is no statoverride. Apply the chmod from configuration management on every run, or remove the package if nothing needs it. rpm -V <package> shows M for a file whose mode no longer matches the package, which is also a useful audit.

Keep the list as a baseline and compare it after each change:

bash
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null | sort > /root/suid-baseline.txt
# later:
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null | sort | diff /root/suid-baseline.txt -

Prove it

The inventory on a stock Debian 12 image, with ping added to show a file capability:

bash
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -exec ls -l {} + 2>/dev/null | awk '{print $1, $3, $4, $9}'
text
-rwxr-sr-x root shadow /usr/bin/chage
-rwsr-xr-x root root /usr/bin/chfn
-rwsr-xr-x root root /usr/bin/chsh
-rwxr-sr-x root shadow /usr/bin/expiry
-rwsr-xr-x root root /usr/bin/gpasswd
-rwsr-xr-x root root /usr/bin/mount
-rwsr-xr-x root root /usr/bin/newgrp
-rwsr-xr-x root root /usr/bin/passwd
-rwsr-xr-x root root /usr/bin/su
-rwsr-xr-x root root /usr/bin/umount
-rwxr-sr-x root shadow /usr/sbin/unix_chkpwd
bash
getcap -r / 2>/dev/null
text
/usr/bin/ping cap_net_raw=ep

chmod alone, then a package reinstall. The bit comes back:

bash
chmod u-s /usr/bin/chfn; stat -c "%A %n" /usr/bin/chfn
apt-get install -y --reinstall passwd
stat -c "%A %n" /usr/bin/chfn
text
-rwxr-xr-x /usr/bin/chfn
-rwsr-xr-x /usr/bin/chfn

With dpkg-statoverride, the change survives the reinstall of both packages:

bash
for f in /usr/bin/chfn /usr/bin/chsh /usr/bin/newgrp; do dpkg-statoverride --update --add root root 0755 "$f"; done
dpkg-statoverride --list
apt-get install -y --reinstall passwd login
stat -c "%A %n" /usr/bin/chfn /usr/bin/chsh /usr/bin/newgrp
find / -xdev -type f -perm -4000 2>/dev/null | wc -l
text
root root 755 /usr/bin/newgrp
root root 755 /usr/bin/chsh
root root 755 /usr/bin/chfn
-rwxr-xr-x /usr/bin/chfn
-rwxr-xr-x /usr/bin/chsh
-rwxr-xr-x /usr/bin/newgrp
5

For comparison, the same search on alpine:3.22:

bash
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null | wc -l
text
0

The test script is secure-tests/finding-removing-setuid-binaries/run.sh.

Mistakes people make

chmod and walk away

The next apt upgrade or dnf update of that package restores the original mode. Use dpkg-statoverride on Debian and Ubuntu, and configuration management everywhere else.

Forgetting setgid and capabilities

A setgid shadow program can read password hashes. A binary with cap_dac_read_search can read any file. Neither shows up in a search for -perm -4000 alone.

Removing the bit from passwd or su

Users can no longer change their own password, and su stops working. Decide per program: if nobody on the host needs it, remove it; if admins need it, limit it to an admin group with mode 4750.

Searching only the root filesystem

-xdev stays on one filesystem, which keeps the search fast and skips /proc. Run it again for each other mounted filesystem, or mount the ones users can write to with nosuid so the question does not arise.

Checklist

  • A list of every setuid and setgid file exists, with the owning package for each.
  • getcap -r / has been reviewed.
  • chfn, chsh and newgrp have no setuid bit on servers.
  • Every removal on Debian or Ubuntu is recorded with dpkg-statoverride.
  • su is limited to an admin group or kept only where needed.
  • User-writable filesystems are mounted with nosuid.
  • The list is compared to a baseline after each package change.

You cannot audit a door you do not know exists. Make the list, shorten it, and make sure the package manager does not quietly reopen what you closed.

FND

Learn it on a live range

Linux 1: the command line to a working, secure host, in Foundation: a real host in your browser, and every objective checked on the machine.

Start free

The Secure Way

More on linux hosts

SSH, sudo, firewalls, mount options, updates and the host basics every engineer should get right.

All linux hosts guides