the_open_door
coffee
ESEN
← Back to index

PHP preg_replace() /e RCE

preg_replace() is PHP's regular-expression replacement function. For years it accepted the /e (eval) modifier, which changed its behaviour completely: instead of treating the replacement as text, it evaluated the replacement as PHP code. If any part of that code comes from user-controlled input, you get remote code execution.

The vulnerable pattern looks like this:

// the /e modifier evaluates the third argument as PHP
preg_replace('/(.*)/e', 'strtoupper("\\1")', $_GET['content']);

Because the text captured by the regex is interpolated into the evaluated code, it is enough to send a content value that closes the call and injects PHP. The classic trigger is \1 (or $1), which inserts the captured group at the point where the eval happens:

# the replacement becomes:  strtoupper("system('id')")
content = system('id')

In a real scenario (say an API that updates a post's content with preg_replace('/.../e', $text, ...)), you inject a system('whoami') first to confirm execution and then persist a webshell without relying on characters that break on the command line:

file_put_contents('cmd.php', base64_decode('<webshell in base64>'))

base64_decode prevents quotes, < or > from breaking the string; the result is a stable cmd.php?cmd= from which you launch the reverse shell.

Requirements. PHP < 7 (the /e modifier was deprecated in 5.5 and removed in PHP 7) and a preg_replace call whose pattern or replacement depends on user input.

How to detect it. The access-log signature is a parameter with PHP function calls where text should be: system(, passthru(, file_put_contents(, base64_decode(, which no legitimate input carries; a WAF inspecting for those flags it. Confirmation is the web process (php/apache2) spawning sh/bash as a child after the request (auditd/Sysmon for Linux) and a new .php appearing in the webroot (FIM), the persisted webshell. A PHP < 7 banner is the preceding exposure.

Defence. Never use /e; its replacement is preg_replace_callback(), where the replacement is a function and no text is evaluated. Upgrading to PHP 7+ removes the vector at the root. Connects with command injection and webshell upload.

Machines where it's used