Image for post
Image for post

Bypass PHP safe mode by abusing SQLite3's FTS tokenizer

Jan 20, 2016 · 7 min read

As a pentester, once you own a webshell you may need to get more access by running extra programs. But disable_functions may stop you from invoking any system command and probably open_basedir was set as well. PHP doesn’t actually have a sandbox, so these restrictions have no effect on native code. If there were a bug that leads to code execution, the access control policies can be easily broken. For example, this exploit exploits a use after free bug to execute arbitrary code.

SQLite3 has a function called fts3_tokenizer to register custom tokenizer for built-in full-text search. The FTS3 and FTS4 extension modules allows users to create special tables with a built-in full-text index (hereafter “FTS tables”). The full-text index allows the user to efficiently query the database for all rows that contain one or more words (hereafter “tokens”), even if the table contains many large documents.[1]

The function fts3_tokenizer

To implement full-text search in SQLite3, create a virtual index table, insert records to it to build index, then search keywords with MATCH clause. Both indexing and searching requires tokenizing user input. By default SQLite uses its built-in “simple” tokenizer. Developers can also implement their own tokenizer to support languages other than English.

A custom FTS3 tokenizer should implement following callbacks:

  • xCreate: init tokenizer
  • xDestroy: destroy tokenizer
  • xOpen: create a new tokenize cursor from a user input
  • xClose: close the cursor
  • xNext: yield next word

These callbacks are registered in a sqlite3_tokenizer_module struct, declared as follow:

When the tokenizer is ready, register it to the SQLite.

FTS does not expose a C-function that users call to register new tokenizer types with a database handle. Instead, the pointer must be encoded as an SQL blob value and passed to FTS through the SQL engine by evaluating a special scalar function, “fts3_tokenizer()”. The fts3_tokenizer() function may be called with one or two arguments, as follows:

Where is a string identifying the tokenizer and is a pointer to an sqlite3_tokenizer_module structure encoded as an SQL blob. If the second argument is present, it is registered as tokenizer and a copy of it returned. If only one argument is passed, a pointer to the tokenizer implementation currently registered as is returned, encoded as a blob. Or, if no such tokenizer exists, an SQL exception (error) is raised.[1]

You may notice that there’s a security warning in SQLite’s official document. Actually we can abuse fts3_tokenizer to execute arbitrary code, and even break a modern system protection.

Leak base address with a simple query

SQLite3 has a few built-in tokenizers, like simple, porter and unicode61. The query below returns a hex string represents a big-endian address:

In ext/fts3/fts3.c it loads the built-in tokenizers into the hash table. Actually the address comes from libsqlite3's .bss section and refers to this:

So we can read libsqlite3's base address by a simple SQL query, which breaks ASLR.

Arbitrary code execution via callbacks

Following queries will crash sqlite3 shell (for 32bit sqlite, use “x’41414141' instead):

Use a debugger to view the context:

RAX is the second parameter from fts3_tokenizer. SQLite3 called the xCreate callback without validation and caused the segment fault. This refers to sqlite3Fts3InitTokenizer in ext/fts3/fts3_tokenizer.c.

...lines omitted...

Assume the virtual table named fulltext has already been created successfully. This query triggers xOpen callback with the string "text goes here" as pInput parameter:

insert into fulltext values("text goes here");

Sources in ext/fts3/fts3_expr.c, function sqlite3Fts3OpenTokenizer:

So we craft a target address on a known memory location, pass the location to fts3_tokenizer to register, trigger the callback, then program counter is hijacked. Yep!

So where the location should be? It can be any variable with known address, for example, global variables in .bss segment, or use the heap spray technique.

Commit e36e9c introduced the soft_heap pragma for limiting the size of heap memory pool. It accepts a 64-bit number set the global variable mem0.alarmThreshold to the given value. This global variable locates in .bss segment and its absolute address can be calculated from previously leaked simpleTokenizer's address.

The pseudo code to describe how to exploit:

Exploit PHP

The SQLite3 extension is enabled by default as of PHP 5.3.0. It's possible to disable it by using --without-sqlite3 at compile time.[2] The extension is compiled with FTS so there's an attack surface to exploit. We don't even have to create a file since SQLite supports in memory database.

PHP does not come with PIE, but apache2 does. PHP interpreter is loaded as a shared object in Apache2 worker processes, who have full protection enabled.

Without a proper gadget for stack pivoting, sadly I only have one chance to call. If we make a direct call to system or so, passing the argument will be another problem, because xCreate is declared like this:

But xOpen looks good. Its second param is a string from SQL which can be fully controlled.

Here's a code snippet from libphp who moves rsi to rdi, then calls popen.

In case of crash, point the xCreate to a valid function like simpleCreate. To set both xCreate and xOpen, we need at least 3 continuous QWORDs (actually xDestroy can be any value) controllable. But the PRAGMA clause only sets one QWORD.

Heap spray fits the limit, except it can't always hit because of alignment. Sending multiply requests is acceptable, and it worked.

Another way is to set PHP.ini entries. In almost every PHP module or extension's source we see the ZEND_BEGIN_MODULE_GLOBALS macro. It stores "global" variables per module scope, and these data are on .bss segment so their locations are predictable. Here's an example picked from mysqlnd.h:

The type zend_long is an alias for int64 on 64bit system, now we can craft the module struct by manipulating php.ini entries. In most cases the function ini_set is disabled, but this could be bypass once the httpd.conf enables AllowOverride.

When using PHP as an Apache module, you can also change the configuration settings using directives in Apache configuration files (e.g. httpd.conf) and .htaccess files. You will need "AllowOverride Options" or "AllowOverride All" privileges to do so.

There are several Apache directives that allow you to change the PHP configuration from within the Apache configuration files. For a listing of which directives are PHP_INI_ALL, PHP_INI_PERDIR, or PHP_INI_SYSTEM, have a look at the List of php.ini directives appendix. [3]

Since we already have the permission to write and execute a webshell, it's not a problem to put another .htaccess file inside the same directory.

The exploit requires two requests. The former leak the address and generate a .htaccess file with directives to craft callback addresses. The later trigger system command by inserting into virtual table.

Here's the test environment.

POC source code:



[1] SQLite FTS3 and FTS4 Extensions
[2] PHP: SQLite3 Installation
[3] How to Change Configuration Settings

中文版:特性还是漏洞?滥用 SQLite 分词器


I write random stuff

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store