Dalam tulisan ini saya akan sharing bagaimana menyelesaikan challenge CTF OWASP dalam Appsec USA 2011.
Dalam challenge ini kita diminta mendownload sebuah file ZIP yang berisi sebuah Applet (dalam JAR) dan html untuk me-load Applet tersebut. Ketika Applet tersebut dibuka di browser akan terlihat seperti ini:
Kita dihadapkan pada form yang meminta kita memasukkan username dan PIN. Jadi challenge kita adalah menemukan username dan PIN yang tepat untuk mendapatkan flag yang terenkripsi. Dari form tersebut kita mendapat clue bahwa PIN ini formatnya adalah 6 digit.
Sebelumnya kita harus tahu dulu darimana data username/PIN tersebut disimpan. Ada dua kemungkinan:
- Server-side authentication: bila keputusan credentials benar/salah diputuskan di server
- Client-side authentication: bila keputusan credentials benar/salah diputuskan di client (browser)
Pertama saya menguji dengan memasukkan user dan PIN sembarang, dari pantauan sniffer tidak terlihat ada traffic data sedikitpun. Dari sini bisa kita simpulkan bahwa ini adalah client-side authentication, artinya keputusan username dan PIN yang dimasukkan benar atau salah sepenuhnya diputuskan di client-side (dalam browser/Applet).
Dalam client-side authentication, pasti ada repository untuk menyimpan data user dan passwordnya. Dalam kasus ini, data tersebut disimpan dalam JAR. Dalam file JAR tersebut kalau kita buka, kita akan temukan file:
org\owasp\appsecusa\capturetheflag\loginchallenge\data.xml. Berikut adalah isi dari data.xml
<?xml version="1.0" encoding="UTF-8"?> <root> <users> <identity> <uid>1</uid> <username>Larry</username> <password>520540d155fdde616031d546cd5c747a</password> </identity> <identity> <uid>2</uid> <username>Curly</username> <password>edbf0f9d73cd984fae64c44c618f3c33</password> </identity> <identity> <uid>3</uid> <username>Moe</username> <password>b2512d38c5702ce5108585d3c730f657</password> </identity> </users> <storage> <record> <sid>3</sid> <data>LCNIk2NvX1JdVcwIoWBxyewfKIFexhCeGnepwYK63RmSrEmAbEg/CHLRA7sK0mjAB/ZJzbM113Mv 3YXg2d46age+gNFoERSce9u2/up8xcpjfLdKd9RilU1hqpkZ0vcXkgZSn8XTnecHVYSRG45gXuWA PzWGvNdy396WivrEKZo5B3kGUOJh8XIBW90aA9RWN0thyHoJvjaiu7alKuv/wtKwMk0ZTuZol5Le rY7iJJdaEPthqeso3bHg0xd3xKmD2X09PdEa1U8ZuOYH2Z3ZjN4fj6gm5EBjDNferc7rSWYcbNkF 064jnQHO73XQAURMXl+vaXWMX3i7uqWinm/eSc1BUspXaB3BBZChcoMqK0+gdsmzd1GGalVqL86f xBL7T5yVS7RJe0r45HtCYXroecR1aaO6xzHUwcAqGAD2Km2n0DrmRz9MoVO0yR2/KipiAfjFzPd+ 7wjbzF5M8BcfLNRtBR9fTK86fkeIO+19+IFwnlewYFzVIWtTebm3zOHJCbQtBttMfwp8YaM+UWxZ sA==</data> </record> </storage> </root>
File XML ini menyimpan data user, password dan data yang terenkripsi (kemungkinan ini adalah flag yang akan kita capture). Dari isinya bisa kita lihat ada 3 user: Larry, Curly dan Moe. Password terlihat dalam bentuk hash sepanjang 32 karakter (dari panjangnya kemungkinan ini adalah MD5).
Karena kemungkinan password disimpan dalam MD5 dan formatnya adalah 6 digit, maka saya mencoba cara paling mudah dulu, yaitu melakukan brute force dengan john the ripper. Dengan diketahui formatnya adalah 6 digit, john bisa melakukan brute force dalam hitungan detik saja.
Sebelum menjalankan john, kita perlu edit john.conf dulu agar lebih efisien. Pada bagian Incremental:Digits Kita masukkan minLength=6 dan maxLength=6 karena kita tahu bahwa yang kita cari panjangnya tepat 6 digit.
Oke sekarang kita coba jalankan john dan berdoa semoga passwordnya ditemukan.
John dengan opsi “incremental:Digits” dan panjang diset 6 ternyata tidak berhasil mendapatkan kecocokan, artinya format tersebut pasti bukan plain MD5, kemungkinan sudah ditambah salt. Oke kalau cara ini gagal, sekarang waktunya “go deeper”.
Decompiling Kita akan “go deeper” dengan melakukan decompiling file class yang tersimpan dalam JAR.
Class yang bertanggung jawab melakukan otentikasi adalah org.owasp.appsecusa.capturetheflag.loginchallenge.Authenticator khususnya pada method authenticate().
1 2 3 4 5 6 7 | public boolean authenticate(String username, String pin) throws EncryptionException { if ((username == null) || (!this.usersIds.containsKey(username))) return false; return this.calculator.compare(pin, (String)this.usersIds.get(username)); } |
Dalam method authenticate() memakai method compare() dari class MD5HashCalculator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public String getHash(String text) throws EncryptionException { try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update("BuildYourOwnRainbow-AppSecUSA:".getBytes(), 0, "BuildYourOwnRainbow-AppSecUSA:".length()); m.update(text.getBytes(), 0, text.length()); byte[] digest = m.digest(); BigInteger bigInt = new BigInteger(1, digest); String hashtext = bigInt.toString(16); while (hashtext.length() < 32) { hashtext = "0" + hashtext; } return hashtext; } catch (NoSuchAlgorithmException ex) { } throw new EncryptionException("Failed to digest:" + ex.getMessage(), ex); } public boolean compare(String text, String hash) throws EncryptionException { return getHash(text).equals(hash); } |
Dari potongan kode di atas terjawab bagaimana password disimpan. Password disimpan memang dalam MD5, tapi sudah diberi salt string:”BuildYourOwnRainbow-AppSecUSA:”, ini sebabnya john the ripper tidak berhasil meng-crack.
Brute forcing PIN Karena kita sudah mengetahui bagaimana otentikasi dilakukan (MD5+salt) maka kita bisa meminjam code di atas dan memodifikasinya untuk melakukan brute force. Karena PIN adalah 6 digit, maka kita bisa melakukan brute force dari 100000 sampai 999999.
Kita hanya menambahkan method main yang melakukan looping dari 100.000 sampai < 1.000.000 dan memanfaatkan method compare getHash() dan compare() yang sama tanpa modifikasi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.io.*; public class md5 { public static void main(String[] args) { for (int i = 100000; i < 1000000; i++) { String text = ""+i; boolean match = compare(text,"b2512d38c5702ce5108585d3c730f657"); if (match) { System.out.println("MATCH nih, Moe:"+text); return; } } } public static String getHash(String text) { try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update("BuildYourOwnRainbow-AppSecUSA:".getBytes(), 0, "BuildYourOwnRainbow-AppSecUSA:".length()); m.update(text.getBytes(), 0, text.length()); byte[] digest = m.digest(); BigInteger bigInt = new BigInteger(1, digest); String hashtext = bigInt.toString(16); while (hashtext.length() < 32) { hashtext = "0" + hashtext; } return hashtext; } catch (Exception ex) { System.out.println("Failed to digest:" + ex.getMessage()); return null; } } public static boolean compare(String text, String hash) { return getHash(text).equals(hash); } } |
Moe’s Flag
Dengan mengetahui PIN dari 3 user tersebut, saya mencoba login. Dari ketiga user tersebut hanya Moe yang mengandung Flag. Berikut tampilan ketika Moe login.
Ups, ternyata kita mendapatkan pesan:
“There is an encrypted flag stored for this user! I am able to decrypt it using your information but, unfortunatly, I cannot share it with you as the displayFlay variable is set to false. That’s about all I can tell you. Good Luck”
Ternyata kita tidak bisa langsung mendapatkan flag, masih ada satu barrier lagi yang harus kita lewati. Mari kita korek lagi source codenya untuk mencari variable displayFlay.
Displaying Flag Dalam class org.owasp.appsecusa.capturetheflag.loginchallenge.Login ada method displayFlag() yang fungsinya menampilkan flag.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private void displayFlag(Boolean displayFlag) throws IOException, EncryptionException { BASE64Decoder de = new BASE64Decoder(); AES128Encryptor instance = new AES128Encryptor(); InputStream in = new ByteArrayInputStream(de.decodeBuffer(this.encodedData)); ByteArrayOutputStream out = new ByteArrayOutputStream(); instance.decrypt(this.myWindow.pin.getText(), in, out); ByteArrayInputStream flagData = new ByteArrayInputStream(out.toByteArray()); BufferedImage image = ImageIO.read(flagData); ImageIcon icon = new ImageIcon(image, "Our precious flag."); if (displayFlag.booleanValue() == true) this.myWindow.loginResponseLabel.setIcon(icon); else this.myWindow.response_label.setText("There is an encrypted flag stored for this user!\nI am able to decrypt it using your information\nbut, unfortunatly, I cannot share it with you as the\ndisplayFlay variable is set to false.\nThat's about all I can tell you. Good Luck!"); } |
Dalam method di atas terlihat bahwa flag berupa image dienkrip dengan AES 128 bit dengan PIN sebagai kuncinya. Masalahnya ada pada baris ke-10, bila method displayFlag dipanggil dengan parameter boolean false, maka akan flag tidak akan ditampilkan.
displayFlag() ini dipanggil dalam actionPerformed() masih di class yang sama. Berikut adalah potongan kode dalam actionPerformed() yang memanggil displayFlag().
1 2 3 4 5 | if (this.dataTest.booleanValue() == true) displayFlag(Boolean.valueOf(false)); else this.myWindow.response_label.setText("There are no encrypted flags stored for this user."); } |
Potongan kode di atas menunjukkan bahwa displayFlag() dipanggil dengan parameter false, akibatnya flag tidak akan muncul. Parameter false ini sudah dibuat static hardcoded dalam source codenya, jadi dalam kondisi apapun akan tetap false. Karena dibuat static dan hardcoded, tidak ada cara lain untuk membuatnya menjadi true tanpa mengubah program.
Capturing the Flag Kita bisa saja mengubah source code class tersebut dan mengubah “displayFlag(Boolean.valueOf(false))” menjadi “displayFlag(Boolean.valueOf(true))” lalu meng-compile dan menjalankan program tersebut. Tapi saya memilih untuk mengubahnya menjadi program kecil di console (bukan Applet), hanya untuk menyimpan flag ke dalam file PNG.
Berikut adalah potongan kode untuk men-dekrip dan menyimpan flag ke dalam file flag.png. Kode tersebut diambil dari kode aslinya dengan sedikit modifikasi unutuk menyimpan flag ke dalam file.
Bila anda ingin mencoba sendiri, source code lengkap untuk mendapatkan flag bisa didownload di: Capture.zip, anda hanya perlu melakukan ‘javac ParseXML.java’ kemudian ‘java ParseXML’ untuk mendapatkan flag.png.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class ParseXML { public static LoginUsers parseXML() throws Exception { String resourceName = "Data.xml"; Reader reader = new BufferedReader(new InputStreamReader(ParseXML.class.getClassLoader().getResourceAsStream(resourceName))); JAXBContext jc = JAXBContext.newInstance(new Class[] { LoginUsers.class }); Unmarshaller um = jc.createUnmarshaller(); return (LoginUsers)um.unmarshal(reader); } private static void displayFlag(String encodedData) throws Exception { AES128Encryptor instance = new AES128Encryptor(); InputStream in = new ByteArrayInputStream(decode(encodedData)); ByteArrayOutputStream out = new ByteArrayOutputStream(); instance.decrypt("495823", in, out); ByteArrayInputStream flagData = new ByteArrayInputStream(out.toByteArray()); BufferedImage image = ImageIO.read(flagData); String fname = "flag.png"; File file = new File(fname); ImageIO.write(image,"png",file); System.out.println("Decrypted and Saved to "+fname); } |
Ketika file tersebut dijalankan akan membuat sebuah file bernama flag.png. Berikut adalah tampilan ketika class ini dijalankan dari command prompt.
Flag.png adalah QR barcode yang mengandung teks. Untuk membaca isi teks dari barcode tersebut saya memakai layanan barcode reader online.
Bendera berhasil kita rebut, yaitu teks: http://www.appsecusa.org/ctf.html. Sekarang tinggal submit jawabannya.
Tidak ada komentar:
Posting Komentar