Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 175 additions & 12 deletions src/pentesting-web/xss-cross-site-scripting/debugging-client-side-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}