Disable Functions Bypass - dl Function

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

dl() lets PHP load a shared extension at runtime. If you can make it load an attacker-controlled module, you can register a new PHP function that internally calls execve, system, or any other native primitive and therefore bypass disable_functions.

This is a real primitive, but on modern targets it is far less common than older writeups suggest.

Why this bypass is uncommon today

The main blockers are:

  • dl() must exist and must not be disabled
  • enable_dl must still allow dynamic loading
  • The target SAPI must support dl()
  • The payload must be a valid PHP extension compiled for the same target ABI
  • The extension must be reachable from the configured extension_dir

The official PHP manual is the most important reality check here: dl() is only available for CLI and embed SAPIs, and for the CGI SAPI when run from the command line. That means the technique is usually not available in normal PHP-FPM/mod_php web requests, so check the SAPI before spending time building a payload.

Also note that enable_dl is an INI_SYSTEM setting and, as of PHP 8.3.0, PHP documents it as deprecated, so you usually cannot flip it at runtime from attacker-controlled PHP code.

If dl() is not viable, go back to the broader list of module/version dependent bypasses.

Fast triage from a foothold

Before building anything, collect the exact parameters that the module must match:

<?php
phpinfo();
echo "PHP_VERSION=" . PHP_VERSION . PHP_EOL;
echo "PHP_SAPI=" . php_sapi_name() . PHP_EOL;
echo "ZTS=" . (PHP_ZTS ? "yes" : "no") . PHP_EOL;
echo "INT_BITS=" . (PHP_INT_SIZE * 8) . PHP_EOL;
echo "enable_dl=" . ini_get("enable_dl") . PHP_EOL;
echo "extension_dir=" . ini_get("extension_dir") . PHP_EOL;
echo "disabled=" . ini_get("disable_functions") . PHP_EOL;
?>

What you care about:

  • PHP_SAPI: if this is fpm-fcgi or apache2handler, dl() is usually a dead end for web exploitation
  • extension_dir: the payload must be loaded from here
  • PHP Version, architecture, debug/non-debug, and ZTS/non-ZTS: your module must match them
  • disable_functions: confirm whether dl is absent because it is disabled or because the SAPI does not support it

Practical exploitation constraints

1. You normally need write access to extension_dir

This is the biggest bottleneck.

dl() takes the extension filename, and PHP loads it from extension_dir. In practice, this means that a normal arbitrary file upload to /var/www/html/uploads is not enough. You still need a path to place a .so/.dll where PHP will actually load extensions from.

Realistic situations where this becomes exploitable:

  • CTFs or intentionally weak labs where extension_dir is writable
  • Shared-hosting or container mistakes that expose a writable extension path
  • A separate arbitrary file write primitive that already reaches extension_dir
  • Post-exploitation scenarios where you already escalated enough to drop files there

2. The module must match the target build

Matching only PHP_VERSION is not enough. The extension also needs to match:

  • OS and CPU architecture
  • libc/toolchain expectations
  • ZEND_MODULE_API_NO
  • debug vs non-debug build
  • ZTS vs NTS

If those do not match, dl() will fail or crash the process.

3. open_basedir is not the main defense here

Once you can place the module in extension_dir and call dl(), the extension code executes inside the PHP process. At that point the relevant barrier was not open_basedir, but the ability to land a valid shared object in the extension loading path.

Building the malicious extension

The classic route is still valid:

  1. Recreate the victim build as closely as possible
  2. Use phpize, ./configure, and make to build a shared extension
  3. Export a PHP function such as bypass_exec($cmd) that wraps native command execution
  4. Upload the compiled module into extension_dir
  5. Load it with dl() and call the exported function

The attack is old, but still relevant because PHP 8.x changelogs continue to include dl()-specific crash fixes. The primitive still exists; the hard part is finding a deployment where it is reachable and where you can land a matching module.

Minimal workflow

On the attacker box

mkdir bypass && cd bypass
phpize
./configure
make

El objeto compartido resultante normalmente estará bajo modules/.

Si estás compilando en un entorno diferente al objetivo, considera el archivo producido como un borrador hasta verificar que el ABI coincida con el de la víctima.

Cargando y usando la extensión

Si el objetivo realmente soporta dl() y el módulo está dentro de extension_dir, la parte en tiempo de ejecución es simple:

<?php
if (!extension_loaded('bypass')) {
dl('bypass.so'); // use the correct filename for the target platform
}
echo bypass_exec($_GET['cmd']);
?>

En Windows el nombre de archivo normalmente será .dll, mientras que en sistemas tipo Unix normalmente será .so.

Notas del atacante

  • No asumas que esto funciona remotamente solo porque function_exists("dl") devuelve true en alguna documentación o old writeup; valida el SAPI en vivo
  • Un intento fallido de dl() puede matar al worker de PHP si el módulo es incompatible
  • A partir de PHP 8, las funciones deshabilitadas se eliminan de la tabla de funciones, por lo que la enumeración en userland puede diferir de posts más antiguos
  • Si no puedes escribir en extension_dir, esta técnica suele ser menos práctica que FPM/FastCGI, LD_PRELOAD, o module-specific bypasses ya cubiertos en esta sección

Referencias

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks