İçeriğe geç
Sedat Özdemir
Yazılar

cybersecurity

__proto__ Deyip Geçme, Bütün Krallığı Kaybedebilirsin: Prototype Pollution'ın Gerçek Yüzü

Node.js dünyasının o meşhur ama sinsi açığı Prototype Pollution'ı, uykusuz bir gecede yaşadığımız gerçek bir kriz üzerinden inceliyoruz. Sadece veri mi sızıyor, yoksa sistemin anahtarlarını mı teslim ediyoruz?

Sedat Özdemir
· 3 dk okuma

Saat sabah 03:22'ydi. Ekranımdaki Grafana dashboard'u, testCompany bünyesindeki ana API gateway'in CPU kullanımının %98'e vurduğunu ve bellek tüketiminin bir 'memory leak' varmışçasına tırmandığını gösteriyordu. İlk başta klasik bir DDoS saldırısı sandık ama trafik paketlerini incelemeye başladığımda tuhaf bir şey fark ettim. Gelen istekler normal görünüyordu, ancak her JSON payload'u içerisinde sinsi bir anahtar saklıydı: __proto__. O an anladım ki, sadece bir servisimiz çökmüyor; bütün bir JavaScript objeleri hiyerarşisi zehirleniyordu.

Olayın Kalbi: JavaScript'in Genetiğiyle Oynamak

JavaScript'te her şey bir objedir, biliyorsun. Ve her objenin, özelliklerini miras aldığı bir 'prototype'ı (prototipi) vardır. İşte bu esneklik, bazen bizim en büyük kabusumuz olabiliyor. Prototype Pollution, bir saldırganın __proto__ veya constructor.prototype gibi özellikleri kullanarak, uygulama içindeki global Object yapısını değiştirmesiyle ortaya çıkar.

Bir düşünsene; senin uygulaman bir kullanıcıyı doğrulamak için user.isAdmin kontrolü yapıyor. Eğer saldırgan global Object prototipine isAdmin: true özelliğini enjekte edebilirse, sistemde o an oluşturulan ve isAdmin özelliği tanımlanmamış her obje otomatik olarak true değerini döndürmeye başlar. Bir anda herkes admin, bir anda herkes kral!

Krizin Kaynağı: Masum Görünen Merge Fonksiyonu

O gece yaşadığımız sorunun kökünde, kütüphanelerden birinde kullanılan klasik bir 'recursive merge' (özyinelemeli birleştirme) fonksiyonu yatıyordu. Geliştirici dostum, iki objeyi birleştirmek için şöyle bir kod yazmıştı (basitleştirilmiş halini bırakıyorum buraya):

function merge(target, source) {
    for (let key in source) {
        if (typeof target[key] === 'object' && typeof source[key] === 'object') {
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

Kağıt üzerinde harika duruyor değil mi? Ama saldırgan şöyle bir JSON gönderdiğinde işler çığırından çıkıyor:

{
    "auth": {
        "__proto__": {
            "polluted": "Yes, I am!",
            "isAdmin": true
        }
    }
}

Bu payload merge fonksiyonuna girdiğinde, target["__proto__"] ifadesi aslında global Object.prototype'a erişiyor. Ve bam! Artık bellekteki her yeni obje polluted özelliğine sahip. Bizim durumumuzda saldırgan, sistemin loglama mekanizmasını tetikleyen bir template engine (şablon motoru) özelliğini 'pollute' ederek RCE (Remote Code Execution) yolunu açmaya çalışıyordu.

RCE'ye Giden Yol: Sadece Yetki Yükseltme Değil!

Birçok kişi Prototype Pollution'ı sadece mantıksal bir açık sanıyor. 'Ne olacak canım, admin paneline girer en fazla' diyorlar. Keşke öyle olsa. Node.js dünyasında bu açık, genellikle child_process.spawn veya popüler template engine'ler (EJS, Pug, Handlebars gibi) ile birleştiğinde tam yetkili bir kod çalıştırma (RCE) zafiyetine dönüşüyor.

Örneğin, uygulaman arka planda bir shell komutu çalıştırıyorsa ve sen env değişkenlerini düzgün sanitize etmiyorsan, saldırgan prototipi zehirleyerek NODE_OPTIONS üzerinden kendi zararlı .js dosyasını sisteme dahil edebilir:

// Zararsızlaştırılmış saldırı örneği (Temsili)
// Saldırgan prototipi şöyle kirletiyor:
// Object.prototype.shell = "/bin/sh"
// Object.prototype.env = { "NODE_OPTIONS": "--require /tmp/evil.js" }

O gece biz, saldırganın testCompany sunucularında whoami komutunu çalıştırıp çıktısını example.com üzerinden dışarı sızdırmaya çalıştığını gördük. Şanslıydık ki, EDR (Endpoint Detection and Response) sistemlerimiz bu garip shell hareketliliğini yakaladı.

Peki, Kaleleri Nasıl Savunacağız?

O geceki yangını söndürdükten sonra ekipçe oturup 'Bunu bir daha nasıl yaşamayız?' diye sorduk. Çözüm sadece yamalarla değil, kodlama alışkanlıklarımızı değiştirmekle ilgiliydi.

  1. Girdileri Asla Ama Asla Küçümseme: Kullanıcıdan gelen JSON verisi, zehirli bir sarmaşık gibidir. Şema doğrulaması (Joi, Zod gibi kütüphanelerle) zorunlu olmalı. __proto__ veya constructor gibi anahtarları içeren objeleri kapıdan bile sokmamalısın.

  2. Map Kullan, Obje Değil: Eğer sadece anahtar-değer çiftlerini saklayacaksan, Object yerine Map kullan. Map prototip saldırılarına karşı doğal olarak dirençlidir.

  3. Prototipsiz Objeler Oluştur: Eğer bir obje oluşturman gerekiyorsa ve bunun 'temiz' kalmasını istiyorsan şunu kullan:

const cleanObj = Object.create(null);
// Bu objenin __proto__'su yoktur, zehirlenemez.
  1. Object.freeze() Kullanımı: Global prototipleri dondurmak radikal bir çözüm olabilir ama bazen hayat kurtarır. Ancak dikkat et, bazı kütüphaneler buna çok kızabilir.
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);

Red Teamer Gözüyle Son Not

Prototype Pollution, bulunması bazen zor ama etkisi yıkıcı bir açık. Bir sızma testinde veya 'bug bounty' avında, uygulamanın 'deep merge' yaptığı her noktayı kurcalarım. Özellikle Node.js ekosistemindeysen, kullandığın kütüphanelerin (lodash'ın eski versiyonları gibi) bu konuda sabıkalı olup olmadığını kontrol etmen şart.

Unutma, siber güvenlikte 'çalışıyor işte' dediğin o masum fonksiyon, gece yarısı uykunu kaçıran o alarmın sebebi olabilir. Kodun her satırına şüpheyle bakmak seni yorar ama sistemini ayakta tutar.

Bir sonraki kriz anına kadar, güvenli kod yazmaya devam!

İlgili yazılar