From 802083510729144507dc3f8fe04ddbdff6831f1c Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Fri, 29 May 2026 10:04:55 +0000 Subject: [PATCH] Add content from: JavaScript Analysis for Pentesters --- .../debugging-client-side-js.md | 187 ++++++++++++++++-- 1 file changed, 175 insertions(+), 12 deletions(-) diff --git a/src/pentesting-web/xss-cross-site-scripting/debugging-client-side-js.md b/src/pentesting-web/xss-cross-site-scripting/debugging-client-side-js.md index d47cae3e1bb..b75926647f6 100644 --- a/src/pentesting-web/xss-cross-site-scripting/debugging-client-side-js.md +++ b/src/pentesting-web/xss-cross-site-scripting/debugging-client-side-js.md @@ -2,32 +2,195 @@ {{#include ../../banners/hacktricks-training.md}} -Debugging client side JS can be a pain because every-time you change the URL (including a change in the params used or param values) you need to **reset the breakpoint and reload the page**. +Client-side JavaScript is often the fastest way to find **hidden endpoints**, **frontend-only restrictions**, **DOM sinks**, **JWT signing logic**, and **public secrets**. + +## Recon: collect and triage JS first + +A practical workflow is: + +1. In **Burp Proxy history**, filter requests/responses ending in `.js` +2. Copy the URLs and download them locally: + +```bash +wget -i urls.txt +``` + +3. Enumerate routes and parameters from the downloaded files: + +```bash +python linkfinder.py -i 'js/*' -o cli | sort -u +python linkfinder.py -i 'js/*' -o cli | sort -u | grep -E 'api|rest|admin|auth|debug' +``` + +4. Hunt for secrets and interesting strings: + +```bash +./trufflehog filesystem ./js --no-verification --include-detectors="all" +rg -n 'password|token|secret|apikey|api[_-]?key|auth|admin|jwt|bearer' js/ +``` + +5. Grep for dangerous client-side sinks and trust boundaries: + +```bash +rg -n 'innerHTML|outerHTML|insertAdjacentHTML|eval\(|postMessage\(|addEventListener\(|localStorage|sessionStorage|document.cookie' js/ +``` + +This usually reveals undocumented APIs, reset flows, admin endpoints, token handling, DOM XSS sinks, and places where the frontend is making security decisions. + +## Find execution entry points quickly + +When there is no inline `onclick=`, inspect the element and check **Event Listeners** to jump directly to the handler. + +Useful tricks: + +- **Elements / Event Listeners**: identify the exact callback bound to a button or form. +- **XHR/fetch breakpoint**: set **Any XHR/fetch** so execution pauses immediately before the browser sends the request. +- **Console helper**: `getEventListeners(window)` can quickly enumerate listeners. +- **Step over / into**: follow how the final request body, headers, JWT, or URL is built at runtime. + +This is specially useful when static analysis is noisy because of minification, bundling, or wrappers. + +## Bypass client-side validation by editing runtime variables + +If the browser sanitizes a value before building the request, that filter is **not** a security boundary. + +Typical workflow: + +1. Break inside the submit handler. +2. Step until the sanitized variable appears in the **Scope** panel. +3. Edit the variable value directly in the debugger. +4. Resume execution so the original application code signs/sends the attacker-controlled value. + +Example: if the UI strips everything outside `[^a-z0-9._-]`, a payload like `127.0.0.1;id` may become `127.0.0.1id` in the UI, but you can restore it in-memory before the request or JWT is finalized. + +This is very valuable when the application signs the request client-side and direct proxy tampering would invalidate the signature. + +## Client-side JWT signing keys are public + +If the browser signs a JWT, the **algorithm**, **signing call**, and **key material** are available to the tester. + +Practical workflow: + +1. Break before the final JWT is generated. +2. Step until the encoded token (`ey...`) appears. +3. Step into the signing function. +4. If the code is obfuscated, inspect the unpacking function and packed string arrays until library names and arguments are clear. + +A common pattern is `KJUR.jws.JWS.sign(...)` from **jsrsasign**. For HS256 flows, the secret may appear as the fourth argument and may be hex-encoded. + +```bash +echo '6465623837323564656533323462383134656535386133626434353431373866' | xxd -r -p +echo -n 'deb8725dee324b814ee58a3bd454178f' | base64 +``` + +After recovering the raw secret, load it into Burp **JWT Editor** as a symmetric key, modify claims, and re-sign tokens. + +> If a server trusts JWTs signed with a secret embedded in browser JavaScript, attackers can usually forge arbitrary claims. + +## Minified, beautified, source-mapped, and obfuscated code + +### Beautify first + +Minified code is harder to read but usually still debuggable: + +```bash +js-beautify main.min.js > main.js +``` + +Beautification restores indentation and line breaks, which is often enough to place breakpoints and inspect control flow. + +### Check for source maps + +Search for: + +```javascript +//# sourceMappingURL=main.min.js.map +``` + +If the map is exposed, fetch it and recover much more meaningful source structure, filenames, and variable names. + +### Common obfuscation patterns + +Expect: + +- packed string arrays +- hex indexes like `d(0x1)` +- proxy unpacking functions +- dead branches +- self-defending regexes +- debugger loops +- disabled console methods + +In many cases the goal is not perfect deobfuscation, just simplifying the code enough to understand data flow and set reliable breakpoints. + +## Useful obfuscation bypasses + +### Self-defending code + +If beautifying or editing the file makes the page freeze or recurse forever, look for self-defending checks and catastrophic-backtracking regexes such as: + +```javascript +(((.+)+)+)+$ +``` + +Removing the self-defending call or stripping the regex often restores normal execution. + +### Debug protection + +Some protectors continuously trigger anonymous `debugger` statements when DevTools is open. Inspect the call stack, move upward until you identify the wrapper that only exists for anti-debugging, and remove/comment that call. + +### Disabled console output + +If `console.log` and friends were replaced with empty functions, create a clean console from an iframe: + +```javascript +var iframe = document.createElement('iframe'); +document.body.appendChild(iframe); +console = iframe.contentWindow.console; +``` + +This is useful when instrumenting obfuscated code with your own logging. + +## Persistent modifications ### `debugger;` -If you place the line `debugger;` inside a JS file, when the **browser** executes the JS it will **stop** the **debugger** in that place. Therefore, one way to set constant breakpoints would be to **download all the files locally and change set breakpoints in the JS code**. +If you place `debugger;` inside a JS file, the browser stops there every time the statement executes. -### Overrides +### Chrome/Chromium Local Overrides -Browser overrides allows to have a local copy of the code that is going to be executed and execute that one instead of the one from the remote server.\ -You can **access the overrides** in "Dev Tools" --> "Sources" --> "Overrides". +**Local Overrides** let the browser serve your modified local copy instead of the network response, which is perfect for: -You need to **create a local empty folder to be used to store the overrides**, so just create a new local folder and set is as override in that page. +- removing client-side filters +- adding `console.log` statements +- inserting `debugger;` +- simplifying obfuscated code -Then, in "Dev Tools" --> "Sources" **select the file** you want to override and with **right click select "Save for overrides"**. +Open **DevTools --> Sources --> Overrides**, grant a local folder, then right-click the remote file and choose **Save for overrides**. ![debugger; - Overrides: Then, in "Dev Tools" -- "Sources" select the file you want to override and with right click select "Save for overrides"](<../../images/image (742).png>) -This will **copy the JS file locally** and you will be able to **modify that copy in the browser**. So just add the **`debugger;`** command wherever you want, **save** the change and **reload** the page, and every-time you access that web page **your local JS copy is going to be loaded** and your debugger command maintained in its place: - ![debugger; - Overrides: This will copy the JS file locally and you will be able to modify that copy in the browser . So just add the debugger; command wherever you want, save the change...](<../../images/image (594).png>) -## References +### Burp Suite browser: persistent overrides with HTTP Mock -- [https://www.youtube.com/watch?v=BW\_-RCo9lo8\&t=1529s](https://www.youtube.com/watch?v=BW_-RCo9lo8&t=1529s) +Burp's embedded Chromium browser does not support Chrome Local Overrides in the same way. A practical workaround is the **HTTP Mock** BApp: -{{#include ../../banners/hacktricks-training.md}} +1. Send the original JS request/response to **HTTP Mock** +2. Modify the mocked JavaScript response +3. Save it +4. Reload the page in Burp's browser +This is useful when you want persistent in-browser testing changes without touching the target server. +## References + +- [JavaScript Analysis for Pentesters](https://kpwn.de/posts/javascript-analysis-for-pentesters/) +- [LinkFinder](https://github.com/GerbenJavado/LinkFinder) +- [TruffleHog](https://github.com/trufflesecurity/trufflehog) +- [js-beautify](https://github.com/beautifier/js-beautify) +- [Chrome DevTools Local Overrides](https://developer.chrome.com/docs/devtools/overrides) +- [HTTP Mock - PortSwigger BApp](https://portswigger.net/bappstore/42680f96fc214513bc5211b3f25fd98b) +- [jsrsasign - KJUR.jws.JWS](https://kjur.github.io/jsrsasign/api/symbols/KJUR.jws.JWS.html) +{{#include ../../banners/hacktricks-training.md}}