JavaScript
A very popular language used to create interactivity on the web, and on the backend using NodeJS
# Related Pages
Cross-Site Scripting (XSS)NodeJSPrototype PollutionpostMessage ExploitationCommon Pitfalls
String Replacement
replace
vs replaceAll
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:
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):
Pattern | Inserts |
---|---|
| Inserts a |
| 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 |
| Inserts the |
| Inserts the named capturing group where |
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:
Global Regexes
Regular Expressions (RegEx) in JavaScript can be written in between /
slash characters. After the last slash, flags can be given such as i
for case insensitivity and g
for global search. This global feature is interesting because it can cause some unintuitive behaviour if you don't fully understand its purpose.
One common mistake is the lack of the global flag in a RegEx that is supposed to replace all characters. When using no regex, only the first match is replaced, the same goes for a non-global regex. Only using a global regex or the replaceAll
function, all matches will be replaced:
When a global regex is re-used, another unexpected behaviour can happen. The instance's .test()
and .exec()
methods will keep save .lastIndex
value that stores the last matched index. On the next call, the search is only continued from this last index, not from the start. Only if a match fails will it be reset to the start.
While primarily useful for matching against the same string, this can cause unexpected behaviour when multiple different strings are matched against the same global RegEx:
One example implementation of a check that can be bypassed with this behaviour is the following:
The above check tries to filter out strings matching characters common in XSS payloads, <>"'
. It does so with the /g
global flag and uses .test()
to check for matches. As we now know, this will remember the .lastIndex
on any match so that the next check is offset. We can exploit this by intentionally prepending a large string that matches right at the end, putting .lastIndex=29
. The next match for the script tag or attribute injection will be before the 29th index, and thus not be matched. That allows the following payload to bypass it fully:
Prototype Properties
In JavaScript, all Objects have a prototype that they inherit methods or properties from. See Prototype Pollution for a technique that abuses writable prototypes. Here, we will look at abusing the existing prototypes to bypass certain checks when objects are accessed with dynamic keys.
Take the following code example:
In this example, the username
and password
come from the query string. A check is performed that the username is inside the users dictionary and that its password property matches the given password. Only then will it return true
.
It is vulnerable because not just 'admin'
is a valid key in the users
object. Its inherited prototype properties like .constructor
or .toString
are still valid properties, but are functions instead of a password entry to match against. The users[username]
will pass, but then its .password
property will become undefined
. Luckily, we can match this with our given password by removing the password
query parameter, making it undefined as well.
This was a solution to a simple JavaScript CTF challenge with a detailed writeup below:
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:
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:
Syntax | Meaning |
---|---|
| Backslash |
| Single quote |
| Double quote |
| Backtick |
(0x0a) | New Line |
(0x0d) | Carriage Return |
(0x09) | Horizontal Tab |
(0x0b) | Vertical Tab |
(0x08) | Backspace |
(0x0c) | 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.
Unrelated to strings, you can also use these templates as "tagged templates" to call functions without parentheses:
No alphanumeric characters
Without "
quotes
"
quotesRegExp
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:
Another common method is using String.fromCharCode()
chains to build out string character-by-character:
Comments
A few different and uncommon ways of creating comments in JavaScript:
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:
Source maps
Bundled/minified code is often hard to read, even with the abovementioned tools. If you're lucky, a website might have published .map
source map 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:
There exists a tool sourcemapper
that can take a URL and extract all the source code files:
Add source map from file
Sometimes, the source map is not given to you by the application you are testing, but you can find it online from sources such as GitHub or a CDN. As explained here, Chrome allows you to manually add a source map to a JavaScript file from another URL.
Right-click anywhere inside the minified source code, then press Add source map... and enter the absolute URL where the .map
file can be found.
Note: After reloading, the source map will be lost. You will need to re-add the source map like explained above to see the sources.
Local Overrides
One very useful feature of Chrome's DevTools is its Local Overrides system. You can override the content of any URL by editing a file locally, while you have the DevTools open.
Start by setting up local overrides as explained in the link above. Once configured and enabled (under Sources -> Overrides -> Enable Local Overrides), you can edit any file in the Sources tab and press Ctrl+S to save it. Edits in CSS properties will also be saved. From the Network tab, you can even override response headers in a special .headers
file.
Note: This feature only works when DevTools are open. If you reload the page while they are closed, the overrides will not be used.
Note: This feature does not work in the Burp Suite Browser, because some default arguments prevent access to the filesystem. This is a known issue and you should use your local Chrome installation instead.
Frames
When looking at complex or edge cases, it can be useful to know how the browser understands the current context. The Application -> Frames panel in Chrome is useful for this as it shows a variety of properties of all frames in the current tab, like how the Content-Security-Policy
is parsed, the Origin, the Owner Element, and much more (source).
Snippets
Useful bits of JavaScript that can quickly give information about an application, or help in an exploit. Run these in the DevTools Console or at will using a Bookmarklet.
Log all non-default global (window) variables
Last updated