Disable Functions Bypass - dl Function

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support 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

The resulting shared object will usually be under modules/.

If you are building on a different environment than the target, treat the produced file as a draft until you verify that the ABI matches the victim.

Loading and using the extension

If the target really supports dl() and the module is inside extension_dir, the runtime side is simple:

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

On Windows the filename will typically be a .dll, while on Unix-like targets it will usually be a .so.

Attacker notes

  • Do not assume this works remotely just because function_exists("dl") returns true in some documentation or old writeup; validate the live SAPI
  • A failed dl() attempt may kill the PHP worker if the module is incompatible
  • From PHP 8 onward, disabled functions are removed from the function table, so userland enumeration may differ from older posts
  • If you cannot write to extension_dir, this technique is usually less practical than FPM/FastCGI, LD_PRELOAD, or module-specific bypasses already covered in this section

References

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks