Eksploitasi Bug Deserialization

Serialization adalah proses mengubah struktur data atau state sebuah objek ke sebuah format yang bisa disimpan atau dikirim. Proses serialization ini bisa dicoding secara manual oleh programmer, atau menggunakan library yang sudah ada. Jika data hasil serialization bisa diubah/tamper, maka ada potensi ini membuat masalah ketika data ini dibaca lagi (di-deserialize).

Tulisan ini akan membahas perkenalan bug deserialization secara umum, tanpa peduli apa format yang dipakai (native, XML, JSON, dsb). Penjelasannya juga saya buat segenerik mungkin, tidak bergantung bahasa tertentu, walau contoh yang dipakai adalah Java.

Serialization dan Deserialization

Sebelum masuk lebih jelas masalah deserialization, saya contohkan dulu proses serialization data dari sebuah kelas sederhana.

class Point {
    public int x;
    public int y;
}

Kita bisa menyimpan nilai x dan y dalam Point ini ke file (atau bisa juga dikirim ke jaringan) dalam berbagai format, misalnya:

  • dalam format teks/String dibatasi karakter tertentu, misalnya x|y, atau x<tab>y
  • dalam representasi biner bilangan tersebut
  • dalam format JSON {"x":10, "y":10}
  • dalam format XML <Point x="10" y="10" />
  • dan masih banyak lagi

Intinya: tidak ada satu format serialisasi, format apa saja bisa untuk serialization. Berikut ini contoh serialization dan deserialization dalam format string dengan pemisah/delimiter berupa |:

public class Point {
    public int x;
    public int y;
    public String serialize() { 
          return Integer.toString(x) + "|" + Integer.toString(y); 
    }
    public void deserialize(String s) throws Exception {
          String parts[] = s.split("|");
          x = Integer.parseInt(parts[0]);
          y = Integer.parseInt(parts[1]);
    }
}

Mengubah data hasil serialization

Untuk mengubah data hasil serialization, kita perlu mengerti di mana data ini bisa ada. Data hasil serialization bisa ada di mana-mana, contohnya:

  • Game bisa menyimpan data hasil serialization di file save game (contoh: kita bisa mengirim file save game yang jika diload di komputer orang akan mengeksekusi kode tertentu)
  • Aplikasi web bisa menyimpan data hasil serialization di Cookie
  • Aplikasi web bisa menyimpan data di parameter GET/POST

Biasanya ketika mengeksploit bug XSS atau bug SQL injection kita hanya melihat ada input, lalu memodifikasi input tersebut. Dalam bug deserialization biasanya kita perlu melihat bahwa data tertentu adalah hasil serialization, lalu mengubah datanya.

Sekarang ini asumsikan saja kita tahu format serialization-nya dan bisa memodifikasi datanya. Dalam contoh kasus di atas, kita tahu formatnya dari membaca kodenya. Lalu dari situ kita bisa membuat data x dan y baru dengan sekedar membuat string baru, misalnya "10|15" (x=10, y=15).

Jika format serialization berupa teks (misalnya: JSON, XML, YAML) maka mengedit data bisa dilakukan dengan editor teks saja. Jika formatnya biner, maka biasanya yang dilakukan adalah: buat kelas yang ingin di-serialize, lalu buat program kecil untuk melakukan serialization terhadap kelas tersebut.

Serialized Config/Serialized Cookie

Eksploitasi paling sederhana dari serialization adalah sekedar mengubah data yang bisa mengubah perilaku program. Contoh, jika cookie mengandung data yang bisa diedit seperti ini, dan kita mengubah “isadmin” dari 0 menjadi 1, maka tiba-tiba kita menjadi admin.

{"user": "yohanes", "isadmin":0}

Hampir semua aplikasi sekarang ini menggunakan signature (dengan hash atau HMAC) sebelum atau sesudah data yang diserialisasi) sehingga perubahan semacam ini tidak mungkin dilakukan. Dalam kasus tertentu, algoritma hashingnya salah, atau kita mendapatkan keynya sehingga bisa membuat data baru dengan signature yang akan diterima aplikasi.

