Skip to main content
All articles

Sudo is a shell in disguise: CVE-2025-32463, GTFOBins, and reading sudo -l properly

A 9.3 in sudo itself, plus twenty ways a single sudoers line hands over root. How to read sudo -l like an attacker, why the chroot bug worked, and what a safe sudoers policy looks like.

7 min read1 view

sudo -l is the first command anyone runs after landing on a Linux host, and most people read the output wrong. They look for (ALL : ALL) ALL and, not finding it, move on.

That is the wrong reading. The question is not "can this user run everything as root?" It is: "can any single thing this user may run as root be persuaded to do something else?"

Almost always, yes. And in 2025, sudo produced a vulnerability where the answer was yes even when the user was in no rule at all.

CVE-2025-32463: root without being in sudoers

Disclosed June 2025 by the Stratascale Cyber Research Unit. CVSS 9.3. Affects sudo 1.9.14 through 1.9.17. Fixed in 1.9.17p1.

The mechanism is a good example of a feature interacting badly with the order of operations.

Sudo's -R / --chroot option runs a command with a user-selected root directory, when sudoers permits it. In sudo 1.9.14, a change was made so that paths were resolved via chroot() into the user-specified directory while the sudoers file was still being evaluated.

That ordering is the bug. During evaluation, sudo performs name-service lookups, and those lookups consult /etc/nsswitch.conf, which, inside the chroot, is a file the attacker created. nsswitch.conf names the shared libraries the resolver should load. So the attacker supplies a configuration pointing at their own library, sudo loads it as root, and the library's initialiser runs with full privileges.

The critical consequence: this works even if the user is not listed in sudoers at all, because the compromise happens during evaluation, before any decision about whether the user is permitted anything.

The upstream response is instructive. The 1.9.14 change was reverted, and the chroot feature was marked deprecated. An acknowledgement that "resolve paths inside an untrusted directory" is a shape that cannot be made safe by fixing one path through it.

Check your version:

sudo --version | head -1

1.9.14 through 1.9.17 needs patching. Note that this is a recent-versions bug: hosts that were carefully kept up to date were vulnerable and old ones were not, which is an unusual and uncomfortable inversion.

The everyday case: GTFOBins

The far more common route to root is a sudoers rule that was written to be helpful.

Every one of these is a real pattern that appears in real /etc/sudoers files, and every one is equivalent to granting a root shell:

RuleWhy it is root
NOPASSWD: /usr/bin/findfind . -exec /bin/sh \;
NOPASSWD: /usr/bin/vim:!sh, or :set shell=/bin/sh then :shell
NOPASSWD: /usr/bin/less!sh from the pager
NOPASSWD: /usr/bin/awkawk 'BEGIN {system("/bin/sh")}'
NOPASSWD: /usr/bin/python3python3 -c 'import os; os.system("/bin/sh")'
NOPASSWD: /usr/bin/tar--to-command, --checkpoint-action=exec=
NOPASSWD: /usr/bin/zip--unzip-command
NOPASSWD: /usr/bin/git-c core.pager=, or !sh from the pager
NOPASSWD: /usr/bin/systemctlThe pager again, or write a unit file
NOPASSWD: /usr/bin/aptAPT::Update::Pre-Invoke hooks
NOPASSWD: /usr/bin/nano^R^X executes a command
NOPASSWD: /usr/bin/cpOverwrite /etc/passwd, /etc/shadow, or a systemd unit
NOPASSWD: /bin/chmodchmod u+s /bin/bash
NOPASSWD: /usr/bin/envenv /bin/sh

Three principles fall out of that table, and they matter more than memorising it:

1. Anything with a pager is a shell. git, systemctl, man, journalctl, less and more all invoke a pager, and less executes !command. The pager is also selected by an environment variable, which is why env_reset matters.

2. Anything that writes a file you choose is root. cp, tee, dd, install, a text editor. Arbitrary write as root is arbitrary execution as root via /etc/passwd, /etc/sudoers, a cron entry, a systemd unit or an SSH authorized_keys.

3. Anything that reads a file you choose is usually root too. /etc/shadow for offline cracking, an SSH private key, a cloud credentials file, a Kubernetes kubeconfig.

GTFOBins is the reference for all of this. Look up every binary in your sudo -l output. Not the ones that look dangerous. every one.

Wildcards, and CVE-2023-22809

Two more patterns worth knowing because they defeat rules that look tight.

Wildcards. A rule like:

alice ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp*

