CodeColorist
Bypass PHP Safe Mode by Abusing SQLite3's FTS Tokenizer

Bypass PHP Safe Mode by Abusing SQLite3's FTS Tokenizer

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 system commands 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 are broken.

For example, this exploit abuses an use after free bug to bypass them.

Backgrounds

SQLite3 has a function called fts3_tokenizer to register custom tokenizers for 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]

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 tokenization. By default SQLite uses its built-in simple tokenizer. Developers can also implement their own tokenizers to support languages other than English.

A custom FTS3 tokenizer should implement the following callbacks:

  • xCreate: initialization
  • xDestroy: destructor
  • 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, we should 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: SELECT fts3_tokenizer(<tokenizer-name>); SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>); 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 the Module Base

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 a hash table. The address comes from libsqlite3's .bss section and refers to this:

So a simple SQL query breaks the ASLR.

Arbitrary Code Execution via Callbacks

The following queries will crash sqlite3 REPL (for 32bit, use x'41414141' instead):

Use a debugger to view the context:

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

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

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

So we can craft a target address on a predictable memory location, pass the location to fts3_tokenizer, trigger the callback, then program counter is hijacked. Yep! It can be a global variable 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's address can be calculated from previously leaked simpleTokenizer.

The pseudo code to describe the exploit:

Exploiting 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. 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 (mod_php.so) in Apache2's worker processes, who have full protection enabled.

Without a proper gadget for stack pivoting, sadly I only have one chance to call. xOpen looks good for PC-control. Its second param is a string from SQL which can be fully controlled.

Here's a gadget to call popen:

To set both xCreate and xOpen, we need at least 3 continuous QWORDs to be controllable. But the PRAGMA clause only sets one. Heap spray fits the need, except it can't always hit because of alignment. Sending multiply requests is acceptable, and it worked.

Another reliable 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.

Linux ubuntu 3.19.0-44-generic #50-Ubuntu SMP Mon Jan 4 18:37:30 UTC 2016 x86_64 Apache/2.4.10 (Ubuntu) PHP Version 5.6.4-4ubuntu6.4

POC source code:

https://github.com/chichou/badtokenizerpoc

Demo:

https://asciinema.org/a/7tj88jfqb0xg6bdnjsu427fkx

References:

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

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