Bypassing PHP WAF to Achieve Remote Code Execution In-Depth Analysis

6 min readJul 22, 2023

Before testing to bypass WAF to execute remote code, first construct a simple and vulnerable remote code execution script, as shown in the figure:

Line 6 is an obvious command execution code, and line 3 tries to intercept functions such as system, exec, or pass-thru (there are many other functions in PHP that can execute system commands, these three are the most common).

This script is deployed behind Cloudflare WAF and ModSecurity + OWASP CRS3. For the first test, try to read the contents of passed.

/cfwaf.php?code=system(“cat /etc/passwd”);

As you can see, it was intercepted by CloudFlare, we can try to bypass it by using uninitialized variables, for example:

cat /etc$u/passwd

Cloudflare WAF has been bypassed, but because the script checks sensitive functions, it is blocked by the script, so how to bypass the function detection of the script? Let’s look at the PHP documentation on strings:

PHP string escape sequences:

  • [0–7]{1,3} Character sequence in octal notation that automatically overflows to fit in one byte (e.g. “400” === “�00”)
  • x[0–9A-Fa-f]{1,2} sequence of hexadecimal characters (e.g. “x41”)
  • u{[0–9A-Fa-f]+} A sequence of Unicode code points to output to a string as the UTF-8 representation of that code point (added in PHP 7.0.0)

Not everyone knows PHP’s syntax for representing strings, and “PHP variable functions” became our Swiss Knife to bypass filters and rules.

PHP variable function

PHP supports the concept of variable functions. This means that if parentheses are appended to the variable name, PHP will look for a function with the same name as the variable evaluates to, and try to execute it. Among other things, this can be used to implement callbacks, function tables, etc.

This means that syntax like $var(args), and “sting”(args are equal to func(args). If I can call a function by using a variable or a string, it means that I can use escape sequences instead of functions name. Here is an example:

The third syntax is an escape character sequence of hexadecimal notation, which PHP converts to the string “system”, which is then converted to the function system using the parameter “ls”. Let’s try with a vulnerable script:

This technique does not work with all PHP functions, variable functions do not work with echo, print, unset(), isset(), empty(), include, and require. Use any of these constructs as variadic functions with wrapper functions.

Improved user input detection

What happens if I exclude characters like double quotes and single quotes from user input to the vulnerable script? Is it possible to get around it even without using double quotes? Let’s try:

As you can see on the third line, the script now prevents the use of “and” in the $_GET[code] query string parameter. My previous payload should now be blocked:

Fortunately, in PHP, we don’t always need quotes to denote strings. PHP enables you to declare the type of an element, eg $a = (string)foo, in this case, $a contains the string “foo”. Also, anything inside parentheses without a specific type declaration is considered a string:

In this case, we have two ways to bypass the new filter: the first is to use something like (system)(ls), but we can’t use “system” in the code parameter, so we can do it like ( sy.(st).em)(ls), Same as concatenating strings. The second is to use the $ GET variable. If I send a request like ?a=system&b=ls&code=$ GET a, the result is: $ GET[a] will be replaced by the string “system”, $ GET[b] will be replaced by the string “ls” and I will be able to bypass all filters!

Let’s try with the first payload (sy.(st).em)(whoami);

and the second payload?


Not useful in this case, but you can even insert comments inside function names and parameters (which might help bypass WAF rulesets that block specific PHP function names). All of the following syntaxes are valid:

get_defined_functions function

This PHP function returns a multidimensional array containing a list of all defined functions, both built-in (internal) and user-defined functions. Internal functions can be accessed with $arr[“internal”] and user-defined functions can be accessed with $arr[“user”]. For example:

This could be another way to access system functionality without using its name. If I grep for “system”, I can discover its index number and use that as a string for my code execution:

Obviously, this should work for our Cloudflare WAF and script filters:

Character Array

Every string in PHP can be used as an array of characters (almost like in Python), and you can refer to individual string characters using the syntax $string[2] or $string[-3]. This might be another way to circumvent the rules that block PHP function names. For example, using this string $a=”elmsty/”, I can write a grammar system(“ls /tmp”).

If you’re lucky, you can find all the characters you need in the script filename. Using the same technique, you can select all the characters you want with something like


With OWASP CRS3, everything is even harder. First, using the techniques I’ve seen before, I can only bypass the first level of paranoia, which is amazing! Because Paranoia Level 1 is only a small subset of the rules we can find in CRS3, this level is designed to prevent any false positives. For level 2 paranoia, everything is made difficult due to rule 942430 “Restricted SQL character anomaly detection (args): Number of special characters exceeded”. All I can do is execute a command without parameters like “ls”, “whoami”, etc. But I can’t execute commands like system(“cat /etc/passwd”) as I can with Cloudflare WAF:




Our mission is to get you into information security. We'll introduce you to penetration testing and Red Teaming. We cover network testing, Active Directory.