All articles

JavaScript Deobfuscation — Reading Obfuscated Code and Decoding Payloads (HTB CWES)

Built from the JavaScript Deobfuscation module of the HTB Academy Web Penetration Tester path (HTB CWES). Revision notes: the workflow and every command kept, condensed. Labs and lessons are on HTB Academy.

Obfuscated JavaScript shows up everywhere — malware droppers, client-side "security", hidden functionality. The job: find it, make it readable, understand it, and replicate what it does. It's a small module but a pure skills one.

1. Finding the source

All front-end code ships to the client, so you can always read it. HTML is structure, CSS is style, JavaScript is behaviour — each can be internal (inline) or external (linked):

<style> … </style>                    <!-- internal CSS -->
<link rel="stylesheet" href="style.css">   <!-- external CSS -->
<script> … </script>                  <!-- internal JS -->
<script src="secret.js"></script>     <!-- external JS -->

View the page source with Ctrl+U; always read HTML comments (devs leave credentials and notes there). Click through to linked .js files. When a script looks like line-noise, that's obfuscation.

2. Recognising obfuscation

Obfuscation keeps code working but makes it unreadable (often slower). It's used to protect IP, to (wrongly) hide client-side "auth/encryption", and — most commonly — to evade IDS/AV in malware. Because JavaScript runs client-side in cleartext, it's the prime target. Forms you'll meet:

Technique Signature
Minification Whole script on one line (.min.js). Readable-ish, just compressed.
Packing eval(function(p,a,c,k,e,d){…}) — the six-arg function is the tell. Strings often still visible.
obfuscator.io var _0x1ec6=[…] arrays, hex names, optional Base64 string encoding — strings hidden.
JSFuck Only []()!+ characters. Runs, but very slow.
JJEncode / AAEncode Symbol soup; slow — used to bypass filters.

3. Deobfuscating

  1. Beautify (undo minification): browser debugger — Firefox Ctrl+Shift+Z, open the script, click { } Pretty Print — or Prettier / Beautifier.
  2. Deobfuscate (undo packing/encoding): beautifying isn't enough for packed code. Use a tool like UnPacker (matthewfl). For packed code, you can also grab the return value and console.log it instead of eval-ing it.
  3. Reverse engineer by hand when custom obfuscation defeats the tools.

A packed snippet unpacks to readable logic like:

function generateSerial() {
  var xhr = new XMLHttpRequest;
  var url = "/serial.php";
  xhr.open("POST", url, true);
  xhr.send(null);
}

4. Analysing and replicating

Read it line by line; Google unfamiliar APIs (XMLHttpRequest = JS web requests, xhr.open/xhr.send = build/send an HTTP request). The function above just POSTs to /serial.php with no data. Unused/hidden functionality like this is worth poking — unreleased features tend to be buggy. Replicate it with cURL:

curl http://TARGET/                       # read the page
curl -s http://TARGET/ -X POST            # empty POST (-s = quiet)
curl -s http://TARGET/serial.php -X POST -d "param1=sample"   # POST with data

5. Decoding encoded strings

Obfuscated code hides data in encoded blobs decoded at runtime. The three you must recognise and reverse:

Base64 — alphanumeric + + /, length a multiple of 4, = padding is the giveaway:

echo "text" | base64            # encode
echo "dGV4dAo=" | base64 -d     # decode

Hex — only 0-9 a-f; each char → its ASCII hex (a=61…). (man ascii for the table.)

echo "text" | xxd -p            # encode
echo "74657874" | xxd -p -r     # decode

ROT13 / Caesar — letters shifted (13 by default); http://www → uggc://jjj, still patterned:

echo "text" | tr 'A-Za-z' 'N-ZA-Mn-za-m'   # encode AND decode (symmetric)

Unsure which encoding? Feed it to a Cipher Identifier (boxentriq). Note: encryption (encoding with a key) can't be reversed without the key — if the key isn't in the script, the tools stop here.

6. What to carry into the CWES exam

  • Ctrl+U first, read comments, follow <script src>. The lead is often sitting in the source.
  • Name the obfuscation by its signature — (p,a,c,k,e,d) = packer, _0x… arrays = obfuscator.io, []()!+ = JSFuck — then pick beautify → deobfuscate → UnPacker.
  • Translate JS into HTTP. Most interesting obfuscated code ends in an XMLHttpRequest/fetch; replicate it with curl -X … -d … to hit the endpoint directly.
  • Spot and reverse base64/hex/rot13 on sight — padding =, 0-9a-f, shifted-but-patterned text.

Cheatsheet — JavaScript Deobfuscation

Find & beautify

Ctrl+U                     view page source (read HTML comments!)
<script src="x.js">        locate external JS
Firefox Ctrl+Shift+Z       debugger → click script → { } Pretty Print
Prettier / Beautifier      online beautify
UnPacker (matthewfl)       deobfuscate packed code

Obfuscation signatures

.min.js                          minified (one line)
eval(function(p,a,c,k,e,d){…})   packer
var _0x1ec6=[…]                  obfuscator.io
only []()!+                      JSFuck        |  symbol soup = JJEncode/AAEncode

Replicate with cURL

curl http://TARGET/
curl -s http://TARGET/ -X POST
curl -s http://TARGET/endpoint.php -X POST -d "param1=sample"

Encode / decode

echo "text" | base64            ;  echo "…=" | base64 -d          # base64
echo "text" | xxd -p            ;  echo "…"  | xxd -p -r          # hex   (man ascii)
echo "text" | tr 'A-Za-z' 'N-ZA-Mn-za-m'                          # rot13 (symmetric)
# identify unknown encodings: boxentriq Cipher Identifier

Built from HTB Academy's JavaScript Deobfuscation module — labs, lessons and the CWES exam are on HTB Academy.