JavaScript Analysis for Pentesters

If you’re pentesting web applications, you certainly come across a lot of JavaScript. Nearly every web application nowadays is using it. Frameworks like Angular, React and Vue.js place a lot of functionality and business logic of web applications into the front end. Thus, to thoroughly pentest web applications, you have to analyze their client-side JavaScript.

This blog post shows you how. It covers the basics of static and dynamic analysis, introduces obfuscation and deobfuscation, and explains how to bypass code protection mechanisms while giving practical examples and suggesting the proper tools for particular tasks.

Note that this blog post is quite long. You can and should skip the topics you already are familiar with. Just use the Table of Contents above to easily navigate through the document. Now, without further ado, let’s dive in! ⤵️

Static Analysis

Static analysis is the analysis of software without its execution. The objectives of static analysis can be numerous. URLs within the code may increase the attack surface and reveal broken access controls. Code could also contain sensitive information like passwords, secrets or API keys. Also, the use of dangerous functions or outdated software might introduce vulnerabilities to the application.

Gather JavaScript Code

To perform a static analysis, you first need to gather the JavaScript code. The easiest way I know of is to use Burp Suite as follows:

  1. Filter Proxy HTTP history to only show files with the js extension:Within Burp Suite’s Proxy HTTP history, click the Filter bar at the top of the GUI.Burp Suite’s proxy history
  2. Mark the resulting list of JavaScript files and Copy URLs:Under “Filter by file extension” choose “Show only” js.Proxy history’s filter settings
  3. Save the URLs to a text file:Mark the remaining requests in HTTP history, right-click and choose “Copy URLs” from the context menu.Context menu to copy selected URLs
  4. Use wget -i urls.txt to download them:Screenshot of the console executing wget -i urls.txt. The output shows that a JavaScript file is being downloaded.Batch download with wget

Alternatively, you can use the developer tools of your browser, to download files one by one:

The “Sources” tab of the developer tools in Chromium list JavaScript files used by the web application.
Chromium’s developer tools

Identify Endpoints

To discover endpoints and parameters in JavaScript files, you can use LinkFinder. Results can be either saved to HTML or printed to stdout:

$ python linkfinder.py -i 'js/*' -o result.html
$ python linkfinder.py -i 'js/*' -o cli

The combination with other command line tools can also be beneficial:

$ python linkfinder.py -i 'js/*' -o cli | sort -u | grep rest
/rest/admin
/rest/captcha
/rest/chatbot
/rest/continue-code
/rest/continue-code/apply/
/rest/continue-code-findIt
/rest/continue-code-findIt/apply/
/rest/continue-code-fixIt
/rest/continue-code-fixIt/apply/
/rest/country-mapping
/rest/deluxe-membership
/rest/image-captcha/
/rest/memories
/rest/order-history
/rest/products
/rest/repeat-notification
/rest/saveLoginIp
/rest/track-order
/rest/user
/rest/user/authentication-details/
/rest/user/change-password?current=
/rest/user/login
/rest/user/reset-password
/rest/user/security-question?email=
/rest/user/whoami
/rest/wallet/balance

Detect Secrets

To detect secrets in code, you can use TruffleHog. Earlier, TruffleHog focused on secrets within git repositories. Nowadays, it natively supports filesystems and more. Just make sure to use the sub-command filesystem.

$ ./trufflehog filesystem ~/Downloads/js --no-verification --include-detectors="all"
🐷🔑🐷  TruffleHog. Unearth your secrets. 🐷🔑🐷

Found unverified result 🐷🔑❓
Detector Type: AWS
Decode Type: PLAIN
Raw result: AKIAIOSFODNN7EXAMPLE
File: ~/Downloads/js/main.js

Burp Suite Professional users can also use JS Miner to detect endpoints and secrets. Issues will show up in Burp Suite’s Issue dashboard as soon as JavaScript files are passively analyzed.

The Issue dashboard of Burp Suite showing findings from JS Miner. Several endpoints along with secrets are detected in the JavaScript file.
Burp Suite’s Issue dashboard

In my experience, JS Miner delivers the best results for detecting secrets and endpoints in JavaScript.

