
SQLite 3.53.4 - High - Local - UAF - rtree last_insert_rowid 0 node hash
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · sqlite-rtree-rowid0
Write-up: https://abraxaslabs.tech/research/sqlite-rtree-rowid0
Classe: UAF Portata: Locale
SQLite 3.53.4 (b09c88c14082339b66c7b7158d609a771e64ca69, SOURCE_ID 2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc) - Hwaci
nodeWrite calcola l'hash di un nuovo nodo rtree con iNode==0 quando sqlite3_last_insert_rowid() rimane 0 dopo un INSERT in %_node che riporta comunque OK. nodeHashDelete salta 0. nodeRelease dopo SplitNode libera il RtreeNode mentre l'hash lo contiene ancora. La successiva nodeHashLookup è un use-after-free sull'heap. La stessa nodeWrite è sul percorso geopoly.
RCE non dimostrata. Lettura ASan in nodeHashLookup. La correzione successiva nel branch 3.53 è SQLITE_CORRUPT_VTAB quando il numero di nodo assegnato è ancora 0. Quel controllo non è presente in questa GA.
| ID | nessuna CVE al momento |
| Classe | UAF (use-after-free sull'heap di RtreeNode; RCE non dimostrata) |
| Portata | Locale (SQL non attendibile o file .db non attendibile su una tabella rtree/geopoly) |
| CWE | CWE-416 |
| CVSS | Alto: 8.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Prodotto | SQLite rtree / geopoly (SQLITE_ENABLE_RTREE) |
| Affette | 3.53.4 (b09c88c) |
| Auth | SQL dell'attaccante o un trigger piantato sulla shadow table / %_node WITHOUT ROWID |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1:18890. Testimone ASan. Nessun payload di exploit. |
Su una tabella rtree o geopoly, far sì che l'INSERT di un nodo riporti OK mentre last_insert_rowid() rimane 0. Modi documentati: sostituire %_node con WITHOUT ROWID, oppure piantare BEFORE INSERT RAISE(IGNORE) sulla shadow table, poi riaprire in modo che l'ultimo rowid della connessione sia 0. Eseguire INSERT finché avviene uno split di un nodo. Il sibling destro viene hashato come nodo 0, rilasciato, e resta comunque individuabile.
Il processo host utilizza quindi un RtreeNode liberato. Il lab mostra un heap-use-after-free di AddressSanitizer. Il controllo dell'instruction pointer non è incluso in questo pacchetto. Il crash sì.
SQLITE_VTAB_INNOCUOUS è impostato, quindi rtree viene comunque eseguito con trusted_schema disattivato. La compilazione predefinita non imposta SQLITE_DEFAULT_DEFENSIVE. Un trigger piantato in precedenza in un file DB non attendibile viene comunque eseguito quando l'INSERT di pWriteNode di rtree viene eseguito.
Venti ricerche nel sorgente di https://github.com/sqlite/sqlite tag https://github.com/sqlite/sqlite/tree/version-3.53.4. L'albero canonico è https://sqlite.org/src. GitHub è un mirror Fossil. La 3.54.0 è ancora una bozza. Questo residuo è tipico del flag extra -DSQLITE_ENABLE_RTREE (la compilazione ufficiale raccomandata, le OPTS nmake di Windows, il fallback autoconf e fuzzcheck lo impostano tutti).
if( pNode->iNode==0 && rc==SQLITE_OK ){
pNode->iNode = sqlite3_last_insert_rowid(pRtree->db);
nodeHashInsert(pRtree, pNode);
}
static void nodeHashDelete(Rtree *pRtree, RtreeNode *pNode){
RtreeNode **pp;
if( pNode->iNode!=0 ){
pp = &pRtree->aHash[nodeHash(pNode->iNode)];
for( ; (*pp)!=pNode; pp = &(*pp)->pNext){ assert(*pp); }
*pp = pNode->pNext;
pNode->pNext = 0;
}
}
nodeAcquire cerca l'hash prima di rifiutare iNode<=0. Un hit sulla voce zero liberata è la UAF. Un miss su iNode<=0 è SQLITE_CORRUPT_VTAB, che è un percorso diverso.
Vicoli ciechi già registrati: trattare %_rowid last_insert_rowid 0 come questo bug (che assegna rowid 0 alla cella, non la UAF dell'hash); aspettarsi SQLITE_CORRUPT_VTAB su questo pin (quella è la correzione successiva); riempire 50 celle in modo che non ci sia split; piantare il trigger e non riaprire; lookaside che nasconde il free ad ASan; chiamare il crash RCE.
cd lab
./run.sh
Amalgamation 3.53.4 con -DSQLITE_ENABLE_RTREE=1 e -fsanitize=address. Vendor rtreeI.test 2.0-2.2: riempimento di 51 righe (RTREE_MAXCELLS), BEFORE INSERT RAISE(IGNORE) su %_node, riapertura con last_insert_rowid()==0, INSERT della 52ª riga (split), poi SELECT *. Lo split di controllo senza il trigger è OK. Bind su 127.0.0.1:18890. L'harness è lab/harness.c.
control-split=ok
reopen-last-rowid=0
witness-before-split=SQLITE-RTREE-ROWID0-WITNESS
ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 ... nodeHashLookup
freed by ... nodeRelease / SplitNode
SUCCESS sqlite-rtree-rowid0 how=asan-uaf-nodehash SQLITE-RTREE-ROWID0-WITNESS
Dopo sqlite3_last_insert_rowid(), se iNode è ancora 0, restituisce SQLITE_CORRUPT_VTAB e non esegue nodeHashInsert. branch-3.53 11ce330557 / Git 63b6d705 (2026-09-24) fa questo. Il test si aspetta "database disk image is malformed". Questa GA non lo fa.
b09c88c14082339b66c7b7158d609a771e64ca69)2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bccnodeWrite, nodeHashDelete, nodeRelease, nodeAcquire, SplitNode11ce330557 (2026-09-24, Claude / Ada Logics)