php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #66550 SQLite prepared statement use-after-free
Submitted: 2014-01-22 17:06 UTC Modified: 2015-04-06 05:34 UTC
From: sean at persistencelabs dot com Assigned: stas (profile)
Status: Closed Package: SQLite related
PHP Version: master-Git-2014-01-22 (Git) OS:
Private report: No CVE-ID: None
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: sean at persistencelabs dot com
New email:
PHP Version: OS:

 

 [2014-01-22 17:06 UTC] sean at persistencelabs dot com
Description:
------------
Summary
-------
 
The sqlite3_close method, which is used to close a database connection, 
tears down the sqlite3_stmt objects associated with any prepared statements
that have been created using the database. The prepared statements can still
be accessed directly after this has occured, leading to a use-after-free 
condition. 

Impact
------

This bug leads to a fairly straightforward use-after-free scenario. Once the
sqlite3_stmt object has been returned to the heap, the attacker may reallocate
the buffer as they wish and then later access the memory as a sqlite3_stmt
object through their handle to the containing php_sqlite3_stmt.

Patch Details
-------------

When the sqlite3_stmt objects are torn down the 'initialised' field of the 
containing php_sqlite3_stmt is set to 0. The attached patch adds a check on
this field in each of the PHP_METHODs associated with an sqlite3stmt. 

Bug Details
-----------

The trigger file below demonstrates the issue. We begin by setting a 
breakpoint on sqlite3Prepare in order to find the sqlite3_stmt that will be
associated with our prepared statement. This is created in trigger.php via

$stmt = $db->prepare('SELECT bar FROM foo WHERE id=:id');

Breakpoint 4, sqlite3Prepare (db=0x8a6ba58, zSql=0xb7eb9274 "SELECT bar FROM foo WHERE id=:id", nBytes=32, saveSqlFlag=1, 
    pReprepare=0x0, ppStmt=0xb7fc3650, pzTail=0x0)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:90141
90141     char *zErrMsg = 0;        /* Error message */
(gdb)

The ppStmt variable is a sqlite3_stmt** and *ppStmt will be filled in with
a pointer to the associated statement. 

(gdb) b 90286
Breakpoint 5 at 0x81caa03: file /home/user/php/ext/sqlite3/libsqlite/sqlite3.c, line 90286.
(gdb) c
Continuing.

Breakpoint 5, sqlite3Prepare (db=0x8a6ba58, zSql=0xb7eb9274 "SELECT bar FROM foo WHERE id=:id", nBytes=32, saveSqlFlag=1, 
    pReprepare=0x0, ppStmt=0xb7fc3650, pzTail=0x0)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:90286
90286     sqlite3StackFree(db, pParse);

(gdb) p *ppStmt
$7 = (sqlite3_stmt *) 0x8a78b08

Next we will use sqlite3_close to free the memory at 0x8a78b08. This is
performed by the call to $db->close() in trigger.php resulting in a call to
zim_sqlite3_close.

File : ext/sqlite3/sqlite3.c

182 PHP_METHOD(sqlite3, close)
183 {
184         php_sqlite3_db_object *db_obj;
185         zval *object = getThis();
186         int errcode;

...

192 
193         if (db_obj->initialised) {
194                 zend_llist_clean(&(db_obj->free_list));
195                 errcode = sqlite3_close(db_obj->db);

...

204 }

On line 194 the zend_llist_clean function is called. The intention is to 
tear down any objects associated with the database object prior to destroying
the object itself. Each object in the list has a registered destructor, and 
we eventually find ourselves in php_sqlite3_free_list_dtor

(gdb) bt 4
#0  php_sqlite3_free_list_dtor (item=0xb7fc3674) at ext/sqlite3/sqlite3.c:2004
#1  0x0849b81d in zend_llist_destroy (l=0xb7fc361c) at Zend/zend_llist.c:112
#2  0x0849b887 in zend_llist_clean (l=0xb7fc361c) at Zend/zend_llist.c:124
#3  0x08134e5f in zim_sqlite3_close (ht=0, return_value=0xb7fc0bec, return_value_ptr=0xb7fa7100, this_ptr=0xb7fc0c08, 
    return_value_used=0) at ext/sqlite3/sqlite3.c:194
(More stack frames follow...)

File : ext/sqlite3/sqlite3.c

2002 static void php_sqlite3_free_list_dtor(void **item)
2003 {
2004         php_sqlite3_free_list *free_item = (php_sqlite3_free_list *)*item;
2005 
2006         if (free_item->stmt_obj && free_item->stmt_obj->initialised) {
2007                 sqlite3_finalize(free_item->stmt_obj->stmt);
2008                 free_item->stmt_obj->initialised = 0;
2009         }
2010         efree(*item);
2011 }

On line 2007 we can print the sqlite3_stmt *, to ensure it refers to our 
prepared statement

(gdb) p/x free_item->stmt_obj->stmt
$9 = 0x8a78b08

The function of interest to us is sqlite3VdbeDeleteObject, which is called to 
free all memory associated with a Vdbe (Virtual Database Engine) object. 
Apparently a sqlite3_stmt is also a Vdbe, and we can see our prepared
statement is passed as the argument p.

