Hack The Box – Obscurity Box Writeup By Nikhil Sahoo

Introduction

Hello everyone, I hope everyone is doing well and is safe and is utilizing this time in a meaningful way 🙂

So, back with a new blog. Today we will go through the walkthrough of the Hack the Box machine Obscurity which retired very recently. It was a medium rated Linux box and was the most challenging and interesting box that I have solved up to now. It involves reviewing  3 python files and reversing it to solve the complete box. So without further ado let’s begin…

Recon

We’ll start with our recon by doing a Nmap scan.

nmap -sC -sV 10.10.10.168

Port 22 and 8080 are open so let’s check the 8080 port first in our browser.

So here we would see a message specifying that the source code of the webserver is in SuperSecureServer.py which is present inside some secret directory.

Let’s try bruteforcing the directory since we already know the filename. We’ll be using ffuf tool for this purpose.

./ffuf -u http://10.10.10.168/FUZZ/SuperSecureServer.py -w /path to wordlist/

After a few seconds, we will be getting the secret directory – “develop”.

 

Initial Foothold

Now let’s jump into our browser again and type in http://10.10.10.168/develop/SuperSecureServer.py and copy the code over to our local machine.

 

 

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}

Now here’s a really long code to go through but let’s break it down to smaller parts and understand only the parts that are necessary.

First, let’s go through the parseRequest function of the Request class, this basically is responsible for parsing the request and breaking the different parts of a web request and storing them into multiple variables like method, doc, headerdict, etc. We can take any random web request and try out with that parseRequest function contents to see the output.

Next moving onto the Server class, the listenToClient function is calling the handleRequest function which checks if the request is in the proper format or not and then sends it to the serveDoc function and this is where the vulnerability lies.

 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?

Let’s focus on the above part of code only.  The path variable is URL decoding the URL path first before proceeding. Then in the info variable, it has specified a format in which the URL path is going to get stored. Next comes the exec() function which is where we would be targetting. exec() is basically used in python to run dynamic code specified inside the function.

The exec() function here is executing info.format(path), format() function would simply replace any string given as parameter with the {}. For eg, our info variable stored a string as “output = ‘Document: {}'”, but now if we use info.format(“Hello”) then this basically results into the following output: “output = ‘Document: Hello'”

But we need to execute our own code inside this exec() function, this could be done by closing the output quotes and then specifying our code after the semicolon and commenting out the rest using “#“. But make sure to escape the single quote using a backslash… Let’s try out a very simple example.

If we see the above picture, we can understand that the exec function didn’t find anything to execute at first but it found a command after the semicolon so it executed the code.

Now, all we have to do is use a reverse shell, use it in the same way, URL encode it, and then send the request over to the webserver and we should be getting our shell if the code gets executed by the exec function present in the server-side.

We’ll be using the python reverse shell for this purpose and we would also need to URL encode it. We can do this properly using the urllib.parse.quote() function of python3.

So all set, let’s open our Netcat port for reverse connect and request this particular path…

We should be getting out shell soon.

We’re in !!!

 

Robert User Escalation

If we move inside the Robert user directory we can find a python file named “SuperSecureCrypt.py” and a few other txt files like check.txt, out.txt, and passwordreminder.txt. The out.txt and  passwordreminder.txt are encrypted, but if we open the check.txt file we can find the following text written inside it: “Encrypting this file with your key should result in out.txt, make sure your key is correct!“.

So what we can understand from here is that if we encrypt the file with the proper key then this will result in out.txt which is in an encrypted format and probably this is achieved using the python file.

So let’s bring all of these files over to our local machine. In order to avoid missing out some characters using copy and paste, it is recommended to first convert the contents to base64 and then copy the base64 format to your local machine and decode it.

 

Now let’s analyze the SuperSecureCrypt.py code.

 

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)

This is not as large as the first one and consists of only two functions: encrypt and decrypt used for obvious reasons.

First coming to the encrypt function,

