guest@6admin.io:~$

Sudo and LOLBins

The comfortable belief about sudo is that a narrow rule is a safe rule, that NOPASSWD: /usr/bin/find grants the ability to run find and nothing more. GTFOBins demolished that belief and the honest version of a sudo hardening note has to start there: the granularity of sudoers limits nothing the moment the binary you allowed can be talked into spawning a shell and a startling fraction of the standard toolbox can. The question is never “which command did I grant”, it is “what can that command be made to do once it runs as root”.

Why the narrow rule is not narrow

Every one of these is a full root shell from a rule that looks tightly scoped:

sudo vi /etc/motd        # then  :!/bin/sh
sudo less /var/log/x     # then  !/bin/sh
sudo find . -exec /bin/sh \;
sudo awk 'BEGIN{system("/bin/sh")}'
sudo systemctl edit --full x   # the editor it opens is root
sudo docker run -v /:/host -it alpine chroot /host

None of these is a bug. An editor is supposed to shell out, find is supposed to exec, awk is supposed to run programs and docker with a host bind-mount is supposed to reach the host. GTFOBins is simply the catalogue of which everyday binaries carry that property and it is long: editors, pagers, interpreters, archivers and most of the system-administration tools. A NOPASSWD rule for any of them is a passwordless root shell wearing the name of a single command.

The corollary is uncomfortable and worth stating plainly: a sudoers file full of carefully scoped rules can be, in effect, unrestricted root and whether it is depends entirely on whether the allowed binaries appear in GTFOBins. The scoping was never the control.

What actually limits it

The real mitigations do not try to make a GTFOBins binary safe; they avoid granting it, or they strip the capability it abuses.

Do not grant interpreters, editors or pagers through sudo at all. When a user needs to edit a root-owned file, the answer is sudoedit (equivalently sudo -e), which copies the file out, runs the editor unprivileged as the user and copies it back, so the editor never runs as root and its shell escape leads nowhere:

alice ALL=(root) sudoedit /etc/nginx/nginx.conf

When a command genuinely must be granted, wrap it rather than exposing the generic tool and have the wrapper validate its arguments against a whitelist so there is no room to smuggle in an escape:

#!/bin/bash
# /usr/local/bin/svcctl, the only thing sudo grants
set -euo pipefail
case "$1:$2" in
    apache2:restart|apache2:reload|nginx:restart|nginx:reload) ;;
    *) logger -t svcctl "DENY $SUDO_USER $*"; echo "not allowed" >&2; exit 1 ;;
esac
logger -t svcctl "$SUDO_USER: systemctl $2 $1"
exec /usr/bin/systemctl "$2" "$1"
webadmin ALL=(root) NOPASSWD: /usr/local/bin/svcctl

The wrapper is owned by root and not writable by the caller, which matters, because a wrapper the user can edit is just a creative path back to a root shell.

The sudoers defaults do the rest of the structural work. secure_path stops a planted binary earlier in PATH from being what runs, env_reset keeps the caller’s environment from leaking into the root command, NOEXEC prevents a granted command from fork-exec’ing a child where the platform supports it and full absolute paths with no wildcards keep the rule meaning what it says:

Defaults env_reset
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults !visiblepw
Defaults passwd_tries=3, timestamp_timeout=5

A wildcard in a command rule (/usr/bin/*, /opt/scripts/*.sh) throws all of this away, because it re-admits every GTFOBins binary and every script the user can create under a matching name. Rules carry full paths, fixed arguments where possible and never a wildcard.

Monitoring, honestly

The useful monitoring is I/O logging, which records what actually happened inside a sudo session rather than guessing from the command line:

Defaults log_input, log_output
Defaults iolog_dir=/var/log/sudo-io/%{user}

What does not work and what I would push back on in a design, is grepping sudo.log for scary substrings and auto-locking the account. It generates false positives on legitimate admin work, it is trivially evaded by anyone who read the regex and an attacker who already has a root shell is not going to be stopped by a log watcher reacting a second later. Detection is for the audit trail and the after-action, not for the boundary. The boundary is not granting the dangerous binary in the first place.

Application control (AppArmor or SELinux) is the one place where constraining a binary rather than withholding it pays off, because a profile can deny a sensitive tool the files and network it has no business touching even when it runs:

# /etc/apparmor.d/local, illustration: a curl confined for a backup job
/usr/bin/curl {
  #include <abstractions/base>
  network inet stream,
  deny /etc/shadow r,
  deny /root/** r,
}

That is a supplement, not a substitute. The profile narrows blast radius; it does not make a GTFOBins sudo grant safe.

Where to land

Grant the narrowest thing that does the job and make sure that thing is not a GTFOBins binary: a wrapper that validates its arguments, sudoedit instead of a root editor, never an interpreter or a pager or a shell, never a wildcard. Keep env_reset and secure_path on, wrappers root-owned and unwritable by the caller and I/O logging flowing somewhere it gets read. And hold the uncomfortable truth in mind while you write the file: the scoping in a sudoers rule is a suggestion to anyone who knows GTFOBins and the only rules that bind are the ones whose allowed command genuinely cannot be turned into a shell.

[edit: 2026] The Ansible implementation of all this: the users role .