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
Status: Closed Package: SQLite related
PHP Version: master-Git-2014-01-22 (Git) OS:
Private report: No CVE-ID:
 [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

Add a Patch

Pull Requests

Add a Pull Request

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-2017 The PHP Group
All rights reserved.
Last updated: Tue Aug 29 15:01:52 2017 UTC