(gdb) bt 7
#0  sqlite3VdbeDeleteObject (db=0x8a6ba58, p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59981
#1  0x08184ded in sqlite3VdbeDelete (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:60016
#2  0x08142bdc in sqlite3VdbeFinalize (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59948
#3  0x08142b00 in sqlite3_finalize (pStmt=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:60876
#4  0x0813a828 in php_sqlite3_free_list_dtor (item=0xb7fc3674) at ext/sqlite3/sqlite3.c:2007
#5  0x0849b81d in zend_llist_destroy (l=0xb7fc361c) at Zend/zend_llist.c:112
#6  0x0849b887 in zend_llist_clean (l=0xb7fc361c) at Zend/zend_llist.c:124
(More stack frames follow...)

File : ext/sqlite3/libsqlite/sqlite3.c

59971 /*
59972 ** Free all memory associated with the Vdbe passed as the second argument.
59973 ** The difference between this function and sqlite3VdbeDelete() is that
59974 ** VdbeDelete() also unlinks the Vdbe from the list of VMs associated with
59975 ** the database connection.
59976 */
59977 SQLITE_PRIVATE void sqlite3VdbeDeleteObject(sqlite3 *db, Vdbe *p){

...

59994   sqlite3DbFree(db, p);

...

59995 }

18489 /*
18490 ** Free memory that might be associated with a particular database
18491 ** connection.
18492 */
18493 SQLITE_PRIVATE void sqlite3DbFree(sqlite3 *db, void *p){
18494   assert( db==0 || sqlite3_mutex_held(db->mutex) );
18495   if( db ){
18496     if( db->pnBytesFreed ){
18497       *db->pnBytesFreed += sqlite3DbMallocSize(db, p);
18498       return;
18499     }
18500     if( isLookaside(db, p) ){
18501       LookasideSlot *pBuf = (LookasideSlot*)p;
18502       pBuf->pNext = db->lookaside.pFree;
18503       db->lookaside.pFree = pBuf;
18504       db->lookaside.nOut--;
18505       return;
18506     }
18507   }

...

18512   sqlite3_free(p);
18513 }

In order to reach sqlite3_free, a wrapper around free(), we just need to pass
the isLookaside check. This is a simple range check on the span of the 
lookaside list which the sqlite3_stmt pointer will pass by default.

isLookaside (db=0x8a6ba58, p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:18444
18444     return p && p>=db->lookaside.pStart && p<db->lookaside.pEnd;
(gdb) p/x db->lookaside.pStart
$10 = 0x8a6c918
(gdb) p/x db->lookaside.pEnd
$11 = 0x8a78498
(gdb) p/x p
$12 = 0x8a78b08

sqlite3_free eventually routes through to sqlite3MemFree which results in a 
free() call on the prepared statement. The actual address of the chunk
associated with the sqlite3_stmt is ADDR-8 as sqlite adds 8 to the pointer
returned by malloc and stores the size of the buffer in the first 8 bytes.

(gdb) bt 11
#0  __GI___libc_free (mem=0x8a78b00) at malloc.c:2954
#1  0x0817b588 in sqlite3MemFree (pPrior=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:14795
#2  0x0813cd01 in sqlite3_free (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:18482
#3  0x08144b6f in sqlite3DbFree (db=0x8a6ba58, p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:18512
#4  0x0813c379 in sqlite3VdbeDeleteObject (db=0x8a6ba58, p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59994
#5  0x08184ded in sqlite3VdbeDelete (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:60016
#6  0x08142bdc in sqlite3VdbeFinalize (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59948
#7  0x08142b00 in sqlite3_finalize (pStmt=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:60876
#8  0x0813a828 in php_sqlite3_free_list_dtor (item=0xb7fc3674) at ext/sqlite3/sqlite3.c:2007
#9  0x0849b81d in zend_llist_destroy (l=0xb7fc361c) at Zend/zend_llist.c:112
#10 0x0849b887 in zend_llist_clean (l=0xb7fc361c) at Zend/zend_llist.c:124
(More stack frames follow...)

At this point the sqlite3_stmt has been returned to the system allocator. 
However, the memory is still referenced by the php_sqlite3_stmt object, and 
is accessible through any of the functions on the statement object. In 
trigger.php we demonstrate this via $stmt->reset(). To illustrate this, after
__GI___libc_free has returned we set the db pointer of the sqlite3_stmt
object to be 0x41414141.

(gdb) set {int}0x8a78b08=0x41414141
(gdb) p/x ((Vdbe*)0x8a78b08)->db
$22 = 0x41414141
(gdb) c
Continuing.

The use-after-free condition can then be seen in the handling of the 
$stmt->reset() call. 

Program received signal SIGSEGV, Segmentation fault.
0x081d83ea in sqlite3VdbeHalt (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59669
59669     if( p->db->mallocFailed ){
(gdb) bt 5
#0  0x081d83ea in sqlite3VdbeHalt (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59669
#1  0x08142d40 in sqlite3VdbeReset (p=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:59874
#2  0x08142ce6 in sqlite3_reset (pStmt=0x8a78b08)
    at /home/user/php/ext/sqlite3/libsqlite/sqlite3.c:60898
#3  0x08137f0d in zim_sqlite3stmt_reset (ht=0, return_value=0xb7fc0bec, return_value_ptr=0xb7fa70e0, this_ptr=0xb7fc0bd0, 
    return_value_used=0) at ext/sqlite3/sqlite3.c:1316
#4  0x08592b91 in zend_do_fcall_common_helper_SPEC (execute_data=0xb7fa71ac) at Zend/zend_vm_execute.h:554
(More stack frames follow...)

(gdb) p/x p->db
$25 = 0x41414141

In order to make use of this bug an attacker just needs to reallocate the 
free'd sqlite3_stmt as some controllable datastructure. They are then free to 
rewrite the contents before triggering the UAF via any of the methods of the
$stmt object.

EOF

Test script:
---------------
<?php

$db = new SQLite3(':memory:');

$db->exec('CREATE TABLE foo (id INTEGER, bar STRING)');

$stmt = $db->prepare('SELECT bar FROM foo WHERE id=:id');
// Close the database connection and free the internal sqlite3_stmt object
$db->close();
// Access the sqlite3_stmt object via the php_sqlite3_stmt container
$stmt->reset();

?>


Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2014-01-22 17:07 UTC] sean at persistencelabs dot com
From 8eb7e567ebfc4abf7ffe754370301565796a42fd Mon Sep 17 00:00:00 2001
From: Sean Heelan <sean@persistencelabs.com>
Date: Fri, 20 Dec 2013 21:29:50 +0000
Subject: [PATCH] Check that php_sqlite3_stmt objects are in a valid state
 prior to using them

---
 ext/sqlite3/sqlite3.c |   16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c
index 19bf58d..e7c9726 100644
--- a/ext/sqlite3/sqlite3.c
+++ b/ext/sqlite3/sqlite3.c
@@ -1274,6 +1274,8 @@ PHP_METHOD(sqlite3stmt, paramCount)
 	php_sqlite3_stmt *stmt_obj;
 	zval *object = getThis();
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	if (zend_parse_parameters_none() == FAILURE) {
 		return;
@@ -1290,6 +1292,8 @@ PHP_METHOD(sqlite3stmt, close)
 	php_sqlite3_stmt *stmt_obj;
 	zval *object = getThis();
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	if (zend_parse_parameters_none() == FAILURE) {
 		return;
@@ -1308,6 +1312,8 @@ PHP_METHOD(sqlite3stmt, reset)
 	php_sqlite3_stmt *stmt_obj;
 	zval *object = getThis();
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	if (zend_parse_parameters_none() == FAILURE) {
 		return;
@@ -1328,6 +1334,8 @@ PHP_METHOD(sqlite3stmt, clear)
 	php_sqlite3_stmt *stmt_obj;
 	zval *object = getThis();
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	if (zend_parse_parameters_none() == FAILURE) {
 		return;
@@ -1349,6 +1357,8 @@ PHP_METHOD(sqlite3stmt, readOnly)
 	php_sqlite3_stmt *stmt_obj;
 	zval *object = getThis();
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	if (zend_parse_parameters_none() == FAILURE) {
 		return;
@@ -1416,6 +1426,8 @@ PHP_METHOD(sqlite3stmt, bindParam)
 	zval *object = getThis();
 	struct php_sqlite3_bound_param param = {0};
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	param.param_number = -1;
 	param.type = SQLITE3_TEXT;
@@ -1447,6 +1459,8 @@ PHP_METHOD(sqlite3stmt, bindValue)
 	zval *object = getThis();
 	struct php_sqlite3_bound_param param = {0};
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
+	
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
 
 	param.param_number = -1;
 	param.type = SQLITE3_TEXT;
@@ -1482,6 +1496,8 @@ PHP_METHOD(sqlite3stmt, execute)
 
 	stmt_obj = (php_sqlite3_stmt *)zend_object_store_get_object(object TSRMLS_CC);
 
+	SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3)
+
 	if (zend_parse_parameters_none() == FAILURE) {
 		return;
 	}
-- 
1.7.9.5
 [2014-02-03 18:56 UTC] stas@php.net
-Type: Security +Type: Bug -Package: *General Issues +Package: SQLite related
 [2015-04-06 05:34 UTC] stas@php.net
-Assigned To: +Assigned To: stas
 [2015-04-06 05:46 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=5ae20c624781bdd39ba14b2f856234c168f7ea38
Log: Fix bug #66550 (SQLite prepared statement use-after-free)
 [2015-04-06 05:46 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2015-04-15 18:06 UTC] tyrael@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=bef6df6a7eedc659a7ddfbad1eb457c193ed6bb8
Log: Fix bug #66550 (SQLite prepared statement use-after-free)
 
PHP Copyright © 2001-2025 The PHP Group
All rights reserved.
Last updated: Mon Feb 03 18:01:32 2025 UTC