looks scoped to one service. * matches anything, including a space and further arguments, so myapp followed by another command may be accepted, and glob semantics rarely match the author's mental model. Wildcards in sudoers argument positions should be treated as an open door until proven otherwise.

sudoedit and CVE-2023-22809. sudo tried to prevent extra arguments being smuggled through EDITOR, VISUAL and SUDO_EDITOR by checking for spaces and quotes. It did not account for --:

EDITOR='vim -- /etc/sudoers'

Everything after -- is a file operand, so the user edits a file they were never authorised for, with the RunAs user's privileges. Affected 1.8.0 through 1.9.12p1; fixed in 1.9.12p2. It is the same argument-injection class that produced CVE-2024-4577 in PHP, in a different program.

And the one to check on genuinely old estates: CVE-2021-3156, "Baron Samedit". A heap overflow in sudoedit argument parsing reachable by any local user, no sudoers entry required. It sat undiscovered for roughly ten years.

Reading sudo -l properly

A worked method:

sudo -l

For each entry:

  1. Look up the binary on GTFOBins. Every time, including for binaries you are sure about.
  2. Check for wildcards in the argument specification.
  3. Check the RunAs field. (ALL) includes root. (ALL : ALL) includes any group. But (someuser) is not safe either, if that user owns application code, a systemd unit, or a cron job, it is a step, not a dead end.
  4. Check env_keep. Preserved variables are an attack surface: LD_PRELOAD and LD_LIBRARY_PATH in env_keep are immediate root; PYTHONPATH, PERL5LIB, RUBYLIB, NODE_OPTIONS, BASH_ENV and PAGER each turn a matching interpreter into a shell.
  5. Check whether secure_path is set. Without it, a relative path or a PATH you control may select your binary rather than the intended one.
  6. Check for NOPASSWD on a script rather than a binary. If the script is writable by you or your group, or calls a binary by relative path, or sources a file you can write. The rule's scope is the whole script's behaviour, not its name.
  7. Check the sudo version against the CVEs above.

Writing a sudoers policy that holds

Grant a script, not a program. Instead of NOPASSWD: /bin/systemctl, write a small root-owned script that does exactly the one thing, with no arguments, and grant that. Make it root:root and mode 0755, if the user can modify the script, you have granted everything.

# Bad: a shell
alice ALL=(root) NOPASSWD: /usr/bin/systemctl

# Better: one operation, no arguments
alice ALL=(root) NOPASSWD: /usr/local/sbin/restart-myapp

Never grant an interpreter, editor, archiver or anything with a pager. python, perl, ruby, node, awk, vim, nano, tar, zip, git, find, less. There is no argument restriction that makes these safe.

Set the hardening defaults explicitly:

Defaults        env_reset
Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults        requiretty
Defaults        log_input, log_output
Defaults        !visiblepw

Avoid wildcards in argument positions. If you need several variants, list them.

Validate before you save. visudo checks syntax. For semantics, run sudo -l -U <user> as root to see the policy exactly as that user will.

Audit what exists now:

grep -rvE '^\s*(#|$)' /etc/sudoers /etc/sudoers.d/ 2>/dev/null

Nearly every estate has a NOPASSWD line added years ago for a deployment that no longer exists.

Detection

  • sudo -l executed by an interactive user. Legitimate use is rare; it is step one of nearly every local escalation.
  • Sudo authentication failures, and NOT in sudoers events, which are logged and rarely read.
  • sudo -R or --chroot in command lines. With CVE-2025-32463 patched this should be nil, and it is a precise indicator of exploitation attempts.
  • A sudo-invoked process spawning a shell. find, vim, tar, git or systemctl running under sudo and producing a sh child is the GTFOBins pattern, and it is unambiguous.
  • Changes to /etc/sudoers and /etc/sudoers.d/. A file integrity rule that costs nothing.
  • log_input/log_output, which give you the actual session transcript when you need it.

Take this away

Sudo does not grant commands. It grants whatever those commands can be made to do.

Read sudo -l as a list of capabilities rather than a list of programs, look up every entry, and keep the version patched, because in 2025 the version alone was enough.


Further reading

Was this useful?

Share

Tags

  • linux privilege escalation
  • cve analysis
  • detection engineering
  • penetration testing

Comments

Loading comments…

Leave a comment

Comments are read and approved by hand before they appear, so yours will not show up straight away. Your email address is optional, is never published, and is only used if we need to reply to you directly.

0/5000

Related service

Internal & External Penetration Testing

Find the paths into your network, and the paths across it once someone is in.

If you want to know whether what you have just read applies to your own systems, that is the engagement that answers it.