The issue’s details in Burp Suite show that 3 secrets have been detected: a password, an API key and an AWS secret access key.
Detected secrets of JS Miner

If you are searching for something specific, you can of course also use basic command line tools like grep. Keywords you might want to try are:

password
admin
login
token
user
auth
key

Locate Dangerous Functions

An open-source analysis tool to detect vulnerabilities in code is Semgrep. You can configure your own detection rules or use rules created by the community. These are able to detect secrets but also the use of potentially vulnerable methods:

Exemplary call: semgrep scan --config auto returns a detected JWT and an innerHTML function in the JavaScript files.
Semgrep detecting a JWT and an innerHTML function

Interesting functions and properties in JavaScript are for example:

Element.innerHTML
eval()
window.postMessage()
window.addEventListener()
window.localStorage
window.sessionStorage
document.cookie

Discover Outdated Libraries

Outdated JavaScript libraries often contain vulnerabilities. A common example is jQuery. Often, you can find version information in the path or file name of the library or as a version string in the file itself.

HTTP request in Burp Suite to /ajax/libs/jquery/2.2.4/jquery.min.js. The response contains the version string jQuery v2.2.4 at the beginning of the file.
Version strings in path and file

To check whether vulnerabilities are published, you can use online services such as snyk.io.

The page https://security.snyk.io/package/npm/jquery/2.2.4 shows that there is a Cross-site Scripting (XSS) vulnerability in jQuery 2.2.4.
Results from snyk.io for jQuery 2.2.4

Burp Suite Professional has a built-in dependency checker that automates this procedure. Additionally, the Burp Suite Professional extension Retire.js can be used.

The issue’s details in Burp suite shows that jQuery 2.2.4 was detected and has known vulnerabilities like CVE-2015-9251.
Outdated jQuery version detected by Retire.js

Attention: Before reporting, verify that the web application is actually vulnerable! Vulnerabilities in libraries often affect only specific functions. If the web application does not use these functions, it is not vulnerable despite including the library. Search the web application’s JavaScript for vulnerable functions with the methods described above.

Dynamic analysis

Dynamic analysis is the analysis of software during its execution. Generally, you do not want to analyze the entire software but only a specific part or function. This lets you reconstruct its functionality and is superior to static analysis for complex computations.

Basic Tools

The basic tools for dynamic analysis of JavaScript are your browser’s developer tools. I prefer the developer tools of Chromium to Firefox’s due to their higher performance.

You can open them either via the main menu bar or by pressing F12.

Screenshot of Chromium menu. Press the main menu, “More tools” and “Developer tools”.
Opening developer tools in Chromium

This will open a toolbar in your current active tab. Choose the Sources tab.

It consists of 4 parts:

  1. The source files of the current page
  2. The content of the selected file
  3. Tools for debugging
  4. The interactive console (open with ESC if not present)Screenshot of the Chromium developer tools with labels in the parts described above.Developer tool’s Sources tab

Example Application

I programmed a small example application to demonstrate the basic techniques. It features a simple ping service, where you can enter a host and receive the output of the ping command. Such a service ought to be vulnerable to command injection. So, let’s take a closer look.

Screenshot of the ping service. 127.0.0.1 was entered into the host field and the result is the output of a Linux ping command for that host.
Exemplary ping service

Client-Side Filtering

First, I analyze the request that is sent when the submit button is pressed. A JWT is sent to the server that among other values contains the host to be pinged. JWTs are signed. This makes it impossible for us to manipulate the host value within Burp Suite or perform an active scan effectively without invalidating the signature.

Screenshot of Burp Suite Request that was sent. The JWT in the body has 3 parts, separated by a period. The parts themselves are base64 encoded. Burp Suite inspector decodes the payload to a dictionary containing “host”:“127.0.0.1”.
Request being sent in Burp Suite

Thus, I try to send the basic command injection payload 127.0.0.1;id from the web application itself. The image below shows how the injection is supposed to work.

Assumed code is “ping -c 1 {host}”. Our command injection terminates the ping command and appends the id command.
Clarified command injection and inserted payload into input field

But the special character ; is filtered on the client-side. The host value inside the JWT contains the value 127.0.0.1id.

Screenshot of Burp Suite Request again. The host value now is “127.0.0.1id” without the semicolon.
Filtered request in Burp Suite

Finding the Entry Point

To analyze what’s happening when the submit button is pressed, I right-click it and choose Inspect.

Screenshot from browser, showing the context menu when the submit button is right-clicked.
Inspect HTML element

This opens the Elements tab in the developer tools. The buttons’ HTML does not reveal anything as there is no onclick property. But choosing the Event Listeners tab shows that an event listener was registered to call the submit function in line 3 of secure.js.

Screenshot of the developer tools, showing the event listener described above.
Event Listeners tab in developer tools

I switch back to the Sources tab by clicking the link to secure.js:3. To analyze the function, I create a breakpoint in line 4 by clicking the line number on the left. The breakpoint is displayed in the toolbar on the right.

Screenshot of the developer tools, showing the registered breakpoint.
Breakpoints in the Sources tab

The Debugger

I click Submit again. The JavaScript execution stops at line 4 and the debugger starts. I step through the particular JavaScript statements by pressing F9 or clicking the appropriate symbol on top of the toolbar.

The browser that has stopped the JavaScript execution. The “Step” button is hovered.
Debugger tools to step through instructions

After each statement that contains a variable, its contents are displayed next to it after being executed, highlighted in yellow. A regular expression seems to remove all special characters but ._-.

Line 5 contains a RegExp “[^a-z0-9._-]”. It is used to delete special characters from the host value.
Variable values are displayed inside the debugger

Before the sanitized host value is submitted as payload to the JWT, I use the Scope section of the toolbar to change the value of the variable back to the original input.

Screenshot of the browser. In the developer tools toolbar within the “Scope” section, the host value is being edited back to “127.0.0.1;id”.
Changing the value back to 127.0.0.1;id

Hitting the play button resumes code execution as normal and shows that the command injection was successful.

The result of the ping service is the output of the Linux id command: “uid=1000(konstantin) …”
Results of the id command

General Advice

Three more notes:

  1. To detect the entry point, it sometimes is helpful to set a breakpoint on Any XHR/fetch via the toolbar. This way, the execution pauses before the XHR is sent and all of the computed variables of the previous statements are displayed.Developer tools with highlight on the add XHR/fetch Breakpoints section.Add XHR/fetch breakpoint
  2. The signing key is of course also present in the JavaScript. It could be extracted and used in Burp Suite to calculate the correct signature there. I’ll cover this in a follow-up thread.
  3. DOM Invader is an extension to Burp Suite’s built-in browser that helps to test for DOM XSS. PortSwigger’s resources to it are pretty good – as usual. If you want me to cover it in a blog post as well, just reach out.

Obfuscation & Deobfuscation

Before we get into obfuscation techniques, let’s start with the concepts of minification and beautification.

Minification

Minification is the process of reducing the file size of JavaScript, while maintaining its functionality. This is achieved by removing unnecessary characters like white spaces or new lines. Sometimes minification also includes shortening variable names and refactoring the source code. Many JavaScript frameworks minify code to be published by default.

The goal of minification is to increase the efficiency of the JavaScript’s transmission. The following is a (somewhat artificial) example of minified code, created with UglifyJS, but clarifies the point.

Unfortunately, I cannot fit the entire code into the alt text. The plain code reads by getElementById().value, checks whether it is null and writes it into another HTML element if it isn’t. The minified code is modified as described in the next toot.
Example of minification

The file size decreased by a notable amount of ~40%, by performing the following transformations:

  • White spaces, new lines, comments and unnecessary curly brackets were removed.
  • The local variable input was renamed to e.
  • The if-else-statement was replaced with the so-called ternary operator (x?a:b).

By the way, you can often recognize minified sources by their extension .min.js.

Beautification

