89 lines
2.9 KiB
Python
Executable File
89 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Statically decode common JavaScript obfuscation layers.
|
|
|
|
Without executing anything, this resolves frequent static encodings: ``\\xHH`` and ``\\uHHHH``
|
|
escapes, ``String.fromCharCode(...)`` sequences, and standalone base64 string literals. It also
|
|
extracts URLs from the decoded text and defangs them. Intended as a first-pass static aid before
|
|
a sandbox (box-js) for dynamic layers.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import binascii
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
RE_HEX = re.compile(r"\\x([0-9a-fA-F]{2})")
|
|
RE_UNI = re.compile(r"\\u([0-9a-fA-F]{4})")
|
|
RE_FROMCHARCODE = re.compile(r"String\.fromCharCode\(([^)]*)\)", re.I)
|
|
RE_B64 = re.compile(r"['\"]([A-Za-z0-9+/]{24,}={0,2})['\"]")
|
|
RE_URL = re.compile(r"https?://[^\s'\"<>)]+", re.I)
|
|
|
|
|
|
def _decode_escapes(text: str) -> str:
|
|
text = RE_HEX.sub(lambda m: chr(int(m.group(1), 16)), text)
|
|
text = RE_UNI.sub(lambda m: chr(int(m.group(1), 16)), text)
|
|
return text
|
|
|
|
|
|
def _decode_fromcharcode(text: str) -> str:
|
|
def repl(m: re.Match) -> str:
|
|
parts = re.split(r"[,\s]+", m.group(1).strip())
|
|
chars = []
|
|
for p in parts:
|
|
p = p.strip()
|
|
try:
|
|
chars.append(chr(int(p, 0)))
|
|
except (ValueError, TypeError):
|
|
return m.group(0)
|
|
return '"' + "".join(chars) + '"'
|
|
return RE_FROMCHARCODE.sub(repl, text)
|
|
|
|
|
|
def _decode_b64_literals(text: str) -> list[str]:
|
|
out = []
|
|
for m in RE_B64.finditer(text):
|
|
blob = m.group(1)
|
|
try:
|
|
dec = base64.b64decode(blob).decode("latin-1")
|
|
if sum(c.isprintable() for c in dec) > len(dec) * 0.8:
|
|
out.append(dec)
|
|
except (binascii.Error, ValueError):
|
|
continue
|
|
return out
|
|
|
|
|
|
def _defang(url: str) -> str:
|
|
return url.replace("http", "hxxp").replace(".", "[.]")
|
|
|
|
|
|
def decode(source: str) -> dict:
|
|
stage1 = _decode_escapes(source)
|
|
stage2 = _decode_fromcharcode(stage1)
|
|
b64 = _decode_b64_literals(stage2)
|
|
urls = sorted(set(RE_URL.findall(stage2)) | {u for d in b64 for u in RE_URL.findall(d)})
|
|
return {
|
|
"decoded_preview": stage2[:1500],
|
|
"base64_decoded": b64[:20],
|
|
"urls": [_defang(u) for u in urls],
|
|
"note": "static decode only; use box-js sandbox for eval/Function layers",
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Statically decode obfuscated JS")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("decode")
|
|
p.add_argument("file")
|
|
args = parser.parse_args(argv)
|
|
source = Path(args.file).read_text(encoding="utf-8-sig", errors="replace")
|
|
print(json.dumps(decode(source), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|