What is the speed and frequency that hackers can bypass rate limiting continuously in a website using login with an SMS OTP? The rate-limiting activates if the same IP address triggers SMS OTP more than x times.
If someone has access to a botnet, even a small one, they could change the IP that their actions are coming from several times per minute or flood their actions from a million IPs in any given moment.
For individuals without a botnet, there are several methods, including resetting their ISP modem, which, in some cases, resets the IP. Turning a phone's airplane mode on/off can also trigger a change in IP (MechMK1 reports this might take as little as 3 seconds to complete).
They could also change between public WiFi networks.
Using a series of VPNs and proxies or VPNs that permit the user to choose the exit node will also work, and services that have different exit nodes can make this switch in seconds. There are also proxy/Tor utilities that can randomise your IP according to a schedule you set. The Janus tool has a default change time of 1 minute, but that can be changed.
Therefore, for any practical case, if the attacker is waiting for the SMS code to come back from your server, then they are only limited by the time it takes to receive the SMS code.
No one seems to have addressed this yet, but I'd like to introduce the biggest problem to IP based rate limiting: IPv6.
If you (naively) applied the same logic as you do to IPv4 addresses to IPv6 addresses (i.e. block the address after a certain number of attempts), your rate limiting will be thwarted in about zero seconds.
Why? Well, the simple version is that IPv6 addressing scheme is substantially different than IPv4. IPv4 is rationed to the highest degree, so much so that even multiple homes and physical city blocks may have the same external address. This greatly contrasts with IPv6 where each machine, by convention, will have a /64 block addresses assigned to it. If you aren't familiar with the /x notation, this means that the first 64 bits of the address are fixed while the rest are left free. Since IPv6 addresses are 128 bits long, this means that each machine, by convention, has 2^64 unique addresses which it can use as it pleases! If you used the same approach as with IPv4 and simply blocked one address, the attacker could simply pick a brand new one from their essentially infinite pool of addresses! Your rate limit is then absolutely destroyed and they can submit an unlimited number of OTP codes as quickly as they want. This arguably makes your 2FA implementation useless since if your OTP code is six digits long, the attacker just needs to keep flooding you until they get lucky. In my real world testing, I found I could break a six digit OTP using this method in under six hours using a single thread so it's very practical and a very real threat.
What can we do to stop this? The natural choice would be to ban the entire /64 block of the requester when they hit the limit instead of just their specific address. On the surface, this seems suitable as it means that the attacker can no longer use any of the addresses in their pool.
This, however, also doesn't work even though far too many companies think it does. The problem is that while a host is generally assigned a /64 by convention, networks are designed to have more than one device on them! This means that even home networks will have oodles of /64 blocks (because why not? we're not going to run out!) given to them so that they can handle a bunch of devices. In my case, I was given a /56 block by my ISP to break up among my devices as desired. This means I actually have 2^8 (or 256) /64 blocks to hand out and break up among the devices on my network as I please.
Since there's nothing stopping me from handing the entire /56 block to one host, that means I have 256 /64 blocks at my disposal to cycle through and circumvent your rate limiting. If you let your users try five OTP codes per /64 block before banning them for some period of time, the attacker (using a residential connection!) would be able to hit you with 1,280 codes before they had to wait for the cooldown period. Compared to a classic IPv4 circumvention attempt (using a botnet, proxies, etc) this is an enormous gain for essentially just using what they already are given for free. If you combine this with a botnet (or even just by paying a small amount for a larger "enterprise size" IPv6 pool), IP based rate limiting is thwarted again for very very cheap.
So what now? Do we start blocking /56s for the purposes of rate limiting? You could, but this would have pretty unfortunate effects since although some people are given /56 blocks, others aren't and so you may very well end up accidentally blocking random people for no reason while not solving the problem of easily buying /52 and /50 blocks. The next reasonable choice would be to try and deduce what range assignment a request is originating from. While this would work, it isn't exactly possible as ISPs do not publish or release this sort of information. With this off the table, we (as far as I know!) are out of options. IP based rate limiting fundamentally does not work on IPv6 against an attacker who actually understands how IPv6 works.
But all is not lost! While IP based rate limiting is useless, rate limiting as a concept is not yet dead. You simply need to pick a different resource to limit access to. Instead of limiting the number of times an IP address (or range, or block, etc) can attempt to attempt OTP verification, you should limit the number of times an account may have OTP performed against it. By locking out the account after a certain number of failed OTP attempts and requiring the user click a link in their email to reactivate OTP, the attacker will not be able to bruteforce the OTP for the account no matter how many addresses they can wield. By setting this limit fairly high (say around 25), a normal user should never be inconvenienced by the policy unless something is very wrong with their authenticator (and so would probably need to initiate recovery anyways).
Such a technique has been used effectively by Instagram, Facebook, LastPass, and likely many more for both SMS reset codes and OTP. It is very effective and, in my opinion, the only safe way to block brute force attempts in a world where IPv6 is no longer simply ubiquitous but often mandatory as quite a few networks (mainly mobile!) are slowly transitioning to IPv6 only and using proxies to access 'legacy' IPv4 resources.
If a hacker is in posession of a botnet, they can have control over thousands of IP addresses and can switch between them at will. So there is not really a practical limit to the frequency and speed with which they can change IP addresses.
This depends on the network between the attacker machine and your server. If it is across the world, the attacker may be limited to single IP address or small number of them. Some cloud providers allow to change IP address of the virtual machine in the cloud the attacker may own, or the attacker may just start another virtual machine. Scripting support may be limited and the action may require multiple seconds at least.
If both computers are inside the same local network, it may be possible to change IP address of the attacker with a single command. That command can be launched from something like Python script that does the rest of attack and take only milliseconds to execute. It must be run as root but this is unlikely to be a big problem.