Beautification is the process of making minified code human-readable again. There are several ways how to beautify minified code:

  • The Chromium debugger pretty-prints JavaScript sources by default. This adds indents and line breaks to the code.
  • To beautify local JavaScript files, you can use a tool like js-beautify:# Install $ pip install jsbeautifier # Usage $ js-beautify main.min.js > main.js
  • There also is an online service:

However, note that variable renaming cannot be reversed with this process.

Source Maps

A way to completely recover minified code are so-called source maps. Source map files just contain a JSON object as can be seen below. The crucial part is the mappings string, which contains the mapping between the original and the minified code.

The mappings array in this example contains the values “AAAA,SAASA,UACR,IAAIC,EAAMC,…”.
Example of source maps

Mappings are VLQ Base64 encoded, which I will not discuss in detail here.1

You can visualize the mapping via source map visualizers. For example, 23->2:5 means that the word at column 23 e= maps to the word at line 2, column 5 input=.

Screenshot of a source map visualizer. It highlights code in the original and the minified file in the same color.
Example of source map visualization

Sometimes source maps are shipped along with the minified code. This is done by adding a comment to the minified JavaScript that contains the location of the source map. The source map will only be loaded when developer tools are opened.

Screenshot of the developer tools. The file main.js was generated from the source map that is shipped within the minified JavaScript file via a JS comment: //# sourceMappingURL=main.min.js.map
Reference to source map from minified JavaScript

Minification and Analysis

During the analysis of JavaScript, minification is not that big of a problem. Yes, without source maps, the code is less comprehensible due to irreversible transformations. However, secrets, links, and calls to vulnerable functions can still be detected by a static analysis, as discussed above.

During dynamic analysis, the lack of descriptive identifiers is compensated by the debugger in Chromium’s developer tools. It displays the values of variables next to the JavaScript statements after their execution.

Obfuscation

Obfuscation of JavaScript has the goal to prevent its analysis, be it static or dynamic, while maintaining its functionality. To achieve this, even a loss in the code’s performance is accepted. Often, obfuscators put words and symbols from within the code into an array. During the execution, the original code is rebuilt by recurring references to that array. The image shows an example of this so-called packing.

The image shows plain code and slightly obfuscated code to clarify the idea of packing.
Example of a packed array in obfuscated code

As you can see, the following transformation happened:

document.getElementById("input").value ➡️ document[d(0x0)](d(0x1))[d(0x2)];

d is a function that references the array, defined in function a0x0 is the first string, 0x1 is the second, and so on.

Some other transformations that are frequently used:

  • Renaming identifiers to hexadecimal strings
  • Self-defending code that breaks when beautified
  • Injecting dead code that increases the file size

Some tools even offer debug protection making it “almost impossible to use the debugger function of the developer tools”2. There are several obfuscators across the Internet. A popular one is javascript-obfuscator You can use it from the command line or online.

Many more obfuscators in diverse variations exist. A prominent one is JSFuck. It uses only these six characters: ![]+()

The screenshot shows the plain code alert(1) and its obfuscation with JSFuck. The obfuscation is a string with a length of 2345 while only consisting of the 6 characters ![]+().
Example of obfuscation with JSFuck

Deobfuscation

Deobfuscation is the process of making obfuscated code human-readable again. As you can imagine from the examples above, this is not trivially possible. Beautifying obfuscated code is only one step and still leaves the code nearly unreadable.

Additionally, deobfuscators often rely on the following techniques:

  • Unpacking arrays
  • Replacing proxy functions
  • Removing dead code branches
  • Renaming identifiers

There also are a lot of deobfuscators available on the Internet. An example is javascript-deobfuscator. It’s usable from the command line or online again. I also like the results of JSNice. It renames identifiers based on a statistical model, which often helps to understand code better.

Obfuscation and Analysis

Unfortunately, deobfuscated results usually don’t get anywhere near the original JavaScript. Still, they often help to simplify the code flow, which is an advantage for a subsequent dynamic analysis. The following image illustrates the transformations from plain to obfuscated and finally deobfuscated code.

The plain code consists of 8 lines. The obfuscated version is much larger and very hard to read due to hexadecimal variable names and no spacing. The deobfuscated version is even larger, as line breaks and indents were added again.
Transformation from plain to obfuscated and deobfuscated code

