maleadt     about     archive

sudo — local privilege escalation

sudo is a popular program for executing commands as a substitute user, most of the times root. For the purpose of user-friendliness, sudo caches the right to elevate for several minutes. By hooking user-level library calls using LD_PRELOAD and waiting until the user unlocks sudo, we can abuse this caching mechanism and gain elevated access.

For this exploit, I am assuming local user access where you can drop a file and change environment flags. This is a hefty requirement; many people would argue local user access means game over already.

That said, the procedure described below is an interesting way of getting to root, and is undocumented as far as I could find. It also works on all recent distributions I’ve tested, regardless of security features such as SELinux.

But first, some context. Skip this if you already know about LD_PRELOAD and how it can’t be used for setuid binaries like sudo.

Context

The famous LD_PRELOAD variable is a powerful yet dangerous way of influencing how dynamic libraries are loaded on most Linux systems. It instructs the dynamic linker to load certain libraries before all others, which makes it possible to override or hook functions. Although many benign use cases exist, the feature can also be misused: imagine some library hooking the C runtime’s read function and spying on everything you type.

To prevent such shenanigans affecting your entire system, the LD_PRELOAD environment flag is mostly ignored when executing binaries with the setuid or setgid bits set. These bits allow an application to change its user identity to whoever owns the binary being executed. For example:

int main()
{
  if (setuid(0))
    perror("setuid failed");
  else
    system("/usr/bin/whoami");

  return 0;
}

If we compile and run this, the setuid call to switch our user identity to user 0 (root) is obviously blocked off. However, if we make root own this binary and toggle the setuid bit, all is allowed:

# chown root test
# chmod 4755 test
$ ./test
root

This mechanism is what powers applications such as sudo. Needless to say, it would be bad if an attacker could influence the behaviour of said applications through means of LD_PRELOAD, which is why these and other environment variables are ignored in such circumstances.

Abusing the cache

Not to have you enter your password over and over again, sudo comes with a credential cache which remembers that you’ve entered your credentials already. Quoting the manual:

Once a user has been authenticated, a record is written containing the uid that was used to authenticate, the terminal session ID, and a time stamp. The user may then use sudo without a password for a short period of time.

By saving the terminal session ID, an attacker cannot hijack your authenticated sudo session from another terminal. That means if we want to abuse an unlocked cache, we’d need to do it from the very same terminal where sudo is being used.

Waiting for a signal

Although we cannot use LD_PRELOAD for hijacking library calls within sudo (as it is a setuid binary), we can hook onto a user-level call and check whether the cache is unlocked. When the user calls an application in which we’ve hooked a certain library call, and the user has recently used (and unlocked) sudo within the same terminal session, we can assume control:

$ application
  ├ preload hooked.so
  └ call foobar()
    └ hooked::foobar()
      ├ is sudo unlocked? nope...
      └ call real::foobar()

$ sudo application
  ├ unlocks the credential cache
  ├ call setuid
  └ exec application

$ application
  ├ preload hooked.so
  └ call foobar()
    └ hooked::foobar()
      ├ is sudo unlocked? yes!
      └ game over

This completely circumvents the existing LD_PRELOAD protections, because it never attempts to preload when executing a setuid binary, but only targets regular applications executed afterwards.

Implementation

Ideally, checking the credential cache would happen quickly after spawning a new application. For the demonstration below, I’ve picked the open call in libc. Using dlsym we look up the address of the original function, and wrap the call before returning:

static int (*open_real)(const char *, int, ...) = NULL;

int open(const char *path, int flags, ...) {
  // Load the real 'open' function
  if (!open_real)
    open_real = (int (*)(const char *, int, ...))
                dlsym(RTLD_NEXT, "open");

  // TODO: check if sudo is unlocked

  // Wrap call
  va_list ap;
  mode_t mode;
  va_start(ap, flags);
  mode = va_arg(ap, mode_t);
  va_end(ap);
  return open_real(path, flags, mode);
}

Now for the crucial part: check whether sudo is unlocked. This can be accomplished by running sudo -n true, which indicates failure when a password is required. I’ve chosen to implement this using a quick fork and exec, while the main thread waits for its child to finish:

pid_t child = fork();
if (child == 0) {
  int null = open("/dev/null", O_WRONLY);
  dup2(null, STDERR_FILENO);

  execlp("sudo", "sudo", "-n", "true", NULL);

  exit(-1);
} else {
  int exit_code;
  if (wait_for(child, 10 /*ms*/, &exit_code) == 0) {
    if (exit_code == 0) {
      printf("I am ");
      fflush(stdout);
      execlp("sudo", "sudo", "whoami", NULL);
    }
  }
}

Note that these code fragments are for demonstrative purposes only, and lack crucial bits of functionality in order to work.

What this means

As said before, this exploits requires local user access, at which point many other options exist1. Still, I think this post describes an interesting path to root, in particular because it targets a design ‘decision’ rather than an ordinary vulnerability.

Because of this, and the fact that LD_PRELOAD is particularly hard to detect (just have a look @haxelion’s recent blogpost), this privilege escalation seems hard to mitigate. The safest path forward would be to either disable LD_PRELOAD or the caching mechanism in sudo, but that might break certain use-cases.

In the end, it all is a matter of securing your local accounts, because if they get breached by a determined hacker it is just a matter of time before your entire system is compromised. This vulnerability might just give him an extra weapon of choice.

  1. Staying close to this exploit, one could for example hook calls to exec*(sudo) and replace them with a familiar looking prompt, getting a hold of your password.