Fixing Broken Sudoers: From NOPASSWD Script Abuse to Strict Least Privilege
Granting a user NOPASSWD access to a specific script feels like an acceptable compromise between operational convenience and system security. In practice, scoped sudo privileges regularly collapse into full root compromises because systems administrators audit the destination binary rather than its execution context. If a script executed via sudo relies on relative paths, unsafe environment variables, or writable sub-scripts, the boundary between restricted operator and root effectively disappears.
The Illusion of Scoped Sudo
A common pattern in enterprise environments is delegating routine operational tasks—such as clearing log files, triggering backups, or restarting microservices—to non-root service accounts or junior operators. To avoid sharing the root password or managing complex key rotation, administrators add targeted entries to /etc/sudoers.
The logic seems sound: restricting execution to /usr/local/bin/maintenance.sh limits the user to only the actions defined inside that file. However, sudo executes the target binary with effective UID 0 while maintaining parts of the invoking environment unless explicitly cleansed. If /usr/local/bin/maintenance.sh invokes secondary programs without absolute paths, sources external configuration files from world-writable locations, or fails to sanitize its execution context, NOPASSWD becomes functionally identical to unrestricted root access.
Setting Up the Vulnerable Scenario
To understand how this vector operates in a lab environment, consider a script created to clean up temporary build artifacts. The administrator creates /usr/local/bin/cleanup.sh with the following contents:
#!/bin/bash
# System cleanup script executed via sudo
TARGET_DIR="/var/log/app"
echo "Cleaning up build artifacts in $TARGET_DIR..."
cd $TARGET_DIR || exit 1
# Execute custom project cleanup hooks if present
if [ -f "./hooks/pre_clean.sh" ]; then
./hooks/pre_clean.sh
fi
rm -rf ./tmp/*
The administrator then grants the low-privileged account appuser passwordless execution rights by adding a line to /etc/sudoers via visudo:
appuser ALL=(ALL) NOPASSWD: /usr/local/bin/cleanup.sh
On the surface, appuser cannot run arbitrary commands as root; attempting sudo root or sudo bash prompts for a password that appuser does not possess.
Executing the Abuse Vector
The flaw in cleanup.sh lies in its execution of a relative path (./hooks/pre_clean.sh) within a directory that may contain user-controlled files or subdirectories. Because sudo runs the script as root, any child process spawned by the script inherits those root privileges.
An operator or attacker with write access to /var/log/app can exploit this behavior by constructing a malicious hook script:
# Navigate to the target directory where the cleanup script operates
cd /var/log/app
# Create the hooks directory structure expected by the maintenance script
mkdir -p hooks
# Create a payload script that grants administrative access
cat << 'EOF' > hooks/pre_clean.sh
#!/bin/bash
cp /bin/bash /tmp/root_bash
chmod u+s /tmp/root_bash
EOF
# Ensure the script is executable
chmod +x hooks/pre_clean.sh
When appuser executes the authorized sudo command:
sudo /usr/local/bin/cleanup.sh
The script changes directory into /var/log/app, detects ./hooks/pre_clean.sh, and executes it as root. The sub-shell copies /bin/bash to /tmp/root_bash and sets the SUID bit. The invoking user then executes /tmp/root_bash -p to acquire a root shell.
Hardening Sudoers and Restricting Execution
Remediating this vulnerability requires fixing both the underlying script and the sudoers configuration. Hardening must be applied at both layers to ensure defence-in-depth.
First, rewrite the script to enforce strict path resolution, explicit file ownership checks, and environment sanitization:
#!/bin/bash
set -euo pipefail
# Enforce a safe PATH and remove dangerous environment variables
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
TARGET_DIR="/var/log/app"
if [ ! -d "$TARGET_DIR" ]; then
echo "Error: Target directory does not exist." >&2
exit 1
fi
# Ensure working directory path is absolute
cd "$TARGET_DIR"
# Validate that hook script is owned strictly by root and not group/world-writable
HOOK_FILE="/var/log/app/hooks/pre_clean.sh"
if [ -f "$HOOK_FILE" ]; then
OWNER_UID=$(stat -c '%u' "$HOOK_FILE")
PERMS=$(stat -c '%a' "$HOOK_FILE")
if [ "$OWNER_UID" -eq 0 ] && [ "$PERMS" -le 755 ]; then
"$HOOK_FILE"
else
echo "Warning: Insecure permissions on hook script. Skipping execution." >&2
fi
fi
/bin/rm -rf -- "$TARGET_DIR/tmp/"*
Second, update /etc/sudoers to enforce secure_path and restrict variable passing. Always edit this file using visudo to prevent syntax errors from locking administrators out of the system.
Ensure the global defaults reset dangerous environment variables:
Defaults env_reset
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Finally, if NOPASSWD is required, strictly limit execution arguments where possible, or replace script-level execution with standard system management tools such as systemctl units, which run in controlled isolation environments outside user-writable filesystem paths.
Related content
Want a second set of eyes on your security posture?
Let's talk about where your real exposure is.
Book an advisory call