Hands-On: Analyze obfuscated code

After having covered the topics static analysis, dynamic analysis and obfuscation of JavaScript, I will go through the dynamic analysis of obfuscated JavaScript using an example application. In each step, I will explain my approach, procedure and conclusions.

Before diving into the tools for dynamic analysis, I want to lose a few words about the appropriate mindset. Dynamic analysis can be a painstaking process. Sometimes it helps to recall that whatever client-side function you’re searching for definitely IS present in the client-side code. As an example: If a web application signs data before sending it to the server, the signing key MUST be in the client-side code. This attitude helps you to try harder.

Example Application

The application I am going to analyze is the ping service from above but with obfuscated JavaScript. The user submits a host and receives the ping response of the server. The interception of the request in Burp Suite shows that the application sends a JWT, containing the host (IP) of the target server.

Screenshot of Burp Suite’s proxy history, showing a request with a JWT in the body. The Inspector of Burp decodes the payload automatically and shows that the payload contains “host”:“127.0.0.1”
Request being sent in Burp Suite

As JWTs are signed, it is not possible to manipulate the host value without invalidating the signature. This prevents me from manipulating the request within Burp Suite, which is essential for a pentest. But as the JWT is calculated with JavaScript, the key must be somewhere in the client-side code. The aim is to extract the secret key to perform a proper analysis of the endpoint.

Finding the Entry Point

First, I want to find out what happens when the Submit button is clicked. For that, I visit the Elements tab of developer tools and check for event listeners. This reveals a click event listener.

Screenshot of the developer tools showing an event listener with a link to secure-low.min.js
Event listener in Chromium’s developer tools

A click on the link opens the Sources tab and highlights the position of the listener function. Note how Chromium’s developer tools beautified the obfuscated code, which actually is written inside a single line.

Screenshot of the developer tools, showing the JavaScript code and highlighting the line of the listener function, which is called submit.
Beautified minified JavaScript with the highlighted submit function

I put a breakpoint on the first statement within the submit function and click the Submit button. The breakpoint triggers and pauses the execution. This means, I found the correct position and can now go through the execution step by step.

Screenshot of the developer tools, where the JavaScript execution paused on the breakpoint.
Paused execution on the breakpoint

Locating the Value of Interest

At some time, the ready-to-send JWT must be stored inside a JavaScript variable. I use the Step over next function call button of the developer tools to find this position. The highlighted lines below contain the JWT header, payload and finally the encoded JWT, starting with the characteristic string ey[...]

The “Step over next function call” button is on the top right of the developer tools.
Locating the calculation of the JWT

The signing must happen somewhere in the line in which the JWT is calculated. I put a breakpoint in that line and delete the old one to skip unnecessary statements during the analysis. Afterwards, I click the Submit button again.

The developer tools display the contents of variables next to the JavaScript statements using that variables. The JWT starts with the characters “ey”.
New breakpoint on calculation of JWT

The Unpacking Function

Now, the tricky part begins. I use the Step button to proceed through the execution. This leads to the unpack function of the obfuscated code, where the JavaScript symbols and strings are loaded from the array.

There is a long array of strings inside the obfuscated code. These strings contain identifiers that are known from JavaScript, like “getElementById”.
Function 0x246a() contains the packed array

The entire array is referenced by a variable now. This can be observed from the Scope section of the developer tools on the right.

The Scope section on the right shows an array with 48 entries.
The packed array observable from the Scope section

This step shows that the 20th entry of the Array is going to be returned.

A variable in the function call resolves to 20 and is used as index for the above array.
The unpacking function doing its work

Indeed the value JWS is returned, which had the index 20.

The return value is “JWS” as shown by the JavaScript debugger.
Returned variable 0x51f223 contains the value JWS

Reconstructing the Signing Call

Using the same method iteratively, I observe that the next unpacked values from the array are:

  • sign
  • HS256
  • 6465623837323564656533323462383134656535386133626434353431373866Screenshot containing the long number from above.Last step of unpacking before JWT calculation

This lets me reconstruct the call that is used to calculate the JWT: The structure of the line that calculates the JWT is KJUR['jws'][a][b](c,d,e,f). The variables abc and f are the unpacked values from above. The variables d and e are the header and payload of the JWT. Putting all together and replacing array- with dot-notation leads to the following function call:

KJUR.jws.JWS.sign("HS256", {header}, {payload}, "6465[...]3866")

The unpacked values from above are concatenated either by array notation [] or as a function call ().
Origins of the obfuscated variables and function calls

Proceeding to the next step validates this. The scope shows which variables have been passed to the function.

The next step in the execution is the calculation of the signature. The function has the name KJUR.jws.JWS.sign and the passed parameters are observable in the Scope window again.
The scope of the KJUR.jws.JWS.sign() function

At this point, the simplest method is to research whether this function comes from a JavaScript library and what JWS.sign exactly is doing. A quick Google search shows that indeed, a library is being used. It is called jsrsasign by the GitHub user kjur.

The documentation shows that the method generates a JWS signature with the specified key. The key, at position 4 of the function call, is a hexadecimal value in this case. This means that the long integer from above is the key.

Screenshot of the docs. The function KJUR.jws.JWS.sign generates JWS signatures by the specified key. If the key consists only of characters from 0-9, a-f or A-F, it is treated as hexadecimal.
Documentation of jsrsasign

Decoding the Key

I decode the hexadecimal key and encode the result to base64 as this is what I need for the Burp Suite extension JWT Editor.

Code showing how to decode hex: echo “646562…” | xxd -r -p; Afterwards the result is encoded to base64 with echo -n “deb872…” | base64;
Using xxd and base64 for decoding and encoding

Signing Manipulated JWTs

In the JWT Editor extension, I create a new symmetric key, as HS256 is a symmetric signing method. I paste the key from the previous step into the "k" field.

Screenshot from JWT Editor Keys extension visualizing the above steps.
Creating a new symmetric key in JWT Editor

I send a request from Burp Suite’s proxy history to the Repeater and switch to the JSON Web Token tab. Here, I replace the host 127.0.0.1 with infosec.exchange and click the Sign button.

Screenshot from the Repeater’s JSON Web Token tab highlighting the updated host name and the buttons to be clicked.
Using the key to sign the manipulated request

I send the request with the updated signature to the server and receive the ping output for infosec.exchange. This means that the signature calculation succeeded and I can now manipulate the value as needed.

Screenshot showing the updated signature and the resulting output from ping after sending the request.
Ping output for infosec.exchange in the server’s response

Using the Burp Suite extension CSTC, I can now automate the process of signing and perform an active scan on the target, which detects the command injection vulnerability. But this is content for another blog post.

Local Overrides

Local overrides are a way to keep changes to JavaScript across page loads. In certain situations, this may be helpful for the analysis.

Typical use cases are:

  • Changing the code flow to bypass client-side protections
  • Adding console.log statements to log variable contents
  • Refactoring code for beautification and deobfuscation

Temporary Changes in Developer Tools

Chromium’s developer tools allow you to change the content of sources right in the Sources tab. I’ll use the ping service from above for demonstration purposes. Lines 5 and 6 implement a client-side filter that removes special characters from the host value.

The lines contain a regular expression that removes all characters but a-z, 0-9, period, dash and underscore.
Client-side filter highlighted in the Sources tab

To bypass the filter, I commented out both lines and saved the changes with Ctrl+S. The warning sign in the top bar states that these changes are not persistent.

The screenshot shows the commented-out lines and the mentioned warning of the developer tools.
Temporary code changes

As you can see, the filter was successfully bypassed and allows command injection from the web application itself.

As the filter was removed, the command injection payload 127.0.0.1;id can be sent from the input field.
Disabled filter allows command injection

However, as soon as the page is reloaded the changes are gone. Real-world web applications often reload pages when navigating through them. To pentest the application efficiently, it is useful to make changes to JavaScript persistent.

Persistent Changes in Developer Tools

Chromium and Chrome have the so-called Local Overrides feature for this purpose. This lets you override page assets with files from a local folder. Configure it via the Overrides tab within the Sources tab.

