sudo — local privilege escalation
Feb 25, 2015sudo
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.
Staying close to this exploit, one could for example hook calls to
[return]exec*(sudo)
and replace them with a familiar looking prompt, getting a hold of your password.