Skip to main content
Linux · lesson 2 of 7
Linux intermediate Lesson 2 of 7

Also part of: Bash Security Fundamentals Ethical Hacking (methodology, lab-only)

Permission Bits, Ownership, and Why passwd Needs setuid

A real permission-denied error from a locked-down file, then stripping the setuid bit from /usr/bin/passwd to see exactly why an ordinary user can't change their own password without it.

By SkillByExample · Updated

linux permissions setuid ownership

Two related mechanisms, both enforced by the kernel on every file access: ordinary permission bits, and the special setuid bit that lets exactly one kind of privilege escalation happen safely.

A real permission-denied error

docker exec linux-lesson bash -c "useradd -m -s /bin/bash alice && mkdir /data && echo 'secret content' > /data/report.txt && chmod 600 /data/report.txt && ls -la /data"
total 12
drwxr-xr-x 2 root root 4096 Sep 14 06:01 .
drwxr-xr-x 1 root root 4096 Sep 14 06:01 ..
-rw------- 1 root root   15 Sep 14 06:01 report.txt

chmod 600 sets owner to read+write (4+2), and both group and everyone-else to nothing — exactly the -rw------- shown. alice is neither the file’s owner (root) nor in its owning group:

docker exec --user alice linux-lesson cat /data/report.txt
cat: /data/report.txt: Permission denied

Not a missing-file error — alice can see report.txt exists (the directory itself is world-readable), but opening its contents is a completely separate check, and her three permission bits on this file are all zero.

docker exec linux-lesson chmod 644 /data/report.txt
docker exec --user alice linux-lesson cat /data/report.txt
secret content

644 adds read access for group and others — now alice’s “others” bit grants her cat.

setuid: a program that runs as someone else

docker exec linux-lesson ls -la /usr/bin/passwd
-rwsr-xr-x 1 root root 59976 Feb  6  2024 /usr/bin/passwd

That lowercase s where owner-execute would normally be an x is the setuid bit — passwd runs with root’s privileges for the duration of the process, no matter which user launched it. This has to exist: changing your own password means writing to /etc/shadow, a file only root can write, and an ordinary user needs some sanctioned way to trigger that write without being handed root access generally.

The bug: stripping setuid breaks passwd for everyone but root

docker exec linux-lesson chmod u-s /usr/bin/passwd
docker exec linux-lesson ls -la /usr/bin/passwd
-rwxr-xr-x 1 root root 59976 Feb  6  2024 /usr/bin/passwd

Plain x now, no s. alice tries to change her own password:

docker exec --user alice linux-lesson bash -c "printf 'oldpass123\nnewpass456\nnewpass456\n' | passwd"
Changing password for alice.
Current password: New password: Retype new password: passwd: Authentication token manipulation error
passwd: password unchanged

Without setuid, passwd runs with alice’s own ordinary privileges the entire time — including the moment it tries to write her new password hash into /etc/shadow. That write is refused by the kernel, exactly as it would be for any other program alice ran, because /etc/shadow grants write access to nobody but root. Correct current password, correct new password twice — none of that matters, because the actual failure is a permission check on a file alice was never trying to open directly.

The fix: restore setuid

docker exec linux-lesson chmod u+s /usr/bin/passwd
docker exec --user alice linux-lesson bash -c "printf 'oldpass123\nnewpass456\nnewpass456\n' | passwd"
Changing password for alice.
Current password: New password: Retype new password: passwd: password updated successfully

Same command, same user, same password values — the only thing that changed is whether the kernel elevates passwd’s privileges while it runs.

Practice

1. Find every setuid binary currently on the system.
docker exec linux-lesson find /usr/bin /usr/sbin -perm -4000 2>/dev/null
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/su
/usr/bin/chfn
/usr/bin/umount
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/chsh
/usr/bin/sudo

-perm -4000 matches the setuid bit specifically — this is the real command a security audit runs to find every binary capable of privilege escalation, worth periodically reviewing on any system for binaries that don’t actually need it.

2. Give a group, rather than "others", access to the restricted file.
docker exec linux-lesson bash -c "chmod 600 /data/report.txt && chgrp alice /data/report.txt && chmod 640 /data/report.txt"
docker exec --user alice linux-lesson cat /data/report.txt
secret content

chmod 640 grants read to owner and group, nothing to others — chgrp alice first makes alice’s own group the file’s group, so this is a narrower grant than 644’s “everyone can read” from earlier.

3. Confirm that only the setuid bit changed, not the binary's other permissions, when it was stripped and restored.
docker exec linux-lesson stat -c '%A %U:%G' /usr/bin/passwd
-rwsr-xr-x root:root

Owner, group, and the r-x for group/others are unchanged from before either chmod call — only the single setuid bit toggled, confirming the fix didn’t accidentally alter anything else about the binary.

Next: processes at the OS level — zombies, orphans, and what /proc actually shows about a running process.

Frequently Asked Questions

What do the three permission groups in ls -l actually mean?
Reading left to right after the file type character: owner permissions, group permissions, then everyone-else permissions — each group is three characters (read, write, execute). chmod 600 sets owner to read+write (6 = 4+2) and both group and others to nothing (0), which is exactly the -rw------- this lesson's report.txt started with.
Why did alice get Permission denied on a file she could see existed?
Being able to list a file (via ls in its parent directory, assuming the directory itself is readable) and being able to open its contents are governed by completely separate permission checks. report.txt's mode 600 grants read access only to its owner (root) — alice, as neither the owner nor in the owning group, has none of the three permission bits set for her, so the kernel refuses the open() call outright.
What does the s in -rwsr-xr-x actually do?
It's the setuid bit — when set on an executable, the program runs with the privileges of the file's owner (here, root) rather than the user who launched it, for the duration of that process only. passwd needs this because changing a password means writing to /etc/shadow, a file only root can write, and an ordinary user has to be able to trigger that write without being handed general root access.
Why did passwd fail after removing its setuid bit, even though alice was only changing her own password?
Without setuid, passwd runs with alice's own regular-user privileges the whole time — including when it internally tries to write the updated hash to /etc/shadow. That write is refused by the kernel regardless of whose password is being changed, because /etc/shadow's permissions don't grant write access to anyone but root; setuid was the only thing bridging that gap.
Is stripping a setuid bit ever something you'd actually do on a real system?
Yes — it's a real hardening technique. A binary with setuid root is a bigger attack surface than the same binary without it, since a bug in that binary can potentially be exploited to gain root. Security audits routinely look for unnecessary setuid binaries (find / -perm -4000) and strip the bit from any that don't genuinely need elevated privileges for their normal job.