Screenshot showing the Local Overrides tab
The Overrides tab within the Sources tab

After allowing Chromium access to the local folder, you can save sources via the item Save for overrides in the context menu.

Screenshot of the context menu to save sources for overrides.
Save file for overrides

This will add a folder for the path within the overrides folder and save the source to it. Changes to this file are now persistent as the purple dot indicates.

When you hover the file name it states the path to which the file is linked.
Persistent code changes

Persistent Changes in Burp Suite

Sadly, this feature is not usable from within Burp Suite’s embedded browser, as the Overrides tab stays empty. The reason for this seems to be that the browser is launched with the --disable-file-system flag. A feature request has been submitted 2.5 years ago.

But fortunately, there is a BApp for that! The extension HTTP Mock lets you define responses that will be returned instead of the real ones. It works like this:

  1. Send the request-response pair to the extension.Screenshot from Burp Suite showing how to send a request to the HTTP Mock extension: Right click, Extensions, HTTP Mock, Mock HTTP response.Context menu for request editor to mock an HTTP response
  2. Make your changes and don’t forget to press the Save button.In the HTTP Mock tab, I again commented out the lines with the regular expression. The save button is at the bottom of the window.Burp Suite’s HTTP Mock extension
  3. Reload the page in the embedded browser and you can see that the changes have been adopted.Screenshot of the developer tools, showing the changed contents of the JavaScript file.Persistent code changes delivered to browser

Bypass code protection

JavaScript obfuscators offer features to protect obfuscated code from deobfuscation and analysis. As an example, obfuscator.io has the following protection measures:

  • Self-defending: Breaks code when beautified or deobfuscated
  • Debug protection: Prevents the use of the debugger statement
  • Disable console output

This section shows you how to bypass all of these measures and provides tips on how to do so with other obfuscators.

Setup

One after another, I will apply protection measures to the default options of obfuscator.io and analyze the result. The screenshot below shows the options for obfuscation.

Screenshot of obfuscator.io highlighting the options preset “Default” and the three protection measures.
Options of obfuscator.io

My demo application uses JavaScript to write input of a text field to the page like this:

function process() {
	let input = document.getElementById("input").value;
	document.getElementById("output").innerHTML = input;
}

In each step, I will deobfuscate the obfuscated script using deobfuscate.io. Note that I am making changes to the JavaScript code directly in the source files. In a real-world example, you would probably use local overrides for this, as explained above.

Self Defending

This option makes the output code resilient against formatting and variable renaming. If one tries to use a JavaScript beautifier on the obfuscated code, the code won’t work anymore, making it harder to understand and modify it.

— https://obfuscator.io

When visiting the page in Chromium, it freezes, giving me no way to analyze what’s happening at all.

Screenshot of Chromium with a popup stating that the page is unresponsive.
Chromium page is unresponsive

Next attempt: Firefox

The result is different and more promising: The error message Uncaught InternalError: too much recursion and a stack trace that reveals the cause of the problem are displayed.

Screenshot of Firefox. The console states the above error message.
Error message in Firefox’s JavaScript console

Line 41 contains the regular expression (((.+)+)+)+$, which exhibits catastrophic backtracking.

Line 41 of the obfuscated source code. The string “(((.+)+)+)+$” sticks out between the obfuscated hexadecimal variable names.
Obfuscated code with a regular expression that sticks out

As you can see, the function _0x3b1622 is called in line 43, before the actual JavaScript code of the web page, which starts in line 44. Thus, the bypass is as simple as commenting out line 43 (or line 41 respectively). As a result, the page is loading in Chromium and is still functional.

Screenshot of Chromium loading the page. The developer tools are opened and Line 43 is highlighted, which calls the function with the catastrophic backtracking.
Obfuscated code with a regular expression that sticks out

This approach is generalizable to any self-defending code of obfuscator.io. The template of the self-defending function is defined here. The regular expression is hard coded into it.

The base code of the self-defending function of obfuscator.io.
Template of self-defending code from obfuscator.io

As a result, the bypass can be as simple as removing this certain regular expression, for example with sed:

