Exploit-DB #47780
Description: OpenBSD 6.x - Dynamic Loader Privilege Escalation
Type: local
Platform: openbsd
Date: 2019-12-16
Author: Qualys Corporation

Exploit:
Qualys Security Advisory

Local Privilege Escalation in OpenBSD's dynamic loader (CVE-2019-19726)


==============================================================================
Contents
==============================================================================

Summary
Analysis
Demonstration
Acknowledgments


==============================================================================
Summary
==============================================================================

We discovered a Local Privilege Escalation in OpenBSD's dynamic loader
(ld.so): this vulnerability is exploitable in the default installation
(via the set-user-ID executable chpass or passwd) and yields full root
privileges.

We developed a simple proof of concept and successfully tested it
against OpenBSD 6.6 (the current release), 6.5, 6.2, and 6.1, on both
amd64 and i386; other releases and architectures are probably also
exploitable.


==============================================================================
Analysis
==============================================================================

In this section, we analyze a step-by-step execution of our proof of
concept:

------------------------------------------------------------------------------

1/ We execve() the set-user-ID /usr/bin/chpass, but first:

   1a/ we set the LD_LIBRARY_PATH environment variable to one single dot
   (the current working directory) and approximately ARG_MAX colons (the
   maximum number of bytes for the argument and environment list); as
   described in man ld.so:

     LD_LIBRARY_PATH
             A colon separated list of directories, prepending the default
             search path for shared libraries.  This variable is ignored for
             set-user-ID and set-group-ID executables.

   1b/ we set the RLIMIT_DATA resource limit to ARG_MAX * sizeof(char *)
   (2MB on amd64, 1MB on i386); as described in man setrlimit:

     RLIMIT_DATA     The maximum size (in bytes) of the data segment for a
                     process; this includes memory allocated via malloc(3) and
                     all other anonymous memory mapped via mmap(2).

------------------------------------------------------------------------------

2/ Before the main() function of chpass is executed, the _dl_boot()
function of ld.so is executed and calls _dl_setup_env():

262 void
263 _dl_setup_env(const char *argv0, char **envp)
264 {
...
271         _dl_libpath = _dl_split_path(_dl_getenv("LD_LIBRARY_PATH", envp));
...
283         _dl_trust = !_dl_issetugid();
284         if (!_dl_trust) {       /* Zap paths if s[ug]id... */
285                 if (_dl_libpath) {
286                         _dl_free_path(_dl_libpath);
287                         _dl_libpath = NULL;
288                         _dl_unsetenv("LD_LIBRARY_PATH", envp);
289                 }

------------------------------------------------------------------------------

3/ At line 271, _dl_getenv() returns a pointer to our LD_LIBRARY_PATH
environment variable and passes it to _dl_split_path():

 23 char **
 24 _dl_split_path(const char *searchpath)
 25 {
 ..
 35         pp = searchpath;
 36         while (*pp) {
 37                 if (*pp == ':' || *pp == ';')
 38                         count++;
 39                 pp++;
 40         }
 ..
 45         retval = _dl_reallocarray(NULL, count, sizeof(*retval));
 46         if (retval == NULL)
 47                 return (NULL);

------------------------------------------------------------------------------

4/ At line 45, count is approximately ARG_MAX (the number of colons in
our LD_LIBRARY_PATH) and _dl_reallocarray() returns NULL (because of our
low RLIMIT_DATA); at line 47, _dl_split_path() returns NULL.

------------------------------------------------------------------------------

5/ As a result, _dl_libpath is NULL (line 271) and our LD_LIBRARY_PATH
is ignored, but it is not deleted from the environment (CVE-2019-19726):
although _dl_trust is false (_dl_issetugid() returns true because chpass
is set-user-ID), _dl_unsetenv() is not called (line 288) because
_dl_libpath is NULL (line 285).

------------------------------------------------------------------------------

6/ Next, the main() function of chpass is executed, and it:

   6a/ calls setuid(0), which sets the real and effective user IDs to 0;

   6b/ calls pw_init(), which resets RLIMIT_DATA to RLIM_INFINITY;

   6c/ calls pw_mkdb(), which vfork()s and execv()s /usr/sbin/pwd_mkdb
   (unlike execve(), execv() does not reset the environment).

------------------------------------------------------------------------------

7/ Before the main() function of pwd_mkdb is executed, the _dl_boot()
function of ld.so is executed and calls _dl_setup_env():

   7a/ at line 271, _dl_getenv() returns a pointer to our
   LD_LIBRARY_PATH environment variable (because it was not deleted from
   the environment in step 5, and because execv() did not reset the
   environment in step 6c);

   7b/ at line 45, _dl_reallocarray() does not return NULL anymore
   (because our low RLIMIT_DATA was reset in step 6b);

   7c/ as a result, _dl_libpath is not NULL (line 271), and it is not
   reset to NULL (line 287) because _dl_trust is true (_dl_issetugid()
   returns false because pwd_mkdb is not set-user-ID, and because the
   real and effective user IDs were both set to 0 in step 6a): our
   LD_LIBRARY_PATH is not ignored anymore.

------------------------------------------------------------------------------

8/ Finally, ld.so searches for shared libraries in _dl_libpath (our
LD_LIBRARY_PATH) and loads our own library from the current working
directory (the dot in our LD_LIBRARY_PATH).

------------------------------------------------------------------------------


==============================================================================
Demonst