Eksploitasi berikutnya yang lebih kompleks adalah: code execution, atau dalam banyak kasus aplikasi web: remote code execution. Dalam kasus ini, kita memaksa agar proses deserialisasi mengeksekusi kode yang kita inginkan. Tapi sebelumnya kita perlu memahami dulu bahwa dalam proses deserialization biasanya akan ada method call.

Method call dalam deserialization

Melanjutkan contoh sebelumnya, bisanya di berbagai bahasa kita tidak langsung mengakses field dengan akses public, tapi memakai getter/setter. Kira-kira seperti ini:

class Point {
    private int x;
    private int y;
    
    public void setX(int x) { this.x = x;}
    public void setY(int y) { this.y = y;}

    public String serialize() { 
          return Integer.toString(x) + "|" + Integer.toString(y); 
    }
    public void deserialize(String s) throws Exception {
          String parts[] = s.split("|");
          setX(Integer.parseInt(parts[0]));
          setY(Integer.parseInt(parts[1]));
    }
}

Dalam kasus ini method/setter setX dan setY dipanggil. Dalam kasus ini masih tidak berbahaya. Tapi sebuah setter bisa mengandung kode lain. Saya contohkan kelas sederhana yang tidak aman. Contohnya agak mengada-ada, tapi masih cukup masuk akal.

Contoh ini adalah sebuah kelas untuk memeriksa koneksi jaringan, dan bisa diset command apa yang digunakan untuk mengetest jaringan (misalnya ping bisa diganti dengan nmap). Di dalam setCmd akan dilakukan pemeriksaan apakah perintahnya bisa dicoba dijalankan, dan jika tidak bisa, maka akan digunakan perintah standard ping.

public class NetworkTester {

    int interval;
    String cmd;

    NetworkTester() {
        interval = 10;
    }

    public void setInterval(int i) {
        interval = i;
    }

    public void setCmd(String cmd) throws Exception {
        Process p = Runtime.getRuntime().exec(cmd + " google.com");
        p.waitFor();
        if (p.exitValue() == 0) {
            this.cmd = cmd;
            return;
            
        }
        this.cmd = "/bin/ping -c 2";
    }   

    public String serialize() {
        return Integer.toString(interval) + "|" + cmd;
    }

    public void deserialize(String s) throws Exception {
        String parts[] = s.split("|");
        setInterval(Integer.parseInt(parts[0]));
        setCmd(parts[1]);
    }

}

Di contoh di atas, jika kita bisa mengubah data hasil serialization, dan mengganti ping dengan perintah lain (misalnya instruksi berbahaya seperti "rm -rf /" atau shell connect back), maka perintah itu akan dieksekusi ketika deserialize dipanggil (karena deserialize memanggil setCmd).

Di dalam contoh yang saya berikan terlihat jelas sekali di mana code execution ini bisa terjadi. Dalam dunia nyata, eksploitnya tidak akan semudah ini . Tapi inti semua code execution dengan serialization adalah ini:

DATA + METHOD YANG OTOMATIS DIPANGGIL = EKSEKUSI KODE

Selain setter, di berbagai library dan bahasa akan ada method tertentu yang secara otomatis/ajaib akan dipanggil. Sekarang mari kita bahas dulu fungsi serialization dan deserialization yang biasanya sudah ada di sebuah library atau bahasa.

Fungsi dan library untuk data serialization

Berbagai bahasa memiliki fungsi buit ini untuk serialization dan deserialization dan fungsionalitasnya bisa ditambah dengan library tambahan. Beberapa contoh:

Dalam PHP, setter dan getter tidak dipanggil ketika serialization. Tapi ada method __wakeup yang dipanggil setelah nilai field diset. Contohnya seperti ini:

<?php

class Point {
       public $x = 0;
       public $y = 0;

       function __construct($x, $y) {
               $this->x = $x;
               $this->y = $y;
       }

       function print() {
                echo "X = ".$this->x. " Y = ".$this->y."\n";
       }

       function __wakeup() {
                echo "WAKEUP X = ".$this->x. " Y = ".$this->y."\n";
       }

};

$p = new Point(10, 10);
$p->print();
$z = serialize($p);
echo $z."\n";
echo "unserializing ...\n";
$p2 = unserialize($z);
$p2->print();

?>

Jika programnya saya jalankan di terminal dengan command line PHP:

X = 10 Y = 10 # output dari Point pertama
O:5:"Point":2:{s:1:"x";i:10;s:1:"y";i:10;} #hasil data yang diserialize
unserializing ... 
WAKEUP X = 10 Y = 10 # __wakeup dipanggil, X dan Y sudah diset menjadi 10
X = 10 Y = 10 # output dari Point kedua

Apa kegunaan __wakeup ini? untuk menginisialisasi kelas setelah deserialisasi. Contoh yang bisa dilakukan di __wakeup:

  • Kelas untuk melakukan koneksi database bisa membuka koneksi ke database setelah nama host, port , username dan password diberikan
  • Kelas yang mengirimkan data ke jaringan bisa mulai membuka kembali koneksi socket setelah mengetahui host dan port tujuan
  • Kelas yang perlu membaca file bisa membuka file dan meload data setelah field nama file diset

Di Python method ajaib ini bernama __reduce__, dan sifatnya sama dengan __wakeup di PHP. Di Ruby instance method marshal_load  akan dipanggil dalam proses deserialization.

Di Java, deserialization defaultnya (bukan dari Library tertentu) akan memanggil readObject() dari interface java.io.Serializeable jika method tersebut ada pada sebuah kelas. Sementara library tertentu mungkin akan memanggil method setter sebuah Field. Sifat .NET serupa dengan Java (hanya beda nama kelas dan methodnya).

Reflection

Dalam contoh pertama yang saya berikan, saya tahu dengan tepat property yang ingin saya serialize adalah x dan y. Bagaiman sebuah library bisa dipakai generik untuk berbagai objek, bagaimana library tersebut tahu property/field apa yang perlu di-serialize? Jawabannya adalah fitur reflection yang ada di berbagai bahasa dinamis. Dengan reflection, kita bisa melakukan hal-hal ini:

  • Mendapatkan kelas dari sebuah Objek
  • Membuat instance dari sebuah kelas
  • Mendapatkan daftar Field dan Method sebuah kelas
  • Membaca dan menulis field/property sebuah objek
  • Memanggil method sebuah objek

Ini contoh kecil dalam bahasa Java bagaimana kita bisa mendapatkan daftar method diberikan sebuah objek:

package testapp;

import java.lang.reflect.Method;

public class Testapp {

    public static void test(Object x) {
        System.out.println("Class Name " + x.getClass().getName());
        //list methods        
        Method methods[] = x.getClass().getMethods();
        for (Method m: methods) {
            System.out.println("Method " + m.getName() + " from " + m.getDeclaringClass());
        }                
    }
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Testapp o = new Testapp();
        test(o);
    }
    
}

Output seperti ini: (ada method duplikat namanya karena method overloading sedangkan contoh di atas tidak mencetak lengkap parameternya)

 Class Name testapp.Testapp
 Method main from class testapp.Testapp
 Method test from class testapp.Testapp
 Method wait from class java.lang.Object
 Method wait from class java.lang.Object
 Method wait from class java.lang.Object
 Method equals from class java.lang.Object
 Method toString from class java.lang.Object
 Method hashCode from class java.lang.Object
 Method getClass from class java.lang.Object
 Method notify from class java.lang.Object
 Method notifyAll from class java.lang.Object

Dengan menggunakan reflection, berbagai library serialization bisa melakukan serialization objek apa saja ke berbagai format. Ketika menuliskan serialization, nama kelas akan dituliskan ke stream, dan ketika dilakukan deserialization, objek akan diciptakan kembali dari nama kelas tersebut.

