Digging for Secrets on Corporate Shares
You will probably recognize the situation that during a red team you run into the event where it was possible to get an initial foothold, however there are no obvious paths to escalate/move from there.
In those situations, the only feasible way forward is to plow through the machine’s filesystems and any reachable shares like the domain controller’s SYSVOL share or company shared drives. These locations often contain scripts, logs, documents and files in which frequently also sensitive information like credentials and tokens are stored. Manually browsing through the directories however takes a lot of time and it is easy to overlook something.
In this blogpost I will expand on several methods that exist to efficiently work through directory listings and search for interesting files. Additionally, I am releasing a .NET tool called Dir2json to expand on these methods in an OPSEC friendly way.
Searching the directory structure
Besides manually going through the directory structure there are various ways to more easily search for potentially interesting files which can subsequentially manually be collected. In the examples below the C-drive is recursively searched for files with the .kdb
and .kdbx
(KeePass password database) file extension and files that contain the password
string in the filename. This search string can be expanded to include many patterns to identify potentially interesting files. Moreover, in the examples only the full path is outputted, but other attributes like the file size and last modified date can also be shown.
Command Prompt (cmd.exe)
Use the dir command to search for one or more patterns to identify files on disk. The output can also be redirected to a file by appending > file.txt
. More examples of searching using dir can be found in the localrecon.cmd
script on the BITSADMIN GitHub, a script I created back when I was working on the OSCP lab1.
dir /S /B /A C:\*.kdb* C:\*password*
Parameter | Meaning |
---|---|
/S |
Search recursively |
/B |
Show in bare format |
/A |
Include hidden and system folders |
PowerShell
Use the Get-ChildItem
cmdlet to search for files where the -Force
parameter makes sure that also hidden and system files are included. Unauthorized access exceptions can be hidden by appending -ErrorAction SilentlyContinue
(shorthand: -ea 0
) to the Get-ChildItem
cmdlet parameters. The output can be redirected to a file by piping to Out-File file.txt
.
Get-ChildItem -Path C:\ -Recurse -Force -Include *.kdbx,*password* | Select-Object -ExpandProperty FullName
Windows Management Instrumentation (WMI)
In this example PowerShell is used to perform the WMI queries, but any language that is able to interface with WMI is able to perform such query. The advantage of WMI is that using the -ComputerName
or -CimSession
parameters such queries can also be performed against remote machines of which only port 135/TCP (RPC) is accessible (or 5985/TCP / 5986/TCP when used over WinRM), meaning port 445/TCP (Microsoft-DS) is not required for enumeration. Administrative privileges on the remote machine are required though. More information on remotely interacting with WMI can be found in this blog post2. To search in a specific folder the Path Like 'C:\\Users\\%'
condition can be added to the query. Moreover, besides the CIM_LogicalFile
class also the CIM_DataFile
class can be used.
Get-CimInstance -Query "Select * from CIM_LogicalFile Where Drive='C:' And ((Extension Like 'kdb%') Or (FileName Like '%password%'))" | Select-Object -ExpandProperty FullName
Component Object Model (COM)
This option is not directly feasible for quick filesystem enumeration, however when turned into code which recurses the directory structure, it could also be used. A PowerShell snippet to search the filesystem using COM is added here for completeness, but like with WMI, any language that is able to interface with COM could be used to enumerate the filesystem.
$o = New-Object -ComObject Scripting.FileSystemObject
$c = $o.GetDrive('C').RootFolder
Function Search-Folder
{
Param($folder)
$folder.Files | Where-Object Name -Match $filter
$folder.SubFolders | ForEach-Object { Search-Folder $_ }
}
$filter = '.*(password|\.kdb).*'
Search-Folder $c | Select-Object -ExpandProperty Path
Summary and limitations
Besides using the various interfaces Windows offers to query the filesystem, another option is to directly access the NTFS Master File Table (MFT). In this blog this option is not considered as the assumption is that one is in a low-privileged context.
Although all aforementioned approaches work to identify files on the filesystem, they have various limitations:
- Whenever you want to perform another search query, the full filesystem needs to be iterated again;
- If based on the results you want to list the other files in the directory of the respective file, interaction with the target (file)system is required again.
The next section discusses an alternative approach to searching the directory structure.
Build directory tree using PowerShell
Instead of searching through the directory structure for a specific pattern, another option is to create a (one-time) file listing which, in addition to the file name and path, also contains information like the file size, modification date and mode flags. This tree can subsequentially be pulled to the attacker’s system and queried offline in various ways. No further interaction with the target system will be required anymore until the moment a file has been identified which you as the attacker want to analyze further. This specific file can then be downloaded and further evaluated offline. Because Windows can have issues with long paths, some tweaks have been added to different ways of collection that deal with long paths correctly.
Windows PowerShell is preinstalled on all Windows versions starting from Windows 7 SP1/2008 R2 SP1 and runs up to version number 5.1. Besides Windows PowerShell there is also PowerShell Core which is built on the .NET Core framework and therefore in addition to Windows, also runs Linux and Mac operating systems and can be obtained from https://aka.ms/pscore6. The first PowerShell Core version is version 6.0 and can be installed separately. The two editions of PowerShell are mostly similar but have some minor differences resulting in slightly different commandlines for enumeration.
Windows PowerShell
The following commandline creates a file listing of the C:\
drive and stores it in the Drive_C.csv
file. The DOS path syntax (\\?\
) is used to avoid running into issues with long file paths. In case a file listing of a network share needs to be created, use the \\?\UNC\MYSERVER\MyShare
value for the -LiteralPath
parameter. Additionally, update the \\?\
value to \?\UNC
in the $_.FullName.Replace('\\?\','')
code. Unless when logic is built into a script, Windows PowerShell does not support avoiding symlinked directories. Such directories can be dangerous because they can be self-referencing and therefore lead to infinite recursion. In the oneliner below the maximum depth is set to 25 which should be sufficient in most cases to collect all relevant files and folders, however if needed this number can be increased, or replaced with the -Recurse
parameter to allow for infinite recursions.
Get-ChildItem -LiteralPath '\\?\C:\' -Depth 25 -Force | ForEach-Object { [PSCustomObject]@{Name=$_.Name; Mode=$_.Mode; Length=$_.Length; LastWriteTime=$_.LastWriteTime.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss'); FullName=$_.FullName.Replace('\\?\','') } } | Export-Csv -Encoding UTF8 -NoTypeInformation Drive_C.csv
PowerShell Core
Even though the Windows PowerShell oneliner can also be used in PowerShell Core, the PowerShell Core onlineliner is slightly shorter and looks a bit cleaner. In contrast to Windows PowerShell, PowerShell Core does not enter into directory symlinks by default. This behavior can be changed by adding the -FollowSymlink
flag to the Get-ChildItem
cmdlet. Moreover, PowerShell Core also natively deals well with long paths, so no DOS path syntax needs to be used. Like with Windows PowerShell, the -Depth X
parameter can be used instead of -Recurse
to limit the depth of enumeration.
Get-ChildItem -Path 'C:\' -Recurse -Force | ForEach-Object { [PSCustomObject]@{Name=$_.Name; Mode=$_.Mode+'-'; Length=$_.Length; LastWriteTime=$_.LastWriteTime.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss'); FullName=$_.FullName } } | Export-Csv -NoTypeInformation Drive_C.csv
In many occasions the PowerShell oneliners will be sufficient for collecting the filesystem trees. In some cases however, a stealthy approach is required and the execution of PowerShell (or load of System.Management.dll
) might trigger alarms. In the next section the newly developed Dir2json tool is discussed which allows for stealthy collection.
Dir2json
Dir2json is a tool I wrote when I noticed the need for a stealthy tool which from the memory of a Cobalt Strike infection is able to create a directory listing, and without touching the disk send the results back to the Cobalt Strike Team Server. Initially I developed a C++ version of the tool to practice the knowledge obtained in Pavel Yosifovich’ (@zodiacon) Windows System Programming course. This tool works well, but then I realized it was not possible to easily turn this into a Beacon Object File (BOF). For that reason instead of fixing the C++ tool to make it work as a BOF I rewrote the Dir2json tool in C# which allows for in-memory execution and file download thanks to Ceri Coburn’s (@EthicalChaos) BOF.NET project3. Additionally, the project can also be compiled and executed as a regular .NET executable. The C# version of Dir2json is available at https://github.com/bitsadmin/dir2json/. The video below shows how to execute the tool from Cobalt Strike.
In-memory execution
Because the tool runs in-memory and does not (like the PowerShell oneliners) directly flush the identified files and folders to disk, it initially needs to store the full directory listing in memory before sending it back to the Cobalt Strike Team Server. For that reason, instead of storing the full paths of the identified items, it builds up a tree structure where subfolders and -files are linked to its parent folder. When the download of the tree is triggered, the tree is serialized to JSON, gzip compressed, and then sent back to the Cobalt Strike Team Server. Because the Dir2json tool might be executed on machines that are tight on memory, or against drives or shares which contain millions of files, it also provides an option (/EntriesPerFile=X
) to flush the memory every X
thousand items collected and download the partial results to the Team Server. Finally, Dir2json can be instructed to follow symbolic links using the /FollowSymlinks
parameter which can be used in combination with the /MaxDepth=Y
parameter to restrict recursion to a maximum of Y
directories, with that avoiding possible infinite recursions.
JSON to CSV
As it is not very easy to search through JSON, the Dir2json repository contains the Json2csv.ps1
PowerShell script which takes the .json
(.gz
) file(s) as input, if needed decompresses it, and then stores the output into a .csv
output file. In case the first file of a split Dir2json file is provided as input, the tool also immediately merges them back to a single .csv
output file. Because the script can also be executed with PowerShell Core, it is also compatible with non-Windows operating systems.
Statistics
To get a better feeling of usage of the different tools, some statistics of execution. The tests have been executed against my laptop with an SSD which contains a combined 1.2 million files and folders entries.
Tool | Duration | Output file | Size |
---|---|---|---|
Windows PowerShell | 02:30m | .csv |
255 MB |
PowerShell Core | 03:20m | .csv |
245 MB |
Dir2json | 20s | .json.gz |
13 MB |
In both PowerShell versions, enumeration takes between 2.5 and 3.5 minutes while for Dir2json enumeration takes just 20 seconds. Regarding Windows PowerShell and PowerShell Core one would expect they yield exactly the same results, however because the Windows PowerShell oneliner does not avoid symbolic links, in Windows PowerShell folders inside of symlinked directories are listed while in PowerShell Core these directories are avoided. This for example happens for the C:\Users\All Users
directory which is a symlink to C:\ProgramData
where in case of Windows PowerShell these files are listed twice in the resulting .csv
.
As mentioned before, to convert the .json.gz
file to a .csv
using the Json2csv.ps1
script, either Windows PowerShell or PowerShell Core can be used. The table below shows that, although it uses 1.5 times more memory, PowerShell Core converts the file 5x faster compared to Windows Powershell. Once the .json.gz
file has been converted, the .csv
contains exactly the same data as the one created by the PowerShell Core oneliner.
Tool | Duration | Max. memory usage |
---|---|---|
Windows PowerShell | 10:00m | 2.7 GB |
PowerShell Core | 02:00m | 4.2 GB |
The next sections will take the .csv
file created by either PowerShell or Dir2json + Json2csv tool as an input and discuss the different ways PowerShell can import the data and query it to find interesting files.
Importing csv into PowerShell
There are various ways to query data in a .csv
file, from using grep
and awk
in Linux to parsing the data using Python. In this section PowerShell’s powerful functionality for dealing with structured data will be discussed. Even though in this article going through the .csv
using Linux commands is not discussed, examples on how to perform equivalent queries are available in the Dir2json repository in the CheatSheet.sh
file.
PowerShell has native support for importing .csv
files using the Import-Csv
cmdlet. During importing it is also possible to enrich the imported data in various ways which, at the expense of an increased loading time and memory usage, improves querying the dataset significantly once it has loaded. The following paragraphs discuss the advantages of the different import methods, where each of the methods builds upon the previous one adding additional attributes.
D0: Plain CSV
The plain import is by far the quickest and allows for wildcard searches in the FullPath
attribute. It however lacks to possibility to perform smart queries.
D1: Length attribute as integer
When using the Import-Csv
cmdlet, PowerShell simply considers all column values as strings. This means that when for example sorting and filtering based on file sizes, the digits will be sorted in an alphabetical way instead of the expected order (e.g. 1
,10
,2
,3
instead of 1
,2
,3
,10
). Forcing conversion of the string to a 64-bit integer will increase import time and memory usage, however result in a better usable data set.
D2: Extension attribute
As based on the file extension it is easy to quickly identify interesting files, another useful attribute to have that attribute prepared. The .NET framework which can be accessed from PowerShell has built-in functionality to obtain the extension based on a filename using the static GetExtension
method in the System.IO.Path
namespace. Because the function does not have any other input than the name string and a directory which contains a period can also be confused to be a file with an extension, the D2 oneliner contains a check to make sure the item is not a directory.
D3: Depth attribute
Because in the .csv
the hierarchical structure has been flattened, it is more challenging to list the files in a specific directory which in turn also has subdirectories. To be able to list the files and folders in a specific folder without also listing the subfolders, the Depth
attribute can be used. In the D3 import oneliner the FullName
attribute which contains the full path and filename of the file or directory is split by the directory separator character (backslash - \
) and the number of directories is counted to determine the depth.
D4: Mode attributes
A final option is to also translate the Mode string attribute into multiple boolean values to be able to filter on those attributes as well. In practice when searching for interesting files to identify credentials, the Mode attribute is less relevant, and both the import time is 1.7 times significantly longer compared to D3 and memory usage also increases by 60% so generally importing using D3 provides sufficient attributes. Moreover, the mode attribute can still be filtered despite it being a string.
The statistics in the table below have been generated making use of PowerShell Core while importing the 1.2 million-record .csv
file created earlier.
# | Features | Command | Import time | Memory usage |
---|---|---|---|---|
D0 | Plain CSV | $csv = Import-Csv Drive_C.csv |
10s | 1.9 GB |
D1 | D0 + CSV with Length converted to integer | $csv = Import-Csv Drive_C.csv | Select-Object Name,@{n='Length';e={[int64]$_.Length}},Mode,LastWriteTime,FullName |
01:56m | 2.9 GB |
D2 | D1 + Extension attribute | $csv = Import-Csv Drive_C.csv | Select-Object Name,@{n='Length';e={[int64]$_.Length}},Mode,LastWriteTime,FullName,@{n='Extension';e={if($_.Mode[0] -ne 'd'){[System.IO.Path]::GetExtension($_.Name)}else{''}}} |
02:23m | 3.5 GB |
D3 | D2 + Depth attribute | $csv = Import-Csv Drive_C.csv | Select-Object Name,@{n='Length';e={[int64]$_.Length}},Mode,LastWriteTime,FullName,@{n='Extension';e={if($_.Mode[0] -ne 'd'){[System.IO.Path]::GetExtension($_.Name)}else{''}}},@{n='Depth';e={$_.FullName.Split('\').Count - 1}} |
02:49m | 3.8 GB |
D4 | D3 + Mode attribute all converted to individual attributes | $csv = Import-Csv Drive_C.csv | Select-Object Name,@{n='Length';e={[int64]$_.Length}},Mode,LastWriteTime,FullName,@{n='Extension';e={if($_.Mode[0] -ne 'd'){[System.IO.Path]::GetExtension($_.Name)}else{''}}},@{n='Depth';e={$_.FullName.Split('\').Count - 1}},@{n='Directory';e={$_.Mode[0] -eq 'd'}},@{n='Archive';e={$_.Mode[1] -eq 'a'}},@{n='ReadOnly';e={$_.Mode[2] -eq 'r'}},@{n='Hidden';e={$_.Mode[3] -eq 'h'}},@{n='System';e={$_.Mode[4] -eq 's'}},@{n='ReparsePoint';e={$_.Mode[5] -eq 'l' -or $_.Mode[0] -eq 'l'}} |
04:47m | 6.1 GB |
Querying using PowerShell
After the directory listing has been imported, it is available in the $csv
variable. PowerShell supports a lot of filtering, sorting, grouping and data manipulation features, which we can use to quickly get a good insight into the file system or share that has been enumerated. In this section for most queries the requirement is that the data has been imported using the D3 (or even more extensive D4) oneliner.
To be able to systematically evaluate the output, the command can also be piped to the Out-Host -Paging
command (shorthand: oh -p
) or stored in a .csv
file by piping it to the Export-Csv
cmdlet). For some more information on the usage the Export-Csv
cmdlet, check out the Cypher query tools section of the Dealing with large BloodHound datasets blog post4.
Statistics
Some numbers to give an idea of how large the dataset is.
Query | Long | Short | Notes |
---|---|---|---|
Total number of entries | $csv | Measure-Object | Select-Object -ExpandProperty Count |
$csv | Measure | % Count |
% is an alias for ForEach-Object . In this case it iterates over the output of Measure-Object (which is only a single entry) and displays the Count attribute. |
Number of files | $csv | Where-Object Mode -NotMatch 'd.....' | Measure-Object | Select-Object -ExpandProperty Count |
$csv | ? Mode -NotMatch 'd.....' | Measure | % Count |
? is an alias for Where-Object . In case import method D4 has been used, the Where-Object can also simply look as follows: ? -not Directory . |
Number of directories | $csv | Where-Object Mode -Match 'd.....' | Measure-Object | Select-Object -ExpandProperty Count |
$csv | ? Mode -Match 'd.....' | Measure | % Count |
In case import method D4 has been used, the Where-Object can also simply look as follows: ? Directory . |
Extensions
As mentioned before, file extensions can be very useful to identify potentially interesting files.
Query | Long | Short |
---|---|---|
Compile list of top extensions | $exts = $csv | Group-Object Extension -NoElement | Sort-Object -Descending Count,Name |
$exts = $csv | group Extension -NoElement | sort -Desc Count,Name |
Show list of top 25 extensions | $exts | Select-Object -First 25 | Format-Table Name,Count |
$exts | select -First 25 | ft Name,Count |
Example output
PS C:\> $exts = $csv | Group-Object Extension -NoElement | Sort-Object -Descending Count,Name
PS C:\> $exts | Format-Table Name,Count | Out-Host -Paging
Name Count
---- -----
189583
.dll 94405
.manifest 82083
.mui 50736
.cat 46573
.mum 43343
.js 22687
.png 16834
.pyc 15442
.xml 12666
.exe 12101
.py 10394
.rtf 9705
.go 8900
<SPACE> next page; <CR> next line; Q quit
Admin file extensions
Some extensions are specifically interesting to investigate if the objective is to locate sensitive information for use of escalation or lateral movement. To get an overview of which extensions related to administrators are present on the system, the following two lines can be used.
PS C:\> $admin = $csv | Where-Object Extension -Match '^\.(kdbx?|pfx|p12|pem|p7b|key|ppk|crt|pub|config|cfg|ini|sln|\w{2}proj|sql|cmd|bat|ps1|vbs|log|rdp|rdg|ica|wim|vhdx?|vmdk)$'
PS C:\> $admin | Group-Object Extension -NoElement | Sort-Object -Descending Count,Name | Format-Table Name,Count | Out-Host -Paging
Name Count
---- -----
.ps1 1037
.config 476
.ini 302
.sql 130
.log 158
.pem 123
.cmd 111
.bat 96
.vbs 84
.p7b 53
.cfg 47
.wim 22
.crt 20
.sln 15
.kdbx 1
<SPACE> next page; <CR> next line; Q quit
In case of common admin extensions, the paths need to be evaluated and filtered further in order to get to the files that seems relevant. Examples of such queries are.
Query | Command |
---|---|
List admin files from most recent to oldest | $admin | Sort-Object -Descending LastWriteTime | Format-Table LastWriteTime,Length,FullName |
Files with the .cmd extension, not located in the Program Files folders nor in the Windows directory |
$admin | Where-Object Extension -EQ '.cmd' | Where-Object FullName -NotMatch 'C:\\(Program Files|Windows).*' | Format-Table FullName,Length,LastWriteTime,Mode |
Office file extensions
Like with the admin extensions, office extensions can also be interesting if you are looking for specific information stored in documents and text files.
Query | Command |
---|---|
Collect office documents and related files | $office = $csv | Where-Object Extension -Match '^\.((doc|xls|ppt|pps|vsd)[xm]?|txt|csv|one|pst|url|lnk)$' |
Store all details of the found files to a csv | $office | Sort-Object FullName | Select-Object FullName,Length,LastWriteTime,Mode | Export-Csv -NoTypeInformation C:\Tmp\office_files.csv |
Additional interesting admin and office file extensions can be found at https://filesec.io/. Once the list of file paths and details are saved, one can systematically work through the list and the download potentially interesting files for further analysis.
Browse folders
Because we have the full directory listing at our disposal and during the import the Depth
attribute has been calculated, it is also possible to list directories to better understand the directory structure and sizes of folders.
PS C:\> $csv | Where-Object Depth -EQ 1 | Where-Object FullName -Like 'C:\*' | Format-Table -AutoSize Mode,LastWriteTime,Length,Name
Mode LastWriteTime Length Name
---- ------------- ------ ----
d--hs- 2022-11-08T21:57:12.893547Z 1 $Recycle.Bin
d----- 2023-02-02T22:13:33.703302Z 1 Install
-a-hs- 2023-01-27T14:11:55.974858Z 5905580032 pagefile.sys
d----- 2019-12-07T09:14:52.147417Z 1 PerfLogs
d-r--- 2022-12-02T17:02:34.260006Z 1 Program Files
d-r--- 2022-11-04T21:14:26.375868Z 1 Program Files (x86)
d--h-- 2022-12-02T12:33:08.733117Z 1 ProgramData
d--hs- 2022-11-08T21:57:19.553494Z 1 Recovery
-a-hs- 2023-01-27T14:11:56.639391Z 16777216 swapfile.sys
d--hs- 2022-11-03T18:43:34.085655Z 1 System Volume Information
d-r--- 2022-11-03T18:30:48.610580Z 1 Users
d----- 2023-01-26T22:03:45.756666Z 1 Windows
PS C:\> $csv | Where-Object Depth -EQ 2 | Where-Object FullName -Like 'C:\Users\*' | Format-Table -AutoSize Mode,LastWriteTime,Length,Name
Mode LastWriteTime Length Name
---- ------------- ------ ----
d-rh-- 2022-12-01T22:42:21.545340Z 1 Default
-a-hs- 2019-12-07T09:12:42.731564Z 174 desktop.ini
d-r--- 2022-11-02T21:58:06.552554Z 1 Public
d----- 2022-11-04T21:37:32.159099Z 1 bitsadmin
PS C:\> ($csv | Where-Object FullName -Like 'C:\Install\*' | Measure-Object -Sum Length).Sum / 1MB
513.31
PS C:\>
Future work
Because the .csv
data structure is not optimized for browsing the filesystem like this I started working on a PowerShell provider5 that would allow to load the .json
collected using Dir2json and expose a drive (e.g. d2j:\C\Users\
) to browse, recurse (Get-ChildItem -Recurse
) and filter the filesystem using the native PowerShell functions through the hierarchical structure. Building such provider however took more time than I anticipated so hereby I am dropping the idea with you in case you want to practice writing C# code and learning about PowerShell internals 😊.
Another nice programming challenge would be to implement enumeration of SharePoint environments. This would be relevant because besides filesystem drives and network shares nowadays an increasing amount of information is stored on SharePoint environments. Classes and functions from the Microsoft.SharePoint.Client
namespace appears to be a good start to implement this functionality in C#.
Conclusion
Instead of searching for specific files, a much more efficient method is to perform a one-time collection of the directory listing and then query the file offline through PowerShell for interesting files. Either a PowerShell oneliner can be used to perform the collection, or the Dir2json tool which allows through in-memory execution through BOF.NET in Cobalt Strike.
Analysis of the resulting csv containing the directory structure can be performed effectively using PowerShell’s filtering, grouping and sorting functions, and any interesting files identified can be downloaded and investigated. The PowerShell queries discussed above and various other queries can be found in the CheatSheet.ps1
file in the Dir2json GitHub repository.
References
- Living Off the Foreign Land - Part 1/3: Setup Linux VM for SOCKS routing
- Living Off the Foreign Land - Part 2/3: Configuring the Offensive Windows VM
- Living Off the Foreign Land - Part 3/3: Using Windows as Offensive Platform
- Digging for Secrets on Corporate Shares
- Dealing with large BloodHound datasets