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.
No comments:
Post a Comment