
Audit World-Writable Files and SUID Binaries with Bash
Before you run this
This script reads your filesystem and reports two classes of risky files: world-writable files and directories (anyone on the box can modify them) and SUID/SGID binaries (they run with the owner's or group's privileges, often root). It writes a timestamped report and, optionally, a baseline you can diff against later. It does not change any permissions, delete anything, or modify a single file — it only runs find and writes a text report to a directory you choose.
Because of that, this is one of the safer scripts I publish. The caution is about acting on its output, not running it:
- Privileges: run it as root (or with
sudo). As a normal user,findcannot descend into directories it can't read, so you'll get a flood of "Permission denied" noise and an incomplete report. Root gives you a complete, quiet audit. - Read it first. It's short — read the whole thing before you run it, and run it on a test VM or a non-production host the first time so you know what its output looks like on your systems.
- The danger is in the follow-up. When you see a SUID binary or world-writable file and decide to
chmodorrmit, that is where you can break things. Legitimate SUID binaries exist (sudo,passwd,mount, and others). Never blanket-strip SUID bits or delete flagged files. Investigate each finding, and change permissions on one file at a time on a test host before touching production.
Assumed environment: Debian 12 or Ubuntu 22.04, GNU findutils (the find that ships by default), and bash. The find expressions here are GNU syntax. On RHEL/Alma the same find flags work; only the package manager and a few default paths differ.
What "world-writable" and "SUID" actually mean
A world-writable file has the o+w bit set — the "other" class can write to it. On a config file or a script that root runs, that's a privilege-escalation path. World-writable directories are fine only when they also carry the sticky bit (/tmp is the classic case); without the sticky bit, anyone can rename or delete other users' files in them.
SUID (u+s, mode 4000) and SGID (g+s, mode 2000) binaries execute with the file owner's or group's identity. That's necessary for tools like passwd, but every extra SUID-root binary is extra attack surface, so you want an inventory and you want to notice when it changes.
The audit script
Save this as perm-audit.sh, make it executable with chmod +x perm-audit.sh, and run it as root.
#!/usr/bin/env bash
#
# perm-audit.sh — read-only audit of world-writable files/dirs and SUID/SGID binaries.
# Writes a timestamped report. Changes nothing on disk except the report file.
set -euo pipefail
# Where to write the report. Replace with a path you control.
REPORT_DIR="/var/log/perm-audit"
# Pseudo-filesystems and network mounts we don't want to scan.
# -prune skips these subtrees entirely.
PRUNE=( -path /proc -o -path /sys -o -path /dev -o -path /run )
mkdir -p "$REPORT_DIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
REPORT="$REPORT_DIR/report-$STAMP.txt"
{
echo "Permission audit on $(hostname) at $(date)"
echo "======================================================"
echo
echo "## SUID binaries (mode 4000)"
# -perm -4000 matches any file with at least the SUID bit set.
find / \( "${PRUNE[@]}" \) -prune -o -type f -perm -4000 -print
echo
echo "## SGID binaries (mode 2000)"
find / \( "${PRUNE[@]}" \) -prune -o -type f -perm -2000 -print
echo
echo "## World-writable files"
# -perm -0002 matches any file with the 'other write' bit set.
find / \( "${PRUNE[@]}" \) -prune -o -type f -perm -0002 -print
echo
echo "## World-writable directories WITHOUT sticky bit (investigate these)"
# -perm -0002 = other-writable; ! -perm -1000 = sticky bit NOT set.
find / \( "${PRUNE[@]}" \) -prune -o -type d -perm -0002 ! -perm -1000 -print
} > "$REPORT"
echo "Report written to: $REPORT"
A few notes on the non-obvious parts:
\( ... \) -prune -o ... -printis the standard GNUfindidiom for "skip these subtrees, then apply the test to everything else." I prune/proc,/sys,/dev, and/runbecause they're kernel/pseudo filesystems and scanning them is meaningless.-perm -4000(note the leading dash) means "the SUID bit is set, regardless of the other bits." Without the dash,findwould match only files whose mode is exactly4000, which is not what you want.- I deliberately do not use
-xdev. If you want to restrict the scan to the root filesystem and skip all other mounts (NFS, big data volumes), add-xdevright afterfind /. That's the one common variant worth knowing.
Add detail to the findings
A bare list of paths is fine for a first pass, but for a report you'll actually read, you want owner, mode, and size. Swap the -print actions for -ls, which prints an ls -l-style line for each match. For example, the SUID section becomes:
find / \( "${PRUNE[@]}" \) -prune -o -type f -perm -4000 -ls
-ls is a standard GNU find action; it needs no extra tooling.
Turn it into a baseline you can diff
The real value is catching change — a new SUID binary appearing after an install or a compromise. Capture today's SUID inventory as a baseline:
# Create the baseline once, on a known-good system.
find / \( -path /proc -o -path /sys -o -path /dev -o -path /run \) -prune \
-o -type f -perm -4000 -print | sort > /var/lib/perm-audit/suid.baseline
Then compare a fresh scan against it:
find / \( -path /proc -o -path /sys -o -path /dev -o -path /run \) -prune \
-o -type f -perm -4000 -print | sort > /tmp/suid.now
# Lines only in the new scan = newly-appeared SUID binaries.
diff /var/lib/perm-audit/suid.baseline /tmp/suid.now
Anything diff reports with a > is a SUID binary that wasn't there when you took the baseline. That's your signal to investigate.
If you'd rather not roll your own baseline logic, this is exactly what dedicated tools like AIDE (Advanced Intrusion Detection Environment) do across the whole filesystem, and it's packaged for Debian/Ubuntu. Use the script above for a quick, dependency-free check; reach for AIDE when you want continuous, signed integrity monitoring.
Verify it worked
The script doesn't change your system, so "verification" means confirming the audit itself is sound:
- Confirm the report exists and has content:
ls -l /var/log/perm-audit/ less /var/log/perm-audit/report-*.txt - Sanity-check against a known SUID binary.
passwdis SUID-root on a normal system:
You should see anls -l /usr/bin/passwdsin the owner-execute position (-rwsr-xr-x) and that same path in your report's SUID section. If it's missing from the report, your scan pruned too much or didn't run as root. - Confirm you ran as root — if you saw
Permission deniedlines, re-run withsudo.
Undo
There is nothing to roll back: the script only creates a report file. To clean up, delete the report directory you chose:
rm -rf /var/log/perm-audit # only if you created it for this and want it gone
The moment you decide to act on a finding — say, removing an unexpected SUID bit with chmod u-s /path/to/file — record the original mode first (stat -c '%A %a %n' /path/to/file) so you can restore it. But that's a separate, deliberate change, made one file at a time, after you've confirmed the file is genuinely unwanted. For the exact behaviour of find's -perm and -prune, see the GNU findutils manual (man find, or the "Finding Files" manual on the GNU project site).
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
