sudo — local privilege escalation25 Feb 2015
sudo is a popular program for executing commands as a substitute user, most of
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
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
function and spying on everything you type.
To prevent such shenanigans affecting your entire system, the
environment flag is mostly ignored when executing binaries with the
setgid bits set. These bits allow an application to change its user identity
to whoever owns the binary being executed. For example:
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:
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.
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
Waiting for a signal
Although we cannot use
LD_PRELOAD for hijacking library calls within
(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.
Ideally, checking the credential cache would happen quickly after spawning a new
application. For the demonstration below, I’ve picked the
open call in
dlsym we look up the address of the original function, and wrap the call
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
while the main thread waits for its child to finish:
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
root, in particular because it targets a design ‘decision’ rather than an
Because of this, and the fact that
LD_PRELOAD is particularly hard to detect
(just have a look @haxelion’s recent
privilege escalation seems hard to mitigate. The safest path forward would be to
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
exec*(sudo)and replace them with a familiar looking prompt, getting a hold of your password. ↩