Contohnya jika kita punya nama "java.awt.Point" (ini kelas sederhana di Java yang hanya memiliki field x dan y). Kita bisa membuat objek java.awt.Point seperti ini:

    public static  void makePoint() throws Exception {
        Class c = Class.forName("java.awt.Point");
        Object o = c.getDeclaredConstructor().newInstance();
        test(o); //test print methods
    }

Ciri-ciri serialized data

Data yang diserialisasi formatnya bisa apa saja, format native, JSON, XML, dsb. Beberapa format serialisasi memiliki ciri-ciri, misalnya PHP memakai format teks seperti ini:

O:5:"Point":2:{s:1:"x";i:10;s:1:"y";i:10;}

Dari penjelasan mengenai reflection , bisa diambil kesimpulan: biasanya dalam hampir semua format serialisasi akan terlihat nama kelas yang diserialisasi (dalam contoh PHP di atas nama kelasnya adalah Point). Berikut ini contoh hasil serialization dalam Java dari sebuah kelas bernama ‘testapp.Point‘.

Data di atas dalam base64.

rO0ABXNyAA10ZXN0YXBwLlBvaW505U+ktqlBqysCAAJJAAF4SQABeXhwAAAAAAAAAAA=

Format serialisasi dari Java jika diencode dengan base64 akan terlihat memiliki header ‘rO0’. Ini Berbagai format lain memiliki header yang lain (silakan dicari sendiri).

Hal yang penting dari sebuah library serialization dan deserialization adalah: apakah library tersebut mengijinkan kita melakukan serialize dan unserialize kelas apapun, atau hanya kelas tertentu saja. Jika kita bisa membuat instance apapun, maka eksploitasi akan lebih memungkinkan.

Gadgets

Langkah eksploitasi sebenarnya sederhana, tapi detailnya yang rumit

  • Cari di titik mana kita bisa mengirimkan request yang akan di-deserialize, atau mengubah data yang akan di-deserialize
  • Cari kelas yang jika di-deserialize akan memanggil method tertentu, dan method tersebut mengeksekusi kode dari nilai field yang kita berikan

Di contoh kelas NetworkTester yang saya berikan di atas, saya sengaja membuat bug yang sangat jelas, tapi kebanyakan aplikasi yang sangat sederhana tidak memiliki bug yang berhubungan dengan serialization di aplikasi itu sendiri. Sayangnya (atau untungnya, bagi pentester/hacker) kebanyakan aplikasi memakai banyak library yang memiliki bug.

Untuk library tertentu dengan versi tertentu, sudah banyak yang menemukan kelas-kelas yang bisa dieksploitasi. Kombinasi berbagai kelas yang bisai dieksploitasi dengan deserialization ini disebut dengan gadget. Beberapa contoh library Java versi lama yang memiliki gadget untuk code execution: commons-collection, spring-core, spring-aop. Berbagai gadget ini bisa dilihat di tool open source ysoserial (untuk Java) dan yoserial.net (untuk .NET). Jadi sebelum susah payah melihat semua kelas dalam sebuah aplikasi, cek dulu library apa saja yang dipakai oleh aplikasi tersebut.

Secara umum masalah dari berbagai kelas tersebut adalah: adanya penggunaan reflection, dan kita bisa memaksa untuk memanggil method manapun.

Penutup

Artikel ini hanya sekedar perkenalan mengenai eksploitasi deserialization. Berbagai artikel lain yang lebih mendalam untuk teknologi tertentu bisa dibaca sendiri, beberapa contohnya:

Sering kali kita bisa dengan buta menggunakan ysoserial dan mencoba-coba semua gadget yang ada untuk mendapatkan code execution. Dalam kasus lain kadang kita perlu sedikit memodifikasi gadget ysoserial (atau mengcompile ulang payload agar sesuai versi target). Dalam kasus lain lagi, kita perlu membuat sendiri gadgetnya.

Tinggalkan Balasan

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses.