28 Ağustos 2016

Kali Linux - Bölüm-4: Shell Scripting

Shell scripting konusu bir Unix/Linux sistem yöneticisi için vazgeçilmez bir konu. Çünkü scriptler her defasında aynı komutları defalarca yazmaktan kurtardığı gibi aynı zamanda uygulama ortamına benzer bir ortam sunarak farklı durumlarda farklı işlemleri otomatik olarak gerçekleştirmeyi de mümkün kılıyorlar.
Aynı nedenlerden dolayı biz pentest uzmanları için de bazı test adımlarımızı otomatikleştirme imkanı sağlıyorlar. Bununla birlikte bir pentest aracı geliştirmek istediğinizde ilk tercihiniz genellikle python, ruby, perl gibi daha gelişmiş API’lerle desteklenen, hem esnek hem de kontrol edilebilir script dilleri olacaktır. Ancak ne var ki, Linux işletim sisteminin startup ve shutdown süreçleri ile pek çok servis yönetim script’i Shell Script olarak geliştirilmiştir. Dolayısıyla ciddi bir sistem yönetim ihtiyacınız varsa shell scripting’den kaçamazsınız.

Shell scripting konusu başlı başına derin bir konu, bu yüzden bu makale kapsamında mutlaka değinilmesi gereken temel konulara değineceğiz. Daha detaylı ihtiyaçlar için elbette internette bulunabilecek pek çok kaynaktan faydalanılabilir.

Örneğimiz için bir ihtiyaç senaryosu geliştirelim, böylece daha ilginç bir script oluşturabiliriz. Ayrıca örneğimizde Kali ile birlikte gelen güvenlik araçlarından bazılarını da kullanma imkanımız olur.

Diyelim ki; hedef tespiti adımında ICMP Echo Request, Timestamp Request ve Netmask Request istekleri ile tarama yapacak bir script geliştirelim. Bu istekleri üretmek üzere “hping3” aracını kullanalım. Bu araç ile söz konusu istekleri yapmak için kullanmamız gereken komut yapısı aşağıdaki gibi:

# hping3 172.16.0.10 –c 1 -1 –C 8

Yukarıdaki komutla “172.16.0.10” IP adresine 1 adet (-c 1) ICMP (-1) paketini Type 8 (-C 8) yani Echo isteği gönderiyoruz.
Script yazmak istememizin nedeni geniş bir IP bloğuna istekte bulunmak için bu komutu her defasında tekrar tekrar yazmak istemememiz. Ayrıca her IP adresi için Echo Request paketinin yanı sıra “Timestamp Request” ve “Netmask Request” isteklerini de göndererek tespit ihtimalimizi artırmaya çalışacağız. Elbette hedef tespiti için daha pek çok yöntem var, ama buradaki amacımız çok ihtiyaç duyduğumuz bir durumda nasıl script yazabileceğimizin üzerinden geçmek.

Uzatmadan hemen script kodumuzu açıklamaya başlayayım:

#!/bin/bash
echo "Lütfen taranacak subnet'in ilk 2 octet'ini giriniz ve [ENTER] tuşuna basınız: "
read SUBNET
function scan {
    local YANIT="0"; local IP=$1.$2.$3
    local ECHO="echo-0"; local TIMESTAMP="timestamp-0";local ADDRESSMASK="addressmask-0"
    hping3 $IP -c 1 -1 -C 8 >& aradosya.$IP # ECHO Request
    if grep "1 packets received" aradosya.$IP >& /dev/null; then
        if ! grep "Unreachable" aradosya.$IP >& /dev/null; then
            ECHO="echo-1"; YANIT="1"
        fi
    fi
    hping3 $IP -c 1 -1 -C 13 >& aradosya.$IP # TS Request
    if grep "1 packets received" aradosya.$IP >& /dev/null; then    
        if ! grep "Unreachable" aradosya.$IP >& /dev/null; then
            TIMESTAMP="timestamp-1"; YANIT="1"
        fi
    fi
    hping3 $IP -c 1 -1 -C 17 >& aradosya.$IP # ADD. MASK Request
    if grep "1 packets received" aradosya.$IP >& /dev/null; then
        if ! grep "Host Unreachable" aradosya.$IP >& /dev/null; then
            ADDRESSMASK="addressmask-1"; YANIT="1"
        fi
    fi
    if test "$YANIT" == "1" ; then
        echo $IP";"$ECHO";"$TIMESTAMP";"$ADDRESSMASK
    fi
    rm aradosya.$IP
}

for i in `seq 1 254`; do
    for j in `seq 1 254`; do
        scan $SUBNET $i $j
    done
done

exit 0

Script’in ilk satırında görülen “#!/bin/bash” (#! karakterleri shebang olarak okunur) ifadesi içinde bulunulan shell’den söz konusu script çalıştırılırken hangi interpreter’in kullanılacağını belirtmek için kullanılır. Bu ifadeyi kullanmazsanız içinde bulunduğunuz shell öntanımlı shell’i interpreter olarak kullanır.

echo "Lütfen taranacak subnet'in ilk 2 octet'ini giriniz ve [ENTER] tuşuna basınız: "
read SUBNET

Bundan sonra gelen bölümde kullanıcıdan bir girdi alıyoruz. “echo” komutu ile kullanıcıyı yönlendiriyoruz ve “read” komutu ile yanında yazdığımız değişkene bu değeri aktarıyoruz. Değişkeni daha önce declare etmemize gerek yok, hemen ilk kullanıldığı anda tanımlayabiliriz. Veri tipi ile ilgili de sıkı kurallar yok, ama veri tipi karşılaştırma noktalarında mutlaka önemli.

function scan {

Bu bölümden sonra bir fonksiyon tanımı geliyor. Fonksiyonumuzun adı “scan”. Uygulama dillerinde olduğu gibi shell script içinde yazılan fonksiyonlar da parametre alabiliyor ve bir değer döndürebiliyorlar. Ancak alınan parametrelerin sayısını ve veri tiplerini tanımlamak gerekmiyor. Fonksiyonlar diğer dillerde de olduğu gibi çok defa kullanılacak script bölümlerini tanımlamak ve script’in bakımını kolaylaştırmak için kullanılıyor. Ancak önemli bir kural, fonksiyonların kullanılmadan önce tanımlanmış olması. Interpreter script’in her bir satırını yorumladıktan sonra bu satırın gereğini yerine getirmeye çalışıyor, bu nedenle bir fonksiyonu çağırdığımızda bunu önceden tanımlamış olmamız lazım.
Fonksiyon içeriğini daha sonra açıklayacağım, şimdi script’in temel fonksiyonalitesinin tanımlandığı bölüme ilerleyelim:

for i in `seq 1 254`; do
    for j in `seq 1 254`; do
        scan $SUBNET $i $j
    done
done

Buradaki algoritmayı anlatmadan önce kullanıcıdan taranacak olan subnet’in ilk 2 octet’ini aldığımızı hatırlatalım. Elbette daha iyi bir kod kullanıcının yapabileceği tüm hataları hesaba katmalı ve bunları ele almalı, ancak kodu kalabalıklaştıracağı için bu kontrolleri kodlamaktan kaçındım.
İki adet “for” döngüsü ile taranacak her bir IP adresinin son 2 octet’ini de biz oluşturacağız. “for” ifadesi “i” değişkenine her defasında hemen sonra gelen diziden bir değer atayarak tüm dizi değerlerini kullanmamızı sağlayacak. Shell’in “command substitution” imkanına daha sonra değineceğimizi söylemiştik. Değineceğimiz alan burası. Normalde pek kullanmadığımız ve tırnak işaretine benzeyen olan ters kesme (back quote) işareti sayesinde shell ters kesme işaretleri arasında bulunan komutu çalıştırıyor ve bu komutun çıktısını içinde bulunduğu satırda üreterek kullanıyor. Tabi interpreter bunu yaparken eğer bir command substitution durumu varsa önce bu işlemi gerçekleştirmeli, daha sonra bu komutun ürettiği çıktıyı da dikkate alarak satırın tamamını yorumlamalı.

Bizim ihtiyacımız 1 ile 254 arasındaki sayılardan oluşan bir dizi oluşturmak ki “seq” komutu da tam olarak bu işlemi yapıyor. “seq” komutunun normalde nasıl çalıştığını şu şekilde görebiliriz:


Bu örnekte 1 ve 5 arasındaki değerleri ürettik. Biliyorsunuz pek çok dilde bir satırın bir komutun bittiğini ifade etmek için “;” işareti kullanılır. Script yazarken buna gerek yok, her satır sonu o komutun bittiği anlamına gelir. Ancak sıralı (batch) komut çalıştırma bölümünde de bahsettiğimiz gibi tek satırda birden fazla komutu da aralarına “;” işaretini koyarak script dosyası içinde de aynı satırda birden fazla komutu yazabiliriz. “for” döngüsünün sonunda gördüğünüz “do” kelimesi normalde farklı bir komut ve ayrı bir satırda yazılmalıydı. Fakat shell’in bize sağladığı batch komut çalıştırma imkanı sayesinde ve benim kodun okunabilirliğini satır sayısını azaltarak artırma niyetim nedeniyle “for” ifadesi ve “do” ifadesini aralarına “;” koyarak aynı satırda yazdım.

İç içe 2 “for” döngüsü ile IP adresinin son 2 octet’ini de oluşturuyoruz demiştik. Her bir IP adresi için “scan” fonksiyonunu çağırıyoruz. “scan” fonksiyonuna 3 parametre veriyoruz.

scan $SUBNET $i $j

Bunlardan ilki “SUBNET” değişkeni, yani kullanıcıdan girmesini talep ettiğimiz ilk 2 octet. Değişkenin başındaki “$” işareti yine shell ortamının sağladığı değişken yerine geçme (variable substitution) imkanını bize sağlıyor. Yani shell interpreter’i çalışırken satırdaki komutu işletmeden önce başında “$” işaretini gördüğü değişkenin değerini satırda bu değişkenin adının geçtiği yere yerleştiriyor, ondan sonra komutu bütün olarak çalıştırıyor. Yukarıda bahsettiğimiz command substitution fonksiyonalitesine çok benzer bir durum.

Aynı şey “i” ve “j” değişkenleri için de geçerli, içinde bulunduğumuz “for” döngüleri içinde bu değişkenlerin aldığı değerler her “scan” fonksiyonu çağırılacağında satırda variable substitution sayesinde kullanılıyor.

exit 0

Son satırdaki bu komut script’in return kodunun “0” olmasını sağlayarak shell’in script’i çalıştırırken başlattığı (spawn ettiği) prosesi sonlandırıyor. Bu komuta gerek yok aslında, ama bir uygulamayı veya scripti çalıştırdığınızda genellikle “0” başarılı, bunun dışındaki tüm tamsayı (integer) dönüş değerler ise bir sorun olduğu anlamına gelir. Bu dönüş (return) kodunu kontrol eden ve bu uygulamayı çağıran kod da bu şekilde ters giden birşey olduğuna karar verebilir. Örneğin biz iyi bir programcı olarak kullanıcının verdiği girdileri kontrol edecek kodları yazsaydık, hatalı bir giriş yapıldığında “exit” komutu ile script’i sonlandırırken aynı zaman da uygulama, script veya hatta shell’in değerlendirmesi için “0” dan farklı bir hata kodu dönebilirdik.

Shell’de bir komut çalıştıktan sonra dönüş (return) koduna “?” değişkeninin değeri ile ulaşabiliriz. Tabi yine variable substitution yapmak için başına “$” işaretini eklememiz lazım.


Artık fonksiyonumuza geçebiliriz:

function scan {
    local YANIT="0"; local IP=$1.$2.$3
    local ECHO="echo-0"; local TIMESTAMP="timestamp-0";local ADDRESSMASK="addressmask-0"

Script dili içinde fonksiyon tanımı için daha önce de söylediğimiz gibi parametrelerin ve veri tiplerinin tanımlanmasına gerek yok. Fonksiyonun içindense bu değişkenlere parametrelerin verildiği sıraya göre $1, $2, $3, .. şeklinde ulaşabiliriz. “scan” fonksiyonumuza hatırlarsanız birinci parametre olarak kullanıcıdan aldığımız “SUBNET” değerini, ikinci parametre olarak ilk “for” döngüsünün ürettiği “i” değerini, üçüncü parametre olarak da ikinci “for” döngüsünün ürettiği “j” değerini kullanmıştık. Bu değerleri kullanarak IP adresini oluşturuyor ve “IP” adlı değişkene atıyoruz. Burada “local” ibaresine aslında ihtiyacımız yok, ancak daha sonra script’i “scan” fonksiyonunu background’da paralel olarak çalıştıracak, böylelikle çok daha hızlı tarama yapacak şekilde değiştireceğim. Ancak bu durumda karşımıza bir “thread” güvenliği problemi çıkıyor. Eğer fonksiyonun içindeki bir değişkeni “local” olarak tanımlamazsam background’da çalışan diğer kanallar (shell bunu nasıl uyguluyor tam olarak bilmiyorum ama mantıken thread’ler olarak yapması lazım, çünkü proses olarak yapsaydı bu problemimiz olmazdı) bu değişkenin değerini bozuyor ve farklı bir thread’in bozduğu değeri okuyarak işlem yapmaya çalışıyorlar. “local” tanımı bu değişkeni gerçekten bir lokal fonksiyon değişkeni haline getiriyor, aksi halde fonksiyon içinde tanımlanmış olsa da global bir değişken olarak davranıyor.

Yukarıda gördüğünüz tüm değişkenler fonksiyonun içinde değiştiği ve kullanıldığı için de “local” olarak tanımlandı.

hping3 $IP -c 1 -1 -C 8 >& aradosya.$IP # ECHO Request
    if grep "1 packets received" aradosya.$IP >& /dev/null; then
        if ! grep "Unreachable" aradosya.$IP >& /dev/null; then
            ECHO="echo-1"; YANIT="1"
        fi
    fi

Daha sonra gelen bölüm aslında temel işlevi yerine getiriyor, “hping3” uygulamasını gerekli parametrelere “Echo Request” paketi üretmesi için çağırıyoruz. “hping3” uygulamasının fonksiyonalitesini kısaca açıklamak gerekirse bu uygulama bir paket üretme (packet crafting) uygulaması. “hping3” uygulamasının çıktısını stderr dahil olmak üzere bir dosyaya yönlendiriyoruz. Daha önce sıralı iş çalıştırma imkanımız olduğunu ve tek satırda “;” ile ayrılmış birden fazla komut çalıştırabileceğimizi söylemiştim. Ama script içinde bir ara dosya kullanmayı daha uygun buldum.
“hping3”ü çağırdığımız satırın sonunda “#” işareti ile başlayan bölüm bir yorum bölümü. “#” işaretinden sonra yazdıklarımız yorumlayıcı (interpreter) tarafından kod olarak yorumlanmıyor.

“hping3” işlevini yerine getirdikten ve tüm çıktısını bir ara dosyaya yönlendirdikten sonra, “grep” komutuyla dosya içinde “1 packets received” ifadesini arıyoruz. “grep” komutundan daha önce de bahsetmiştik, özetle parametre olarak aldığı bir dosya veya kendisine yönlendiren çıktının içinde eğer aranan ifade geçiyorsa, bu satırın tamamını döndüren bir komuttur. Ancak biz burada “grep”in çıktısından ziyade “grep”in dönüş (return) koduyla ilgileniyoruz. Eğer “grep” aranan ifadeyi bulabilirse “0” döndürüyor, bulamazsa “1” değerini döndürüyor. Bu değer de “if” koşulu için “true” veya “false” anlamına geliyor. Biliyorum diğer dillere göre biraz kafa karıştırıcı çünkü uygulama dillerinden “false” genellikle “0” olur, “0” dışı tüm değerler de “true” anlamına gelir.


Yukarıda da gördüğünüz gibi shell için “true” durumu “0” ile ifade ediliyor.
Biz “grep”in döndürdüğü değerle ilgilenmediğimiz için de çıktısını “/dev/null” dosyasına yönlendiriyoruz. Bu dosyanın özel bir dosya olduğunu ve bu amaçla kullanıldığını daha önce de belirtmiştik.

“hping3” aracını script’le kullandığımızda gördük ki, bazen çok fazla istekle karşılaşan ağ cihazları hedef IP adresine ulaşılamadığına ilişkin “Host Unreachable” veya “Network Unreachable” yanıtı üretiyorlar. Bu durumda da bizim ICMP yanıtı alındığını anlamak için kullandığımız “grep” komutu olumlu sonuç dönüyor, halbuki dönen yanıt bizim isteğimizle ilgili değil. Bu hatadan kurtulmak için dosyanın içinde bir de “Host Unreachable” ifadesini arıyoruz. Ancak bu defa “!” işareti ile “değil” mantıksal işlemini yapıyoruz, yani “if” koşulu ancak bu ifade yoksa içindeki kodu çalıştırıyor.
Eğer “Echo Request” isteğimize bir yanıt aldıysak ve bu yanıtın içinde de “Host Unreachable” ifadesi geçmiyorsa “ECHO” değişkenimize “echo-1” değerini, “YANIT” değişkenimize de “1” değerini atıyoruz.

if test "$YANIT" == "1" ; then
        echo $IP";"$ECHO";"$TIMESTAMP";"$ADDRESSMASK
    fi
    rm aradosya.$IP
}

Diğer iki bölüm de farklı ICMP isteklerini aynı şekilde gönderdiği ve yanıtları işlediği için atlıyorum. Fonksiyonun son bölümünde herhangi bir ICMP isteğine yanıt alındığı anlamına gelen “YANIT” değişkeninin değerinin “1” olup olmadığına bakıyoruz, eğer bir veya daha fazla yanıt almışsak da ilgili IP adresini ve hangi isteğimize yanıt aldığımızı raporluyoruz. Buradaki “if” koşulunda belirtmekte fayda gördüğüm bir konu var, bu da shell scripting’in ilginç durumlarından birisi. “YANIT” değişkeninin değerinin “1” olup olmadığını kontrol ederken sadece $YANIT ifadesini kullanamıyoruz. Çünkü bu değerin atanmaması veya tamsayı olması durumunda karşılaştırma yapılan ve bir string gibi işlem gören “1” ifadesiyle uyumsuzluk oluşuyor. Bu durumda da shell hata üretiyor. Bundan kurtulmak amacıyla $YANIT ifadesini çift tırnak içine alıyoruz ki bu değişken atanmasa da içeriği tamsayı olsa da bir string gibi muamele görsün.

Bu raporlama adımından hemen sonra da ara dosyamızı siliyoruz.
Şimdi script’imizi bir deneyelim (dürüst olmak gerekirse eğer sürekli script yazmıyorsanız zaten script’i geliştirirken defalarca denemek ve hata ayıklamak gerekiyor):


Alacağınız sonuçlar ilgili ağ bölümünde ne kadar cihaz bulunduğuna bağlı olacaktır, ancak bu haliyle script’imiz kullanılamayacak kadar yavaş. Bunun için kodumuzda aşağıdaki gibi bir değişiklik yapıyoruz.

for i in `seq 1 254`; do
    for j in `seq 1 254`; do
        scan $SUBNET $i $j &
        sleep 0.2
    done
done

Yukarıdaki kodda farklı olan 2 nokta var, birincisi “scan” fonksiyonunu çağırdığımız satırın sonuna “&” karakterini ekledik. Bu karakter bildiğiniz gibi bir komut shell’de bu şekilde çalıştırıldığında bu işlemin arka planda (background) çalıştırılmasına neden oluyor. Şimdi bu değişikliği yaptıktan sonra script’imizi çalıştıralım.


Bu kez çok daha kısa bir sürede çok geniş bir ağ’dan yanıt alabilmeye başladık.

Shell scripting konusuyla ilgili son olarak script dosyasının çalıştırılabilir olması ihtiyacına değinelim. Dosya erişim haklarının düzenlenmesi bölümünde “chmod” komutundan, dosya erişim haklarının neler olduğundan ve “umask” konusundan söz etmiştik.

Script dosyamızı ilk oluşturduğumuzda dosya üzerinde tanımlı olan haklar aşağıdaki gibiydi:


Bu haliyle script’imizi çalıştırmak istediğimizde aşağıdaki hatayı alıyoruz:


Yukarıda gördüğünüz gibi sadece dosya ismini kullanarak onu çalıştırmak istediğimizde yetki hatası veriyor. Ayrıca çalıştırılabilir dosya olmayan bir dosya adı bu şekilde kullanıldığında shell’in dosya adını tamamlama desteğinden de (yani dosya adının bir kısmını yazdıktan sonra TAB tuşuna basarak adın geri kalanını otomatik olarak tamamlama) faydalanamıyoruz, çünkü böyle bir kullanım mantıklı değil.
Hemen sonra “bash” uygulamasını çalıştırıp bu defa script dosyasını parametre olarak verdiğimizde script’imiz çalışıyor. Ancak buna ihtiyaç duymadan doğrudan script dosyamızın adını kullanarak script’imizi çalıştırmak istersek dosyanın erişim haklarına çalıştırma (execute) hakkını da eklememiz gerekir.


Bu örnekte sadece dosyanın sahibi kullanıcı olan root kullanıcısına bu hakkı verdik, sadece “chmod +x” diyerek tüm kullanıcılar için de bu hakkı tanımlayabilirdik. Bu işlemi yaptıktan sonra yetki hatası almadan script’imizi başlatabildik.
Script’imizin çıktılarını “;” karakteri ile birbirinden ayırmamız daha sonra işimize yarayabilir. Örneğin “cut” filtresi ile bu çıktılardan sadece istediklerimizi listeleyebiliriz.


Yukarıdaki örnekte script’imizin çıktılarının “;” delimiter’i ile ayrıldıklarını belirtiyoruz, “-f 1,3” ifadesi ile bunlardan 1. ve 3. sırada olanlarını listelemek istediğimizi ve bunları listelerken de “ “, yani boşluk karakterleri ile birbirlerinden ayırmak istediğimizi belirtiyoruz. Böylece listelenen IP adresleri ve sadece bunların Timestamp Request isteklerimize yanıt verip vermediklerini görmüş oluyoruz.

<<Önceki Bölüm                                                                                               Sonraki Bölüm>>