the logic is very simple, it basically takes the first character of the plain text and the key then converts it to their respective ASCII codes using the ord() function and adds them. This result is then passed to a modulus operation with 255 and the final result is again converted back to its corresponding character respective to its ASCII code using the chr() function. This becomes the first character of the encrypted text and the operation is performed again by using the second plain text character with the second character of the key. But the thing to note is that if the key size is small, so once it reaches its last character, for the next character it will again start from its first character. For eg: if the plain text is “Nikhil” and the key is “ab” then this would be the encryption format:

N – a

i – b

k – a

h – b

i  – a

l – b

Similarly for the decryption function,

the algorithm is the same as encrypt function, only thing is that instead of adding it is subtracting the ASCII code of the key character from the ASCII code of the encrypted text character.

 

So what we have is basically, two encrypted files, one clear text file, and what we need currently is the key that is used to encrypt and decrypt.

And here is what we could do in order to retrieve the key.

 

import sys
import argparse


def decrypt(text, plain):
    i = 0
    decrypted = ""
    for x in text:
        newChr = ord(x)
        for y in range(0,255):
           newChr2 = chr((newChr - y) % 255)
           if newChr2 == plain[i]:
           	decrypted += newChr2
           	print(chr(y))
        i+=1
    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('-m',
                    metavar='plain',
                    type=str,
                    help='Plain 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.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()
        with open(args.m, 'r', encoding='UTF-8') as g:
            data2 = g.read()

        print("Decrypting...")
        decrypted = decrypt(data, data2)

        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)

What we have done is we have modified the decrypt function a little bit to retrieve the key. This would take a single character from the encrypted text and convert it to its ASCII code and then we are subtracting all the values of ASCII codes from 0-255 one by one using the loop and performing a modulus operation with 255 and then is converted back to its character format using the chr(). The resulting value is then matched with plain text character of the same index, if there’s a match then we can identify the character of the key and this process would go on till goes through all the characters of the encrypted text.

The original code had a few mandatory arguments like -i for the input file, -o for the output file to store the output, -d was optional used for decryption mode.

For our key cracking purpose, we have added an extra argument in the code: -m which would take the plain text file (check.txt) and the -i would take the encrypted file (out.txt)…-d option should also be provided since we are utilizing the decrypt function.

python3 exploit.py -d -i out.txt -m check.txt -o key.txt

So the key is “alexandrovich

Now let’s use this key to decrypt the passwordreminder.txt file as well.

We’ve got the decrypted text as “SecThruObsFTW“.

Let’s try to ssh using this credential for Robert user.

We’re in and should be getting our user flag 🙂

Privilege Escalation

Now onto root, if we do sudo -l to check what all commands Robert can run as root we will find that BetterSSH.py file could be run as sudo which is present inside /home/robert/BetterSSH/ directory.

Let’s analyze its code:

 

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

To summarize the whole code, it first prompts the user to type in the user and password and then extracts the contents of /etc/shadow file formats it and stores it in a file inside the /tmp/SSH directory with a random file name. The password we entered is then hashed and matched with the hash present inside the new file under /tmp/SSH copied from /etc/shadow before. If the hash matches then it shows the message “Authed!”.

However, the problem is that it is storing the /etc/shadow file contents inside that newly created file till the time authentication check is done. The duration is very small to view the contents so we can monitor continuously and view files accordingly.

while true; do cat * ;done

This would basically go in an endless loop and keep on viewing all the files in the directory continuously, so once the new file is created, its contents are also displayed.

So we got the root hash, let’s try to crack it using John with a wordlist. Let’s use the very common rockyou.txt

john hashfile –wordlist=/path to wordlist

Cracked! The password is “mercedes“.  Let’s try to login as root now

su root

So we got our root flag 🙂 … That explains it all.

So that’s for now. See you next time.

Till then Stay Safe, Stay Home

Happy Hacking !!!

 

You can have a look at my previous article on Hack The Box: OpenAdmin Box Walkthrough. Here is the link of the article

Loved what you read?

If so, then kindly comment, follow and share our website for much more interesting stuff  ?

For any queries you can send a Hi to my Linkedin Handle: Here

You may also like...

1 Response

  1. Ajay says:

    Very good detailed write up. The steps are very descriptive. Keep on the good work.

Leave a Reply

Your email address will not be published. Required fields are marked *