PHP2Shell (CVE-2026-109618 / CVE-2026-109619): 14 Yıllık PHP Zafiyeti

Read in English

Biliyorsunuz ki, 21 Ekim 2026’da PHP projesi, kimliği doğrulanmamış herhangi bir saldırganın sadece HTTP isteklerini kullanarak neredeyse tüm PHP sunucularında komut çalıştırmasını (RCE) sağlayan iki kritik zafiyeti açıkladı. Bu zafiyetin fena noktası şu: oturum açmaya ya da dosya yüklemeye bile gerek yok. Zafiyet doğrudan PHP runtime’ın, yani PHP motorunun içinde. İşin şaşırtıcı kısmı, PHP2Shell adı verilen bu zafiyet 2012’de çıkan PHP 5.4.0’dan beri kodda duruyormuş. Yani 14 yıllık sürüm geçmişinin tamamı etkileniyor.

Araştırmacılar bunu baya AI destekli bir şekilde bulmuşlar. Human in the loop ile başladık, human on the loop ile gidiyoruz. Az kaldı, artık human out of the loop olacağız bence. AI’lar saldırı yapacak, AI’lar savunacak, biz de evde cips yiyeceğiz. AI’ın manyaküstü gelişimini küçümsemek artık bana komik geliyor. Her neyse, bu konuda konuşasım varmış, başka bir yazıda bahsedeyim.

Gelelim konumuza: iki CVE aldı. Birisi CVE-2026-109618, diğeri CVE-2026-109619 ve ikisi de CVSS v4.0’a göre 9.2’lik puanla kritik olarak sınıflandırılıyor.

Detaylar resmi yamalardan önce sızdığı için yamalar çıktığında saldırganlar da hazırdı. Kimi sistemler saldırıya uğradı, kimi sistemler de önlem olarak pasife alındı. Japonya Başbakanı Sanae Takaichi bile X’ten acil patch çağrısı yapıp kendi sitesini kapattı. Siber güvenlik dünyasında canlı olarak takip edebildiğim en kaotik anlardan birisiydi.

Bu vesileyle zafiyeti daha iyi tanımak ve canlı olarak deneyimlemek için TryHackMe’nin PHP2Shell için hazırladığı odayı beraber çözeceğiz. Oda çok uzun olduğu için atladığım kısımlar olacak. Yani bu yazı flag’leri bulabileceğiniz bir walkthrough değil.

Kısaca ne oluyor? PHP, dosya yükleme isteğini uygulama kodun çalışmasından önce okuyor. Bellek bitince PHP’nin acil çıkış mekanizması temizliği atlıyor ve arkada bozuk bir adres kalıyor. Aynı süreçteki sonraki istek bu adresi kullanıyor. Saldırgan önce sunucunun belleğinden adres sızdırıyor, sonra bu bozukluğu komut çalıştırmaya çeviriyor. Kalıcı çözüm PHP’yi güncellemek. Hadi başlayalım.

Önce şu session.upload_progress işini anlayalım

Bir dosya yüklerken sayfada gördüğün yükleme çubuğunu düşün. Tarayıcı dosyayı gönderirken PHP, “ne kadar geldi” bilgisini kullanıcının oturumuna yazıyor. Buna session.upload_progress deniyor. PHP, dosya yükleme isteğinin (multipart/form-data) gövdesini daha okurken yüklemenin durumunu, dosya adını, boyutunu ve o ana kadar kaç byte geldiğini $_SESSION içine kaydediyor. Sonra ikinci bir istek bu bilgiyi okuyup ilerlemeyi sayfaya gösterebiliyor. Özellik varsayılan olarak açık. Devreye girmesi için formda adı session.upload_progress.name ayarıyla eşleşen bir alanın olması yeterli (varsayılan adı PHP_SESSION_UPLOAD_PROGRESS).

Buradaki kritik nokta bunun ne zaman olduğu. PHP, isteği uygulama kodunu çalıştırmadan çok önce okuyor. Yükleme bilgisi de tam bu sırada, daha tek satır uygulama kodu çalışmadan yazılıyor. Yani bu koda ulaşmak için hedef sayfada session_start() olmasına, giriş yapılmış bir oturuma, hatta ortada bir uygulama olmasına bile gerek yok. Sunucuda POST kabul eden herhangi bir URL yeter. İçindeki betik sadece echo "OK"; yazıyor olsa bile.

