There’s a move every sysadmin has seen at least once. The application throws a permission denied error. Somebody runs chmod 777 on the directory. The error goes away. Everyone moves on.
Except now that directory is world-writable. Every user, every service, every process — they can all read, modify, and execute everything inside it. If that directory has config files, any compromised service can rewrite them. If it has scripts, anyone can inject code.
File permissions are boring. Nobody gets excited about rwxr-xr--. But misconfigured permissions are consistently in the top vectors for Linux privilege escalation. Not because the attacks are sophisticated — because the mistakes are simple.
The Permission Model in 60 Seconds
Run ls -la and you see:
-rw-r--r-- 1 root root 1542 Mar 15 09:20 /etc/nginx/nginx.conf
drwx------ 2 deploy deploy 4096 Mar 15 09:22 /home/deploy/.ssh
Breaking down -rw-r--r--:
First character = file type (- file, d directory, l symlink). Then three groups: owner (rw-), group (r--), others (r--).
r= read (4),w= write (2),x= execute (1),-= none (0)
So -rw-r--r-- in numeric = 644: owner reads/writes, everyone else reads.
For directories, x means you can enter it and access files. A directory with r-- lets you list filenames but not read files. With --x you can access files if you know their names but not list them.
Sane defaults: 644 for files, 755 for directories.
Step 1: Find World-Writable Files
World-writable = anyone on the system can modify the file. Lowest-hanging fruit for attackers.
Find every world-writable file:
sudo find / -type f -perm -o+w \
-not -path "/proc/*" \
-not -path "/sys/*" \
-not -path "/dev/*" \
2>/dev/null
For directories:
sudo find / -type d -perm -o+w \
-not -path "/proc/*" \
-not -path "/sys/*" \
-not -path "/tmp" \
-not -path "/var/tmp" \
2>/dev/null
/tmp and /var/tmp are supposed to be world-writable. Everything else needs investigation.
Red flags:
- Config files (
.conf,.ini,.env) — anyone can change app behavior - Scripts (
.sh,.py,.php) — code injection vector - Cron files — writable cron scripts run as the cron user
- Web app files — writable PHP = direct code execution
Fix:
sudo chmod o-w /path/to/file
# Or set correct permissions
sudo chmod 644 /path/to/file
Step 2: Hunt for SUID and SGID Binaries
SUID means when you run this binary, it executes with the file owner’s permissions — not yours.
If root owns a SUID binary, anyone who runs it gets root-level execution. That’s by design for passwd (needs root for /etc/shadow) and sudo. The problem is unexpected SUID binaries.
Find all SUID:
sudo find / -type f -perm -4000 2>/dev/null
Find all SGID:
sudo find / -type f -perm -2000 2>/dev/null
A clean Linux server should have:
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/su
/usr/bin/mount
/usr/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/pkexec
Everything else is suspicious. GTFOBins lists Linux binaries exploitable with SUID — find, vim, python, bash, nmap, and many more.
If python3 had SUID set (it never should):
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'
Instant root shell.
Remove unnecessary SUID:
sudo chmod u-s /path/to/binary
Step 3: Check Sensitive Files
SSH files:
sudo stat -c '%a %U %G %n' /home/*/.ssh /home/*/.ssh/* 2>/dev/null
Required:
~/.ssh/— 700~/.ssh/authorized_keys— 644 or 600~/.ssh/id_ed25519(private key) — 600 (SSH refuses looser permissions)~/.ssh/config— 600
System credentials:
sudo stat -c '%a %U %G %n' /etc/passwd /etc/shadow /etc/group /etc/gshadow
/etc/passwd— 644 root:root/etc/shadow— 640 root:shadow (contains password hashes)/etc/group— 644 root:root/etc/gshadow— 640 root:shadow
If /etc/shadow is world-readable, any user can read hashes and attempt offline cracking.
Web server:
Web directories should be 755, files 644. Config files owned by root at 640. Never make web files writable by the web server user unless absolutely necessary (uploads directory only).
Step 4: Find Orphaned Files
Files without valid owners = deleted user accounts or improper management:
sudo find / -nouser -o -nogroup 2>/dev/null | grep -v '/proc\|/sys'
Risk: if a new user gets the recycled UID, they inherit all orphaned files.
Fix:
sudo chown root:root /path/to/orphaned-file
Step 5: Set Proper Defaults with umask
Check current:
umask
Common values:
- 022 — new files 644, directories 755 (standard)
- 027 — new files 640, directories 750 (others can’t read)
- 077 — new files 600, directories 700 (only owner)
For servers, 027 is ideal. Set in /etc/login.defs:
UMASK 027
For systemd services:
[Service]
UMask=0027
The Permission Audit Checklist
Run quarterly on every production server:
- World-writable files —
find / -type f -perm -o+w— fix everything outside /tmp - SUID/SGID binaries —
find / -perm -4000— compare against known-good list - SSH permissions — directories 700, private keys 600
- /etc/shadow — must be 640, never world-readable
- Web files — directories 755, files 644, config owned by root
- Orphaned files —
find / -nouser -o -nogroup— reassign - Verify umask — should be 022 or 027
For automated auditing, Lynis does comprehensive permission checks. Install with sudo apt install lynis and run sudo lynis audit system.
Permissions aren’t glamorous. Nobody puts “I audited file permissions” on conference slides. But I’ve seen production databases exposed because /etc/shadow was 644. I’ve seen web servers owned because deploy scripts left everything world-writable. I’ve seen root shells from SUID on python3.
The boring stuff prevents the exciting breaches.