sed -i 's/(((.+)+)+)+\$//g' file.js

Debug protection

This option makes it almost impossible to use the debugger function of the Developer Tools.

— https://obfuscator.io

Visiting the page in Chromium while the developer tools are opened, immediately pauses the execution and opens the debugger. Also, one seems to be stuck in an anonymous function that is calling the debugger again and again. With closed developer tools, everything works as usual.

Screenshot of Chromium calling the debugger in an endless loop. This opens tabs VM231, VM233, … in the developer tools.
Debug protection opens debugger in anonymous functions

The easiest way to get rid of this is to prevent the call of the anonymous function you’re stuck in. For this, I recommend using the Call Stack of the developer tools. Click through the particular calls until you find one that seems easy to comment out without breaking something. The further you proceed through the stack, the more likely it is that the function is relevant for the web page to work. Thus, stay as close to the top as possible.

Screenshot of Chromium developer tools, highlighting the call stack on the right of the screen.
The call stack yielding valuable information

In this case, I decided to delete the else statement in line 54 as it contains the call _0x3c9b48(0), which eventually led to endless calls of the debugger. And indeed, the page is now loading in Chromium without starting the debugger.

Screenshot of the demo application showing that the debug protection was bypassed because the debugger is no longer blocked.
Disabled debug protection by removing the else statement

Disable console output

Disables the use of console.logconsole.infoconsole.errorconsole.warnconsole.debugconsole.exception and console.trace by replacing them with empty functions. This makes the use of the debugger harder.

— https://obfuscator.io

For demonstration purposes, I added a console.log statement into the process function that should print the value of the input field to the console. However, the console output stays empty due to the disabled console output of obfuscator.io.

Screenshot of the obfuscated code of the application but between reading the input and writing it to the DOM I have added a console.log() statement.
console.log statement added to the obfuscated code

There are multiple ways to bypass this measure.

  1. Remove the symbol console from the packed array. This works because the obfuscated code needs to override the console object, for which it needs its name. Still, the packed array could be obfuscated in a way such that this is not simply possible.Screenshot of the packed array, containing all JavaScript symbols of the application, like innerHTML and getElementById. The symbol “console” is highlighted.The packed array contains the symbol console
  2. The template of the function to disable console output is here. It contains the name of the console functions to be overridden: logwarninfoerrorexceptiontabletrace.Screenshot of the source code. The line with the functions is highlighted.Methods to be overridden from obfuscator.ioLocate and remove this array from the obfuscated code. Depending on the degree of obfuscation, it may not be easily identifiable.The packed array within the obfuscated code is identifiable as it contains the entries “info”, “error”, “exception” among some other obfuscated entries.The obfuscated array of methods to be overridden
  3. Use the console function from an embedded iframe. This is by far the most reliable method. It works by appending an iframe to the current document via JavaScript. This iframe has its own internal DOM, which can be accessed via its contentWindow property. The following code shows this:var iframe = document.createElement("iframe"); document.body.appendChild(iframe); iframe.contentWindow.console.log("This is logged!") I added this code to the obfuscated code.The above code added to the obfuscated codeThe above code added to the obfuscated codeOf course, you can also override the console method of the document with the one from the iframe like this:console = iframe.contentWindow.console; The following screenshot shows that console output is working again.Screenshot of the demo application showing that the console printed the value “Bypassed 🥷”Re-enabled console output

General advice

Depending on the degree of obfuscation and the obfuscator used, the above techniques might not be applicable. If possible, you should always try to find out which obfuscator has been usedMinimal examples as above give a great idea of how the obfuscation works and can be generalized to larger code bases.

Always try to run and analyze JavaScript in different browsers or JavaScript engines. As seen above, the results might slightly differ. In this case, Chromium froze and Firefox threw the error with the essential piece of information: The line number with the recursion. You can also use services like jsconsole.com.

Recall that whatever client-side protection is in place can also be removed. In the end, that’s the fun thing about JavaScript hacking 😏

Source : https://kpwn.de/2023/05/javascript-analysis-for-pentesters/?utm_source=substack&utm_medium=email