Upload progress kod çalışmadan önce devreye giriyor İşte bu “kod çalışmadan önceki pencere” (pre-execution window) saldırgan için bir dayanak noktası oluyor. PHP2Shell’in tamamı, uygulama daha mışıl mışıl uyurken, saldırganın sıradan bir dosya yükleme isteğine koyduğu verilerle bu pencerede gerçekleşiyor.

İşler nerede bozuluyor?

Buradaki asıl zafiyet yükleme ilerleme özelliğinin kendisi değil. Zafiyet, PHP bu işi yaparken belleği biterse ortaya çıkıyor.

PHP’nin bir isteğe ayırdığı bir bellek sınırı var: memory_limit. İstek bu sınırı aşınca PHP devam edemiyor ve motorun bailout dediği acil çıkışı kullanıyor. Normalde bir istek bitince PHP aldığı belleği geri verir ve o belleğe işaret eden adresi temizler. Ama bailout bu temizliği atlıyor. Yani düzenli bir kapanış değil, acil çıkış kapısı.

Zafiyet tamamen zamanlama meselesi. Bellek hatası tam “bellek geri verildi ama adres temizlenmedi” anına denk gelirse, geride silinmiş bir yeri gösteren bir adres kalıyor. Buna dangling pointer (sarkan işaretçi) deniyor. Bu tür hatalar CWE-824 olarak sınıflandırılıyor.

Memory bailout temizliği atlıyor, geriye dangling pointer kalıyor Ölü bir isteğin içinde kalan bozuk adres tek başına zararsız aslında, çünkü istek bitmiş. Tehlikeli yapan şey sunucunun çalışma modeli. Apache’nin içinde çalışan PHP kurulumunda (mod_php) tek bir worker süreci istekleri arka arkaya işliyor. Yani bozuk adres ölen istekle birlikte yok olmuyor; sürecin belleğinde kalıyor. Aynı worker’a düşen bir sonraki istek, PHP’nin hiç sıfırlamadığı bu adresi kullanıyor. Saldırgan o boşluğa ne yazılacağını kontrol ederse, adresin neyi gösterdiğini de kontrol ediyor demektir.

Bu tek hatadan araştırmacılar iki ayrı saldırı çıkarmış, iki CVE olmasının sebebi de bu. CVE-2026-109618 sunucunun belleğini okumaya yarıyor. CVE-2026-109619 ise bu bilgiyi kullanıp sunucuda komut çalıştırıyor. PHP’nin yaması da zaten “bailout sırasında durumun bozulmasını engelle” diyor. Şimdi sırasıyla hedefi kontrol edeceğiz, belleği sızdıracağız ve sonra komut çalıştıracağız.

Hedef makine

SSH ile makineye bağlandım. Victim, zafiyetli sürümlerden biri olan PHP 8.4.26’yı kullanıyor (fix: 8.4.27):

ubuntu@tryhackme-2404:~$ docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED      STATUS         PORTS                    NAMES
d77a04da7d60   php:8.4.26-apache   "docker-php-entrypoi…"   2 days ago   Up 4 minutes   127.0.0.1:8088->80/tcp   php2shell-target

Gördüğünüz gibi hedefte zafiyetli uygulama kodu bile yok, aşırı simple bir sayfa:

ubuntu@tryhackme-2404:~$ curl -s http://127.0.0.1:8088/index.php
OK
ubuntu@tryhackme-2404:~$ docker exec php2shell-target cat /var/www/html/index.php
<?php echo "OK\n"; ?>

Sunucu, varsayılan olarak açık gelen session.upload_progress ile standart bir Apache + mod_php kurulumu çalıştırıyor. Laboratuvar için bu yapı tek worker’a düşürülmüş ve memory_limit kısılmış. Yani aynı süreç bütün istekleri işliyor ve bellek hızlı bitiyor. Bu da bozuk adresin tekrar kullanılmasını kolaylaştırıyor.

CVE-2026-109618: bellekten adres sızdırmak

Modern sistemler kodu ve veriyi her çalıştırmada farklı adreslere koyuyor. Buna ASLR (adres rastgeleleştirme) deniyor. Saldırgan adresleri bilmezse komut çalıştırmak için doğru yeri bulamıyor. CVE-2026-109618 işte bu adresleri sızdırıyor.

Mekanizma yine aynı bellek hatasını kullanıyor. Bu sefer oturum kimliği (session id) işlenirken bellek bitiyor ve geride oturum kimliğine işaret eden bozuk bir adres kalıyor. Sonraki istek o boşluğu yeniden dolduruyor. Artık bozuk adres, uzunluğu saldırgan tarafından şişirilmiş bir string’i gösteriyor. PHP oturum çerezini (session cookie) gönderirken bu string’i okuyor ve Set-Cookie: PHPSESSID yanıt başlığına (header) koyuyor. Yani sunucu, hafızasındaki bir parçayı yanlışlıkla sana geri gönderiyor.

Exploit bunu iki kez yapıyor: ilkinde PHP’nin belleğinin, ikincisinde PHP’nin kendi kodunun nerede olduğunu öğreniyor. İkisi birleşince ASLR’nin koruması kalkıyor ve komut çalıştırmak için gereken adresler ortaya çıkıyor.

Exploit makinede ~/php2shell-bypass-aslr-exp altında hazır. Klasörün içi şöyle:

ubuntu@tryhackme-2404:~/php2shell-bypass-aslr-exp$ ls
ASLR_PATHS.md      ASLR_PURE_HTTP_RESEARCH.md  HTTP_HEAP_LEAK.md  NOTES.md     NO_DISPLAY_ERRORS.md  docker  exploit.py  logs  readme_en.md  tools
ASLR_PURE_HTTP.md  DISPLAY_ERRORS_OFF.md       NOASLR_REPORT.md   NOTES_BG.md  README.md             e.py    lab         out   src

Hiçbir argüman vermeden çalıştırıyorum:

ubuntu@tryhackme-2404:~/php2shell-bypass-aslr-exp$ python3 exploit.py -u http://127.0.0.1:8088/index.php
[*] target http://127.0.0.1:8088/index.php (cmd='id>/out/p1')
[*] offsets loaded from /home/ubuntu/php2shell-bypass-aslr-exp/out/offsets.env
[*] offsets: zif=0x465370 dtor=0x65eb60 htdata=0x690d8
[*] rotating worker over HTTP
[*] stage 1: heap leak (attempt 1/3)
[+] chunk base = 0x799e81200000
[*] rotating worker over HTTP
[*] stage 2: libphp leak (attempt 1/3)
[+] libphp base = 0x799e82eb3000
[*] rotating worker over HTTP
[*] stage 3: HTDATA=0x799e812690d8 ZIF=0x799e83318370
[+] W3 attempt 1
PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)

Çıktıdaki chunk base PHP’nin belleğinin, libphp base ise PHP kodunun adresi. PROOF satırı komutun sunucuda www-data kullanıcısı olarak çalıştığını gösteriyor. Yani exploit sadece adres sızdırmakla kalmamış, aynı çalışmada komut da çalıştırmış.

CVE-2026-109619: adreslerden komuta

Sızıntı saldırgana her şeyin nerede olduğunu söyledi. CVE-2026-109619 bu bilgiyi sunucuda çalışan bir komuta çeviriyor.

PHP bir değeri temizlerken, o değerin yanında duran bir fonksiyon adresini çağırabiliyor. Exploit, bozuk adresin gösterdiği yere sahte bir yapı koyuyor ve bu yapının “temizlik sırasında çağrılacak fonksiyon” (destructor) alanını system() (komut çalıştıran fonksiyon) yapıyor. PHP temizlik yaptığını sanırken saldırganın verdiği komutu çalıştırıyor. Komut web sunucusu kullanıcısı olarak çalışıyor. Ekstra bir kütüphane ya da PHP dışından hazır bir saldırı parçası gerekmiyor; PHP’nin kendi kodu yetiyor.

ubuntu@tryhackme-2404:~/php2shell-bypass-aslr-exp$ python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'cat /f>/out/p1' --proof-file out/p1
[*] target http://127.0.0.1:8088/index.php (cmd='cat /f>/out/p1')
[*] offsets loaded from /home/ubuntu/php2shell-bypass-aslr-exp/out/offsets.env
[*] offsets: zif=0x465370 dtor=0x65eb60 htdata=0x690d8
[*] rotating worker over HTTP
[*] stage 1: heap leak (attempt 1/3)
[+] chunk base = 0x799e81200000
[*] rotating worker over HTTP
[*] stage 2: libphp leak (attempt 1/3)
[+] libphp base = 0x799e82eb3000
[*] rotating worker over HTTP
[*] stage 3: HTDATA=0x799e812690d8 ZIF=0x799e83318370
[+] W3 attempt 1
PROOF: THM{....}

Bu şekilde argüman verip istediğimiz komutu çalıştırdık ve flag’i elde ettik.

Önlemler

PHP2Shell PHP’nin kendi içinde olduğu için “siteyi güncelleyeyim” refleksi burada işe yaramıyor. WordPress, Drupal, Joomla, Laravel güncellemek uygulamayı değiştirir; zafiyetli kod hepsinin altındaki PHP’de. Tek tam çözüm PHP’yi yamalamak: dağıtımınızın verdiği PHP paketini ya da container’ınızın temel imajını (base image) 8.5.12, 8.4.27, 8.3.36 veya 8.2.35’e çıkarıp bütün worker’ları yeniden başlatmak. Eski kodu çalıştırmaya devam eden tek bir process kalmasın.

Yama uygulanana kadar bazı geçici önlemler var, ama PHP ekibi bunların hiçbirinin yamanın yerini tutmadığını söylüyor. session.upload_progress.enabled = 0 yapıp worker’ları yeniden başlatmak saldırının tetikleyicisini kaldırıyor. PHP’nin kod önbelleğini kapatmak (opcache.enable = 0) kod çalıştırma yolunu zorlaştırıyor. PHP 8.5’te hata izlerini kapatmak (fatal_error_backtraces = 0) özellikle CVE-2026-109619’a karşı. Ağ kenarında dosya yükleme (multipart/form-data) isteklerini reddetmek teslimatı kapatıyor, ama meşru dosya yüklemelerini de kırıyor. PHP’yi en az yetkiyle çalıştırmak da zincir tutarsa saldırganın kazanacağını sınırlıyor.

Bir de işe yaramayan bir savunma var: PHP modülü olarak çalışan bir WAF (web uygulama güvenlik duvarı), trafiği ancak PHP kontrolü uygulamaya devrettikten sonra görebiliyor. Bu saldırı ise uygulama hiç çalışmadan önce işini bitiriyor. Yani PHP’nin içindeki filtre görme şansı bulamıyor. Koruma ya yamalı PHP’de ya da PHP’nin önündeki bir katmanda olmak zorunda.

Tespit için şu izlere bakabilirsiniz: Set-Cookie yanıtlarında anormal büyük PHPSESSID değerleri, içinde PHP_SESSION_UPLOAD_PROGRESS alanı taşıyan çok büyük ve çok alanlı POST istekleri, loglarda PHP worker çökmeleri ve bellek hataları. Bunlardan biri, hele birkaçı bir arada, yamalama sürerken alarm verilmesi gereken şeyler.

Daha fazla bilgi

Bu yazıdaki anlatımın bir kısmı TryHackMe’nin CVE PHP2Shell odasına ve Wiz Research’ün PHP2Shell araştırmasına dayanıyor. Daha fazla teknik detay için bu iki kaynağa bakabilirsiniz.

Sonuç

PHP2Shell sadece bu yıla değil, tarihe de damgasını vuran bir zafiyet oldu. Yazılımın sadece uygulamadan ibaret olmadığını bize tekrar hatırlatıyor. PHP motorunun bellek bitince nasıl davrandığına 2012’den beri kimse yeterince dikkat etmemiş belli ki. Framework olarak ne kullanırsanız kullanın, web’in yaklaşık yüzde 70’ini kapsayan bir motordan bahsediyoruz.

AI çağı her gün yeni gelişmelerle bizi şok ediyor. Zafiyet araştırmacıları AI sayesinde belki de hiç fark edilmeyecek açıkları buluyor. AI, kötü niyetli aktörlerin elinde ise çok tehlikeli bir silah oluyor. Ne yalan söyleyeyim, savunma tarafının işi çok zor. Saldırganın avantajı şu: tek bir yol bulması yeterli. Savunma ise her yeri güvenli tutmaktan mesul.

Bakalım gelecekte, belki de yarın, her gün kullandığımız uygulamalardan kim bilir daha ne zafiyetler çıkacak. Siber dünyada hiçbir zaman güvende değiliz, orası kesin ;)


Yazımı okuduğunuz için teşekkür ederim. Benim için de low level anlamında baya detay öğrendiğim keyifli bir yazı oldu.

Enes Bayraker

Hack The Planet