Sudo'dan Kurtuluş: O Güvensiz Bash Scriptini Çöpe Atıp Rust ile Setuid Trapdoor Yazdım

Sistem mühendisliğinden gelip siber güvenlik alanına geçtiyseniz, çok iyi bildiğiniz o yorgunluk hissi vardır: Teoriden kopup pratik dertlerle uğraşmak.

Kısa süre önce, bilgisayarımı dış dünyadan izole etmek için kurduğum mimariyi The Machine: Harold Finch’in Siber Paranoyasını NixOS ile İnşa Etmek başlıklı yazımda anlatmıştım. O yazıda, izole tarayıcım ile sistemim arasında anlık bir köprü kurmak için “Sovereign Trapdoor” adını verdiğim bir bash scripti (dl-toggle.sh) kullandığımdan bahsetmiştim.

Buradaki asıl trajedi şuydu: Sistemimi korumak için gururla anlattığım o “tuzak kapısı” scripti, aslında evimdeki en büyük güvenlik açığıydı.

Sorun: O Bash Scriptini Neden Çöpe Attım?

Harold Finch yazısında da bahsettiğim gibi, Downloads klasörümü varsayılan olarak root kullanıcısına kilitliyor, sadece dosya indireceğim anlarda bu script ile kendi kullanıcıma açıyordum. Kod gayet basitti:

#!/usr/bin/env bash
REAL_DIR="$HOME/.local/share/SecureDownloads"
OWNER=$(stat -c "%U" "$REAL_DIR")

if [ "$OWNER" = "xmrah" ]; then
    sudo chown root:root "$REAL_DIR"
    chmod 755 "$REAL_DIR"
    notify-send "Kilitlendi"
else
    sudo chown xmrah:users "$REAL_DIR"
    chmod 755 "$REAL_DIR"
    notify-send "Açıldı"
fi

Dışarıdan bakınca pratik duruyor. Ama bir güvenlikçi gözüyle baktığınızda burası resmen mayın tarlası:

  1. Path Injection: Bu script çalışırken $PATH değişkeni manipüle edilmişse, chown komutu yerine zararlı bir binary çalıştırılabilir. Üşenmeyip scriptin içine doğrudan absolute path yazsaydım bu riski ucuza kapatabilirdim, ama tembellik etmişim.
  2. Sudo Bağımlılığı: Her çalıştırmada sudo tetikleniyor. Sudo kurallarındaki en ufak bir hata doğrudan privilege escalation demek.

Çözüm ve Yeni Riskler: Native Rust ve Kernel Çağrıları

Bu kırılgan scripti, dış komutlara bel bağlamadan doğrudan işletim sistemi API’sine dokunan bir Rust binary’sine dönüştürdüm. Sadece işin mantığını gösteren sadeleştirilmiş hali şu şekilde:

use anyhow::{Context, Result};
use nix::unistd::{chown, Uid, Gid};
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::Path;

fn main() -> Result<()> {
    // Not: Path ve UID değerleri kişisel aracım olduğu için hardcoded bırakıldı.
    // Production ortamında bunlar config dosyasından okunmalıdır.
    let real_dir = Path::new("/home/xmrah/.local/share/SecureDownloads");
    let current_uid = fs::metadata(real_dir).context("Kasa metadata okunamadı")?.uid();

    // Dış komut spawn etmiyoruz. Yetki setuid/capabilities ile geliyor.
    if current_uid == 1000 {
        // TOCTOU Riski
        // metadata okuma ve chown arasında başka bir process araya girebilir!
        chown(real_dir, Some(Uid::from_raw(0)), Some(Gid::from_raw(0)))
            .context("Yetki devredilemedi")?;
    } else {
        chown(real_dir, Some(Uid::from_raw(1000)), Some(Gid::from_raw(100)))
            .context("Yetki devredilemedi")?;
    }
    Ok(())
}

Bu koda geçerek Path Injection riskini sıfırladık ve sudo bağımlılığından kurtulduk. Her şey çözüldü mü? Tabii ki hayır. Kodda dürüstçe belirttiğim gibi, fs::metadata ile okuma yapıp ardından chown kullanmak ciddi bir TOCTOU zafiyeti yaratır. İşi kitaba göre yapmak isteseydim, O_PATH ile dosyayı openat üzerinden güvenle açıp, açık file descriptor üzerinden fchownat yapmam gerekirdi. Buradaki amacım o karmaşıklığa boğulmaktan ziyade çalışan sistemi daha güvenli bir temele oturtmaktı.

Dağıtım: NixOS ve Setuid Çatışması

Peki bu Rust binary’sini sisteme nasıl yerleştireceğiz? NixOS Flake’im içinde security.wrappers kullandım:

security.wrappers."dl-toggle" = {
  source = "${pkgs.dl-toggle}/bin/dl-toggle";
  owner = "root";
  group = "root";
  setuid = true; # Gerçekten iyi bir fikir mi?
};

Setuid root kullanmak, bu programın her zaman root yetkisiyle çalışmasını sağlar. Bu sayede sudo kullanmamıza gerek kalmaz. Ancak kendime şunu sormam gerek: Setuid root, sudo’dan gerçekten daha mı güvenli? Çoğu zaman hayır. Capabilities ile sadece CAP_CHOWN verirseniz, process root olmadan sadece dosya sahipliği değiştirebilir. Ancak Setuid root ise, uygulamanın içindeki en ufak bir bellek sızıntısında saldırgana full root yetkisini altın tepside sunar. Eğer bu aracı production’da bir sunucuya koysaydım, setuid yerine Linux Capabilities (CAP_CHOWN) kullanmak veya süreci Polkit üzerinden yönetmek çok daha doğru bir “Zero Trust” yaklaşımı olurdu.

Günah Çıkarma

Bash’ten Rust’a geçiş yapmak sihirli bir değnek değil. Yeni araçlar, yeni mimariler ve beraberinde yeni trade-off’lar getiriyor. Ancak OSCP grind’leri arasında NixOS’un immutable yapısını kullanmak ve işletim sistemi seviyesinde “gerçekten ne olduğunu” anlamak, sizi sadece bir sertifika avcısı olmaktan çıkarıp olaya hakim bir mühendise dönüştürüyor.

Siz de kendi ev laboratuvarınızdaki o “geçiştirilmiş” güvenlik önlemlerini masaya yatırın. Kusursuz olmak zorunda değil, ama üzerine düşünülmüş olmak zorunda. Farkı hissedeceksiniz.

EOF.