How to Hack User Logins with Dictionary Attacks and Custom CRMs

Daniel Pericich
19 min readNov 6, 2022
Photo by Hunters Race on Unsplash

*Disclaimer**: This article is not meant for malicious attacks, but is meant to be an informative guide to build secure apps using OWASP best practices for handling authentication.

Data breaches seem like a daily occurrence, and with the average time between initial breach and discovery being 206 days, bad actors have plenty of time to have their way with your accounts. Though a data breach is a great way to get a chunk of users’ records, there is more than one way to gain access to a user’s account.

While hacking accounts is often seen as some elaborate skill that takes deep technical knowledge, in reality it can be a very simple process. After I read the article Life360 Potentially Leaves its Users Sensitive Data at Risk I became interested in how login exploits could work for websites from small online storefronts to enterprise apps, and what measures could be taken to prevent account breaches. This article brought up a number of requirements for application authentication standards that the company in question was missing.

Unfortunately for customers, security is not an easy fix for websites and services. Security is to software engineering what quality is to manufacturing. There are no special widgets that can be stapled on during the last step of your app design for “security.” No, good security and quality are implemented and checked at every step from idea conception through continuous integration and deployment. Luckily we have OWASP (Open Web Application Security Project) to guide us in implementing security best practices.

OWASP is a critical resource for everyone from security experts and software architects to small business owners. It offers resources to inform and empower development of secure apps to protect both companies, and their users. While these guides are freely available, their recommendations are not always followed. Whether it’s for time savings, lack of experience or even conscious negligence, security is often an after thought. For small companies with limited engineering resources it can seem more important to push a requested feature than to fix a buggy login or review endpoint access for business critical APIs.

This situation is a more common occurrence than most tech users know, as the startups that juggle some of your most personal information are built on a bed of proverbial security quicksand. Depending on the security measures of the company you interact with you may be putting full names, dates of birth, active credit cards, social security numbers, banking routing numbers, full contact lists, geolocation data, credit scores and infractions and much more at risk of exposure.

In this article I want to discuss two methods that can be used to exploit weak login authentication flows. Along with theoretical discussions, I want to build a simple account target CRM (all code can be found here https://github.com/dpericich/Password-Cracker), along with some basic login web automations to demonstrate just how easy it can be to penetrate weak logins and exploit user data.

OWASP’s ASVS Framework

A major divide exists between security experts and product owners. This divide is the need to enhance customer safety while reducing product friction. Safety features can make products more difficult and confusing to use. This difficulty leads to an increase in use friction for the service or product and can slow customer acquisition and even cause customers to leave a platform.

While it would seem that product owners should always push for fewer security features to allow for a smoother product experience, this approach would be short sighted. While security features that cause friction may make growth numbers increase more gradually, massive security breaches can empty entire user bases if the product handles sensitive items such as money exchanges, medical prescription fulfillment services or real time location tracking services for your children.

Balancing customer security with customer product experience is an important part of a company’s business plan. Again, OWASP gives all the recommendations for how to fully secure an app or service, but not every cat photo sharing app needs 2 factor authentication, secondary passphrases, device checks and IP filters. Finding a good balance of product features and security features is not a one size fits all situation.

Luckily we have a list of all the recommendations, and can match and apply recommendations as we see fit for our product and customer base. OWASP has a document called the ASVS which provides a basis for testing web application technical security controls and also provides developers with a list of requirements for secure development. It’s about 70 pages of technical documents, but for our purposes we only care about section V2. This section focuses on technical standards for Authentication and presents security items to implement as well as ways to test our apps.

For now, know that this resource exists as we will be returning to it in the second half of this article to help build our CRM and implement web automation logic.

What is a CRM?

Before we can start building tools for password cracking, we need to have systems in place to track our progress. It may seem strange, but criminal organizations follow tech company best practices better than some tech companies do. The most enterprising “companies” leverage terms and procedures from the corporate world to run their businesses efficiently. If we are dealing with a number of potential “leads” and want to be able to track our progress for each of these leads, we will need a CRM.

CRM stands for customer relationship management. A CRM system helps companies stay connected to customers, streamline processes, and improve profitability. These tools are often used for product focused companies who are looking to expand the number of customers buying their products and services and maintain and increase the quantity of purchases or subscription tier for current customers.

In theory a CRM could be as simple as a spreadsheet with names, phone numbers and emails divided into potential and current customers. They can get as complex as diverse groupings (buckets) of current and potential customers who are segmented by location, age, gender and consumer makeup to allow for highly targeted campaigns:

Figure 1. Basic CRM for acquisition, retention and retargeting.

There is a large range between these complexity bounds, but we will settle somewhere closer to the back of the napkin version of the CRM. We’ll touch more on exactly what we’ll track in our CRM when we build it, but for now we can agree to track both the services or websites to attack, and our progress for the individual accounts targeted.

Guessing Passwords — The Brute Force Way

There are many ways to force your way through a login screen, but the most straight forward method is through brute force. A brute force attack is the process of repeatedly submitting many potential passwords in hopes of finding the correct password for a user. This could entail anything from guessing random words or phrases all the way up to running through all alpha numeric and special character combinations for certain password lengths.

The latter method would take an inordinate amount of time and processing power to successfully find correct passwords. Along with issues for needing large amounts of resources, an automated brute force attack will have trouble succeeding if the target company has imposed a backoff on login attempts or freezes the account after a set number of failed logins. An account backoff occurs if the website forces a certain amount of time to pass between failed password attempts. While login endpoints can handle thousands of requests a second, a backoff could limit you to three attempts per 5 minutes or worse.

To be able to make our tool better, we will take the backoff into consideration to avoid locking ourselves out, but will also need a better approach to limit our potential number of attempts. Again, each potential client in our CRM is their own entity so if we hit a backoff or a lockout then we can note this and move to the next client. This doesn’t mean we can be naïve about our approach.

Reading Down the Dictionary

To be more clever with our brute force attack we can use a dictionary attack. A dictionary attack reduces the total possible combination of characters or terms to a smaller, more manageable set which can be iterated over with a group of accounts. This is a smarter approach as there are terms and passwords that are common among populations, network devices and software engineering in general.

A dictionary attack still mimics the brute force attack as we are repeatedly trying different passwords for target accounts until we gain access. Where this approach differs is that the smaller set of guesses from our segmented dictionary of potential passwords is more agile and may help get around some quantity based security measures.

There are many ways to construct this dictionary. We could search the top 1,000 or 10,000 most common passwords, or get the top 3,000 words for a target service’s language. If our goal was more targeted we could do social engineering to leverage publicly available information on a user to generate a user specific list of passwords. Whatever it may be, having a subset of guesses will always beat trying billions or trillions of random alphanumeric permutations:

Figure 2. Different potential data sources for building a password dictionary.

Trapdoors and Encryption

We’ve talked about brute force methods for accessing accounts that we have no information on, but what would happen if we had more information about our targets? Unfortunately for the general public, data breaches occur and a common data set to steal is usernames and passwords.

This would seem like a lost cause for us as nothing could be easier than breaking into something that you have the address and key for. However, this process is not as simple as it would seem. Unless a company is bordering on gross negligence, any passwords stored for a username password login will be encrypted. What does this mean for users?

If we have a user who has an affinity for aardvarks, then they may use the proper name of their favorite anteater to secure their account. When the user first goes through the login flow they will enter “aardvark” as their password, but this value will not be entered in the database. Instead, this plain text password will pass through a trap door function and be transformed into something like “6h72k#LpN0.” This encrypted password is a computer generated hash value that leverages cryptography for security.

Trap door functions are great for encryption because, as their name implies, it is very easy to pass something through them to get a repeatable result, but almost impossible to retrieve their original value given the encrypted output value. Our password “aardvark” is easy to read and understand, but the hash “6h72k#LpN0” has no meaning. If we know the input, finding the output is trivial. However, if all we have is the output then there is no way for us to determine what the output means:

Figure 3. Trapdoor functions and their one way nature.

This is where the security of storing encrypted passwords comes into play. It is very easy for us to accept the plain text password of our user at login, run it through our encryption function on the server and diff the given encrypted password’s value with the encrypted value stored in the database. Again, this encrypted value has no meaning to us as there is no way to tell what the plain text version is, but there is a way to get around this.

A main reason for why weak passwords are bad is that they allow for easy encryption checks to occur. If you had a data breach with 1,000 username and encrypted password sets, there are some methods we can use to determine the password values. First, we can determine the main language of the users, which will allow us to select the top 1,000–5,000 words for their spoken language.

After we get this term set, we can research the tech stack for the target company or service. If the company is development focused and builds solutions from scratch we can determine the stack components and investigate the most popular authentication packages for the used frameworks. If the service is more CRM driven, using a service like WordPress, our best bet is looking for popular authentication plugin to guess what the target is using.

By determining what the most likely tool for authentication is, we now have the potential tools to turn our most used plain text words list into their encrypted password equivalents. We aren’t limited to doing this with a simple tool. If we had 4 npm packages for authentication we could run our top used words list through all 4 and create 4 separate sets of ciphers to compare our user data artifact against.

The beauty of this approach is for very little effort, and without interacting with the accounts or service, we can test for and find matches for all of our encrypted data. Even if we are not interacting with the service, and don’t have to worry about alerting someone to our hacking intentions, we’d still like to have an optimal process.

To make this an O(n) time operation we can store our authentication package output values as a hash where the encrypted value is the key and the plain text password is the value. Then we can iterate over the list of encrypted user passwords, calling the hash by key which is an O(1) time lookup. If there is a match, we can update our user record with the potential plain text match:

Figure 4. Naive approach for using encryption methods to decipher encrypted password breaches.

The previous method relies on the idea that the hash function for encrypting passwords has not been salted. A salt is a cryptographically generated value that is added as an argument to the trapdoor function to ensure that the output value is always unique, regardless of repeated inputs. This prevents hackers from being able to create ciphers like we just did.

Using a salt in your trapdoor function is a best practice, but not a consensus practice so there is always a chance a hacker could get lucky if their target encrypts passwords with a naive trapdoor function.

With anything nefarious like this, the payout of large breaches is a much smaller subset of useable data. There is no honor among thieves and the criminals who breach databases are likely to pump their record set’s with stale or useless records to make their products seem like a better deal.

Out of a 1,000 records sales package, a hacker may only hit on 10–20 records, but this is still enough for a +$10,000 payout. While skimming credit card information or bank routing numbers is a one time heist, an especially clever hacker may become a repeat offender on your food order or restaurant apps, so make sure you don’t link a credit card to these.

Now we’ve discussed what a CRM is, different ways to implement brute force attacks, and how a hacker may deal with data breach data. Let’s get a little more technical and dig into OWASPs ASVS docs section V2 to look at how we may interact with login pages.

Setting Up or Project

Before we build anything I’d like to offer another disclaimer. I will not be building a full featured app as I do not want to create tools for script kiddies. This article and exercise are meant to be explanations of why following user login best practices is important to protecting your data. Simply forcing users to have a complex passwords renders a lot of these methods useless, so it is good to start with the basics even if you think that it may lead to a slightly worse user experience.

To get started you will spin up a new rails project in any directory of your choice by running “rails new password-checker” in your terminal. This command will initialize a new rails project called password-checker. Change into the password-checker directory with “cd password-checker” and run “rails server” to verify that the setup worked. Your browser should launch a new tab with the Rails project home screen.

There are only a few components we will need to build for this project. They are a target users table to store all the usernames that we interact with (our CRM), we will need to create a services table to track configurations for different stores and platforms that we target, a dictionary table to manage the password guess sets we create for different targets, a controller to manage our calls to the web scraper and finally logs to store critical messages while we’re scraping. I am omitting creating the login automation tools as again, this is not meant to empower hackers, but educate developers and company owners on better security.

Target User Model

The whole reason to build this app is to complete our incomplete target user records. Why are these records incomplete? They are missing the crucial piece of data needed to login: the plain text password. Our user model will not be as simple as “username” and “password” but will need to include a number of other fields as well.

A user record is not unique based on the username, as a user, or multiple different users, may share usernames across different platforms and services. Therefore our unique identifier will be a composite key of the username and the target platform. We can add a validator to our model to check the uniqueness of our records, but for our schema we will need to make sure we store a foreign key of the target service on each user record.

Along with storing a foreign key for the target service, we will want to have a status field on each record. This status field will correspond to the state of our record and should match any critical logging messages that we store. We can use an enum on our model to set expected status values, but we can expect 3 values. These values are “pending”, “completed” and “locked”. We will discuss these values more when we create our critical logging messages table.

There are two more fields that we will need on our users table, a dictionary foreign key as well as a current position index value. We will be working through a specific dictionary for each of these users based off the service as well as the location and demographics of the user. We can keep track of the dictionary to use on the user record by including the dictionary record’s unique key.

With the dictionary we want to keep track of the values we have tried. Because of login backoffs and account freezes we may need to pause between guessing passwords instead of trying the entire dictionary in a single sitting. In order to keep our place we can store the index of our position in the dictionary on the user record and iterate this value with every guess.

The output of our work for our users table looks like this:

Figure 5. Schema for users table.

Critical Event Logging

We’ve built our user table, but now we want to create logs for critical events. We are potentially testing thousands of passwords for each user so keeping track of the outcome of our attempts is critical. Considering the value of our data to log, it becomes clear that there are a large number of outcome occurrences we don’t care about.

If we test a password and it fails login, we don’t care. However, if we test a login and the account locks, this is something we’d be very interested in. Along with locks, we care about warnings for attempt backoffs as this could be a red flag for platform managers about rogue actors. Both of these events are negative outcomes that we may want to log.

The other event we’re interested in is successful logins. In any case we want to store a log record with more than just the enum value. Along with the outcome enum, we will also store the foreign key for the target user and foreign key of the target service. Sure, storing the target service foreign key on this record when we could access it from the user relationship seems unnecessary. We will do this anyways because it will simplify any queries we may want to run on the event logs by service in the future. With security it is important to make data science and dashboards easy to populate.

This will give us a full picture of the attempt that took place. With this understanding of potential outcomes and their meaning our table will look like this:

Figure 6. Schema for event logs table.

Dictionary Records

It could seem like a good idea to store our array of potential passwords as a constant on our main controller, but dictionaries change and our approach will break with modified constants. Instead of having a variable constant on our controller, we will have a table to store every iteration of our dictionary.

Why would we have multiple dictionary records? Maybe we have targets that speak different languages so we need to change the most used words from a language. Maybe there’s an organization with shared dates, words and phrases that could be good potential passwords we want to incorporate into another dictionary. Whatever the cause of the permutation, it is good to be able to keep records of what dictionary sets we plan to use for a user or set of users.

This record is straightforward as we will have only one field to set, an array of terms included in the dictionary instance. Our schema will list the array as a field named “terms” of type string, but we can set this field to act as an array on the Dictionary model itself.

Target Services

Our final model to create is the services model, which will store a lot of our logic for interacting with OWASP ASVS settings. Reading through ASVS V2 it becomes very apparent that brute force attacks and dictionary attacks should not cause any issues for even medium security conscious companies. There are about two dozen recommendations that can slow or completely prevent hackers from getting these attacks to work.

Because even minimum security standards are not followed by a large group of companies and organizations, let’s look at what data our services model needs to safely break through an insecure login. The first attributes we care about are the name of our service and a base url. To get the web automation tools to work we will need an entry point for where to point our tool. This is where the base_url value comes in.

Assuming this allows us to login into the navigation page, there are number of factors that affect how to interact with the login. The first bullet point of ASVS V2 section goes into many of these factors. To be able to correctly populate out our services instance’s values, we can take a couple throwaway user accounts to find the limits. Along with this we can use Google dorking to search for potential authentication documentation or API specs that give us exact answers for rate limits and endpoint behavior.

The first value we will need to populate is the failed logins before hard account lock. This will be our far wall that we can’t pass without potentially tipping our hand to the rightful account owner. Along with this we need to know if there are backoffs imposed on a set number of failed attempts in a time period. We should be mindful of this as we never want to hit a number of failed attempts that will cause a backoff to occur.

If the service does not have account locks or backoffs, we are free to run requests at will. One hang up we may have though is API rate limiting. Even if the service won’t lock us out, they may cut us off from their service for a period of time if we are sending them too much traffic. There are certain countries that are well know for mischief so we may want to use a VPN to test for any blocked IP address areas and add these values to our blocked_ip_origins attribute. Assuming the hacker is using a VPN this should not be an issue, but it would be smart not to have the first interaction with a service’s account being from a blocked IP.

Our services table will look like this:

Figure 7. Schema for targeted services table.

These are the considerations we can glean from the ASVS V2 documentation. There are another 8 pages of specs that if put in place should make our methods impossible to implement, but they are not as common outside of OAUTH security companies or more serious shops and services.

Handling User Login Attempts

Now that we have the models in place to manage our data, we need some endpoints to call to put our app into action. We will have a “Checker” controller that has two main methods: “check_user” to look at interact with a single user and “check_user_batch” to interact with with a collection of users. For this article we will only look at how “check_user” would work:

Figure 8. CheckersController with single record and batch record actions.

In this code chunk above we can see a simple approach being applied for “check_user.” This method receives a single user_id that we can then use to retrieve our user record, service record and dictionary record. We assume that there is no hard account lock, and instead only watch for the backoff. In order to do this we can initialize a counter variable and increment and complete our checks within a for loop to ensure we don’t hit a backoff.

Each time we make another loop through the while loop we will call the automation tool on our specified service to make an attempt for the user. We can pass in the current indexed term from our dictionary and wait for a response. Based on the response we have one of three next steps.

If we are able to log in, the service will return “success.” We will create an event log record for the service, user and message, update the password field value on our user record and return a json response to our UI. If we get back locked, we will update the user status to “locked,” create an event log with “locked” and render a json response with the status.

The more interesting case comes when the response has a status of “failure.” As we stated in the event logging section, we do not care about tracking failed attempts. We will need to update the current index of our user’s current_dictionary_index value and increment the current_attempts. We will not render json on this response either as the backoff json will only be sent if we hit a condition to exit our while loop.

I have added another method in our Checker controller which is called “check_user_batch.” This method should allow for running our login checks against a collection of user records instead of a single user record. The logic will largely be the same, but I will not dive into how to fully set it up. Here’s a little homework if you’re wishing to push your understanding.

Conclusion

If you own or build a well secured app then there is absolutely no reason that these methods should work. Two factor authentication, rate limiting, preventing users from using specific common passwords and soft locks make these methods useless. I could continue on how foolish it would be to build an app that allows for any of these methods, but if you’re reading this then you’re probably not one of the hundreds or thousands of companies that are risking data breaches and user safety by using insecure logins. I hope that this exercise has shown you how some common, light friction security measures can prevent unwanted visitors from your website.

Notes

https://dataprot.net/statistics/data-breach-statistics/#:~:text=The%20average%20time%20it%20takes,an%20organization%20is%20206%20days.&text=IBM's%20annual%20Cost%20of%20Data,by%20nine%20days%20since%202018.

https://hackernoon.com/life360-potentially-leaves-its-users-sensitive-data-at-risk

https://www.google.com/search?q=the+cuckoos+egg&oq=the+cuckoos+egg&aqs=chrome..69i57.3346j0j7&sourceid=chrome&ie=UTF-8

https://owasp.org/www-project-application-security-verification-standard/

https://www.salesforce.com/crm/what-is-crm/

https://www.fortinet.com/resources/cyberglossary/brute-force-attack

--

--

Daniel Pericich

Former Big Beer Engineer turned Full Stack Software Engineer